From e93528b13c8be1814657d8b8fbe73228de1afb97 Mon Sep 17 00:00:00 2001
From: Shreyash Gupta <48386323+shreyashkgupta@users.noreply.github.com>
Date: Thu, 12 Dec 2024 22:17:34 +0530
Subject: [PATCH 1/2] feat: Spacer block
---
.../src/components/blocks/SpacerBlock.tsx | 59 +++++++++++++++++++
.../src/components/editor/AIRewriteButton.tsx | 15 +++++
.../src/components/editor/AddBlockButton.tsx | 2 +
.../components/editor/BlockFormatToolbar.tsx | 2 +-
.../src/lib/renderers/BlockRenderer.tsx | 9 +++
packages/akiradocs/src/types/Block.ts | 2 +-
.../src/app/editMode/[...slug]/page.tsx | 5 ++
.../src/components/blocks/SpacerBlock.tsx | 59 +++++++++++++++++++
.../src/components/editor/AIRewriteButton.tsx | 15 +++++
.../src/components/editor/AddBlockButton.tsx | 2 +
.../components/editor/BlockFormatToolbar.tsx | 2 +-
.../src/lib/renderers/BlockRenderer.tsx | 9 +++
packages/editor/src/types/Block.ts | 2 +-
13 files changed, 179 insertions(+), 4 deletions(-)
create mode 100644 packages/akiradocs/src/components/blocks/SpacerBlock.tsx
create mode 100644 packages/editor/src/components/blocks/SpacerBlock.tsx
diff --git a/packages/akiradocs/src/components/blocks/SpacerBlock.tsx b/packages/akiradocs/src/components/blocks/SpacerBlock.tsx
new file mode 100644
index 0000000..5fdd01e
--- /dev/null
+++ b/packages/akiradocs/src/components/blocks/SpacerBlock.tsx
@@ -0,0 +1,59 @@
+import { cn } from '@/lib/utils'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
+import { useState } from 'react'
+
+interface SpacerBlockProps {
+ size?: 'small' | 'medium' | 'large' | 'xlarge'
+ isEditing?: boolean
+ onUpdate?: (size: string) => void
+}
+
+const spacingSizes = {
+ small: 'h-4',
+ medium: 'h-8',
+ large: 'h-16',
+ xlarge: 'h-24'
+}
+
+export function SpacerBlock({ size = 'medium', isEditing, onUpdate }: SpacerBlockProps) {
+ const [isFocused, setIsFocused] = useState(false)
+
+ if (!isEditing) {
+ return
+ }
+
+ return (
+ setIsFocused(true)}
+ onBlur={(e) => {
+ if (!e.currentTarget.contains(e.relatedTarget)) {
+ setIsFocused(false)
+ }
+ }}
+ >
+
+ {isFocused && (
+
+ )}
+
+ )
+}
\ No newline at end of file
diff --git a/packages/akiradocs/src/components/editor/AIRewriteButton.tsx b/packages/akiradocs/src/components/editor/AIRewriteButton.tsx
index eb1c654..33e9954 100644
--- a/packages/akiradocs/src/components/editor/AIRewriteButton.tsx
+++ b/packages/akiradocs/src/components/editor/AIRewriteButton.tsx
@@ -193,6 +193,13 @@ const blockStyles = {
label: 'Descriptive',
prompt: 'Rewrite the API reference to be more descriptive and engaging.'
}
+ ],
+ spacer: [
+ {
+ value: 'default',
+ label: 'Default',
+ prompt: 'No rewriting options available for spacer'
+ }
]
} as const;
@@ -213,6 +220,10 @@ export function AIRewriteButton({ onRewrite, blockType, isRewriting }: AIRewrite
};
const handleRewrite = async () => {
+ if (blockType === 'spacer') {
+ return;
+ }
+
try {
await onRewrite(style)
} catch (error) {
@@ -224,6 +235,10 @@ export function AIRewriteButton({ onRewrite, blockType, isRewriting }: AIRewrite
}
}
+ if (blockType === 'spacer') {
+ return null;
+ }
+
return (
diff --git a/packages/akiradocs/src/components/editor/AddBlockButton.tsx b/packages/akiradocs/src/components/editor/AddBlockButton.tsx
index 00a1770..5eb1ffd 100644
--- a/packages/akiradocs/src/components/editor/AddBlockButton.tsx
+++ b/packages/akiradocs/src/components/editor/AddBlockButton.tsx
@@ -29,6 +29,7 @@ import {
Music,
File,
CheckSquare,
+ ArrowUpDown,
} from 'lucide-react'
interface AddBlockButtonProps {
@@ -83,6 +84,7 @@ export const AddBlockButton = forwardRef<
{ type: 'audio', icon: , label: 'Audio', description: 'Embed audio content.', group: 'Media' },
{ type: 'file', icon: , label: 'File', description: 'Upload or link to a file.', group: 'Media' },
{ type: 'checkList', icon: , label: 'To-do list', description: 'Track tasks with a to-do list.', group: 'Basic' },
+ { type: 'spacer', icon: , label: 'Spacing', description: 'Add vertical space between blocks.', group: 'Basic' },
]
const filteredOptions = blockOptions.filter((option) =>
diff --git a/packages/akiradocs/src/components/editor/BlockFormatToolbar.tsx b/packages/akiradocs/src/components/editor/BlockFormatToolbar.tsx
index 8a70ee5..0d16b41 100644
--- a/packages/akiradocs/src/components/editor/BlockFormatToolbar.tsx
+++ b/packages/akiradocs/src/components/editor/BlockFormatToolbar.tsx
@@ -120,7 +120,7 @@ export function BlockFormatToolbar({
audioContent,
onAudioMetadataChange,
}: BlockFormatToolbarProps) {
- if (blockType === 'file') {
+ if (blockType === 'file' || blockType === 'spacer') {
return null;
}
diff --git a/packages/akiradocs/src/lib/renderers/BlockRenderer.tsx b/packages/akiradocs/src/lib/renderers/BlockRenderer.tsx
index b640646..e81b244 100644
--- a/packages/akiradocs/src/lib/renderers/BlockRenderer.tsx
+++ b/packages/akiradocs/src/lib/renderers/BlockRenderer.tsx
@@ -22,6 +22,7 @@ import { Table } from '@/components/blocks/TableBlock'
import { VideoBlock } from "@/components/blocks/VideoBlock"
import { AudioBlock } from "@/components/blocks/AudioBlock"
import { FileBlock } from "@/components/blocks/FileBlock"
+import { SpacerBlock } from "@/components/blocks/SpacerBlock"
interface ImageBlockContent {
url: string;
@@ -234,6 +235,14 @@ export function BlockRenderer({ block, isEditing, onUpdate }: BlockRendererProps
onUpdate={(content) => onUpdate?.(block.id, content)}
/>
);
+ case 'spacer':
+ return (
+ onUpdate?.(block.id, size)}
+ />
+ );
default:
return null
}
diff --git a/packages/akiradocs/src/types/Block.ts b/packages/akiradocs/src/types/Block.ts
index 61f1b7a..ec9f5fc 100644
--- a/packages/akiradocs/src/types/Block.ts
+++ b/packages/akiradocs/src/types/Block.ts
@@ -12,8 +12,8 @@ export type BlockType =
| 'video'
| 'audio'
| 'file'
- // | 'emoji'
| 'callout'
+ | 'spacer'
| 'apiReference';
export interface Block {
diff --git a/packages/editor/src/app/editMode/[...slug]/page.tsx b/packages/editor/src/app/editMode/[...slug]/page.tsx
index 116dbff..03e3046 100644
--- a/packages/editor/src/app/editMode/[...slug]/page.tsx
+++ b/packages/editor/src/app/editMode/[...slug]/page.tsx
@@ -157,6 +157,11 @@ export default function ArticleEditorContent({ params }: { params: Promise<{ slu
metadata: {}
}
+ // Add default content for spacer blocks
+ if (newBlock.type === 'spacer') {
+ newBlock.content = 'medium'
+ }
+
if (newBlock.type === 'table') {
newBlock.metadata = {
headers: ['Column 1', 'Column 2'],
diff --git a/packages/editor/src/components/blocks/SpacerBlock.tsx b/packages/editor/src/components/blocks/SpacerBlock.tsx
new file mode 100644
index 0000000..5fdd01e
--- /dev/null
+++ b/packages/editor/src/components/blocks/SpacerBlock.tsx
@@ -0,0 +1,59 @@
+import { cn } from '@/lib/utils'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
+import { useState } from 'react'
+
+interface SpacerBlockProps {
+ size?: 'small' | 'medium' | 'large' | 'xlarge'
+ isEditing?: boolean
+ onUpdate?: (size: string) => void
+}
+
+const spacingSizes = {
+ small: 'h-4',
+ medium: 'h-8',
+ large: 'h-16',
+ xlarge: 'h-24'
+}
+
+export function SpacerBlock({ size = 'medium', isEditing, onUpdate }: SpacerBlockProps) {
+ const [isFocused, setIsFocused] = useState(false)
+
+ if (!isEditing) {
+ return
+ }
+
+ return (
+ setIsFocused(true)}
+ onBlur={(e) => {
+ if (!e.currentTarget.contains(e.relatedTarget)) {
+ setIsFocused(false)
+ }
+ }}
+ >
+
+ {isFocused && (
+
+ )}
+
+ )
+}
\ No newline at end of file
diff --git a/packages/editor/src/components/editor/AIRewriteButton.tsx b/packages/editor/src/components/editor/AIRewriteButton.tsx
index eb1c654..33e9954 100644
--- a/packages/editor/src/components/editor/AIRewriteButton.tsx
+++ b/packages/editor/src/components/editor/AIRewriteButton.tsx
@@ -193,6 +193,13 @@ const blockStyles = {
label: 'Descriptive',
prompt: 'Rewrite the API reference to be more descriptive and engaging.'
}
+ ],
+ spacer: [
+ {
+ value: 'default',
+ label: 'Default',
+ prompt: 'No rewriting options available for spacer'
+ }
]
} as const;
@@ -213,6 +220,10 @@ export function AIRewriteButton({ onRewrite, blockType, isRewriting }: AIRewrite
};
const handleRewrite = async () => {
+ if (blockType === 'spacer') {
+ return;
+ }
+
try {
await onRewrite(style)
} catch (error) {
@@ -224,6 +235,10 @@ export function AIRewriteButton({ onRewrite, blockType, isRewriting }: AIRewrite
}
}
+ if (blockType === 'spacer') {
+ return null;
+ }
+
return (
diff --git a/packages/editor/src/components/editor/AddBlockButton.tsx b/packages/editor/src/components/editor/AddBlockButton.tsx
index 00a1770..5eb1ffd 100644
--- a/packages/editor/src/components/editor/AddBlockButton.tsx
+++ b/packages/editor/src/components/editor/AddBlockButton.tsx
@@ -29,6 +29,7 @@ import {
Music,
File,
CheckSquare,
+ ArrowUpDown,
} from 'lucide-react'
interface AddBlockButtonProps {
@@ -83,6 +84,7 @@ export const AddBlockButton = forwardRef<
{ type: 'audio', icon: , label: 'Audio', description: 'Embed audio content.', group: 'Media' },
{ type: 'file', icon: , label: 'File', description: 'Upload or link to a file.', group: 'Media' },
{ type: 'checkList', icon: , label: 'To-do list', description: 'Track tasks with a to-do list.', group: 'Basic' },
+ { type: 'spacer', icon: , label: 'Spacing', description: 'Add vertical space between blocks.', group: 'Basic' },
]
const filteredOptions = blockOptions.filter((option) =>
diff --git a/packages/editor/src/components/editor/BlockFormatToolbar.tsx b/packages/editor/src/components/editor/BlockFormatToolbar.tsx
index 8a70ee5..0d16b41 100644
--- a/packages/editor/src/components/editor/BlockFormatToolbar.tsx
+++ b/packages/editor/src/components/editor/BlockFormatToolbar.tsx
@@ -120,7 +120,7 @@ export function BlockFormatToolbar({
audioContent,
onAudioMetadataChange,
}: BlockFormatToolbarProps) {
- if (blockType === 'file') {
+ if (blockType === 'file' || blockType === 'spacer') {
return null;
}
diff --git a/packages/editor/src/lib/renderers/BlockRenderer.tsx b/packages/editor/src/lib/renderers/BlockRenderer.tsx
index ba5d2a9..6117c55 100644
--- a/packages/editor/src/lib/renderers/BlockRenderer.tsx
+++ b/packages/editor/src/lib/renderers/BlockRenderer.tsx
@@ -22,6 +22,7 @@ import { Table } from '@/components/blocks/TableBlock'
import { VideoBlock } from "@/components/blocks/VideoBlock"
import { AudioBlock } from "@/components/blocks/AudioBlock"
import { FileBlock } from "@/components/blocks/FileBlock"
+import { SpacerBlock } from "@/components/blocks/SpacerBlock"
interface ImageBlockContent {
url: string;
@@ -230,6 +231,14 @@ export function BlockRenderer({ block, isEditing, onUpdate }: BlockRendererProps
onUpdate={(content) => onUpdate?.(block.id, content)}
/>
);
+ case 'spacer':
+ return (
+ onUpdate?.(block.id, size)}
+ />
+ );
default:
return null
}
diff --git a/packages/editor/src/types/Block.ts b/packages/editor/src/types/Block.ts
index 61f1b7a..ec9f5fc 100644
--- a/packages/editor/src/types/Block.ts
+++ b/packages/editor/src/types/Block.ts
@@ -12,8 +12,8 @@ export type BlockType =
| 'video'
| 'audio'
| 'file'
- // | 'emoji'
| 'callout'
+ | 'spacer'
| 'apiReference';
export interface Block {
From 35b3c9a0a14b013ddd9a9568c23c7cd0cd909156 Mon Sep 17 00:00:00 2001
From: Shreyash Gupta <48386323+shreyashkgupta@users.noreply.github.com>
Date: Fri, 13 Dec 2024 00:46:25 +0530
Subject: [PATCH 2/2] feat: Button block
---
.../src/components/blocks/ButtonBlock.tsx | 103 ++++++++++++++++++
.../src/components/blocks/SortableBlock.tsx | 21 ++++
.../src/components/editor/AIRewriteButton.tsx | 12 ++
.../src/components/editor/AddBlockButton.tsx | 9 +-
.../components/editor/BlockFormatToolbar.tsx | 97 ++++++++++++++++-
.../src/lib/renderers/BlockRenderer.tsx | 28 +++--
packages/akiradocs/src/types/Block.ts | 9 +-
.../src/app/editMode/[...slug]/page.tsx | 12 ++
.../src/components/blocks/ButtonBlock.tsx | 103 ++++++++++++++++++
.../src/components/blocks/SortableBlock.tsx | 21 ++++
.../src/components/editor/AIRewriteButton.tsx | 12 ++
.../src/components/editor/AddBlockButton.tsx | 9 +-
.../components/editor/BlockFormatToolbar.tsx | 97 ++++++++++++++++-
.../src/lib/renderers/BlockRenderer.tsx | 20 ++--
packages/editor/src/types/Block.ts | 9 +-
15 files changed, 520 insertions(+), 42 deletions(-)
create mode 100644 packages/akiradocs/src/components/blocks/ButtonBlock.tsx
create mode 100644 packages/editor/src/components/blocks/ButtonBlock.tsx
diff --git a/packages/akiradocs/src/components/blocks/ButtonBlock.tsx b/packages/akiradocs/src/components/blocks/ButtonBlock.tsx
new file mode 100644
index 0000000..a54e8b1
--- /dev/null
+++ b/packages/akiradocs/src/components/blocks/ButtonBlock.tsx
@@ -0,0 +1,103 @@
+import { cn } from "@/lib/utils"
+import { Button, buttonVariants } from "@/components/ui/button"
+import { useState } from "react"
+
+interface ButtonBlockProps {
+ content: string
+ metadata?: {
+ buttonUrl?: string
+ buttonStyle?: {
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
+ size?: 'default' | 'sm' | 'lg'
+ radius?: 'none' | 'sm' | 'md' | 'lg' | 'full'
+ }
+ }
+ isEditing?: boolean
+ onUpdate?: (content: string) => void
+ align?: 'left' | 'center' | 'right'
+}
+
+export function ButtonBlock({
+ content,
+ metadata,
+ isEditing,
+ onUpdate,
+ align = 'left'
+}: ButtonBlockProps) {
+ const [isFocused, setIsFocused] = useState(false);
+ const buttonStyle = metadata?.buttonStyle || {}
+ const url = metadata?.buttonUrl || '#'
+
+ const buttonClasses = cn(
+ "relative",
+ {
+ 'rounded-none': buttonStyle.radius === 'none',
+ 'rounded-sm': buttonStyle.radius === 'sm',
+ 'rounded-md': buttonStyle.radius === 'md',
+ 'rounded-lg': buttonStyle.radius === 'lg',
+ 'rounded-full': buttonStyle.radius === 'full',
+ 'w-full': buttonStyle.size === 'lg',
+ 'w-24': buttonStyle.size === 'sm',
+ 'w-auto': buttonStyle.size === 'default'
+ }
+ )
+
+ const wrapperClasses = cn(
+ 'w-full',
+ {
+ 'text-left': align === 'left',
+ 'text-center': align === 'center',
+ 'text-right': align === 'right'
+ }
+ )
+
+ if (isEditing) {
+ return (
+
+
+
+ onUpdate?.(e.target.value)}
+ onFocus={() => setIsFocused(true)}
+ onBlur={() => setIsFocused(false)}
+ className={cn(
+ buttonVariants({
+ variant: buttonStyle.variant || 'default',
+ size: buttonStyle.size || 'default'
+ }),
+ buttonClasses,
+ "absolute inset-0 border-0 outline-none",
+ !isFocused && "opacity-0"
+ )}
+ placeholder="Button text..."
+ />
+
+
+ )
+ }
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/packages/akiradocs/src/components/blocks/SortableBlock.tsx b/packages/akiradocs/src/components/blocks/SortableBlock.tsx
index 9afa175..020e810 100644
--- a/packages/akiradocs/src/components/blocks/SortableBlock.tsx
+++ b/packages/akiradocs/src/components/blocks/SortableBlock.tsx
@@ -35,6 +35,12 @@ interface SortableBlockProps {
align?: 'left' | 'center' | 'right'
type?: 'info' | 'warning' | 'success' | 'error'
title?: string
+ buttonUrl?: string
+ buttonStyle?: {
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
+ size?: 'default' | 'sm' | 'lg'
+ radius?: 'none' | 'sm' | 'md' | 'lg' | 'full'
+ }
}
}
updateBlock: (id: string, content: string) => void
@@ -514,6 +520,21 @@ export function SortableBlock({
updateBlock(block.id, JSON.stringify(updatedContent));
}
}}
+ showButtonControls={block.type === 'button'}
+ buttonMetadata={{
+ url: block.metadata?.buttonUrl,
+ style: block.metadata?.buttonStyle
+ }}
+ onButtonMetadataChange={(metadata) => {
+ updateBlockMetadata(block.id, {
+ ...block.metadata,
+ buttonUrl: metadata.buttonUrl,
+ buttonStyle: {
+ ...block.metadata?.buttonStyle,
+ ...metadata.buttonStyle
+ }
+ })
+ }}
/>
)}
, label: 'File', description: 'Upload or link to a file.', group: 'Media' },
{ type: 'checkList', icon: , label: 'To-do list', description: 'Track tasks with a to-do list.', group: 'Basic' },
{ type: 'spacer', icon: , label: 'Spacing', description: 'Add vertical space between blocks.', group: 'Basic' },
+ { type: 'button', icon: , label: 'Button', description: 'Add a clickable button with link.', group: 'Basic' },
]
const filteredOptions = blockOptions.filter((option) =>
diff --git a/packages/akiradocs/src/components/editor/BlockFormatToolbar.tsx b/packages/akiradocs/src/components/editor/BlockFormatToolbar.tsx
index 0d16b41..e575d98 100644
--- a/packages/akiradocs/src/components/editor/BlockFormatToolbar.tsx
+++ b/packages/akiradocs/src/components/editor/BlockFormatToolbar.tsx
@@ -76,6 +76,23 @@ interface BlockFormatToolbarProps {
caption: string;
alignment: 'left' | 'center' | 'right';
}>) => void;
+ showButtonControls?: boolean;
+ buttonMetadata?: {
+ url?: string;
+ style?: {
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
+ size?: 'default' | 'sm' | 'lg';
+ radius?: 'none' | 'sm' | 'md' | 'lg' | 'full';
+ };
+ };
+ onButtonMetadataChange?: (metadata: Partial<{
+ buttonUrl: string;
+ buttonStyle: {
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
+ size?: 'default' | 'sm' | 'lg';
+ radius?: 'none' | 'sm' | 'md' | 'lg' | 'full';
+ };
+ }>) => void;
}
export function BlockFormatToolbar({
@@ -119,6 +136,9 @@ export function BlockFormatToolbar({
showAudioControls = false,
audioContent,
onAudioMetadataChange,
+ showButtonControls = false,
+ buttonMetadata,
+ onButtonMetadataChange,
}: BlockFormatToolbarProps) {
if (blockType === 'file' || blockType === 'spacer') {
return null;
@@ -133,7 +153,7 @@ export function BlockFormatToolbar({
"bg-popover border shadow-md rounded-md",
className
)}>
- {!showImageControls && !showCodeControls && !showVideoControls && !showAudioControls && blockType !== 'table' && (
+ {!showImageControls && !showCodeControls && !showVideoControls && !showAudioControls && !showButtonControls && blockType !== 'table' && (
<>
)}
+ {showButtonControls && (
+ <>
+
+
+ onButtonMetadataChange?.({ buttonUrl: e.target.value })}
+ placeholder="URL"
+ className="h-7 w-32 text-xs"
+ />
+
+
+
+
+
+
+ >
+ )}
+
{!showImageControls && !showVideoControls && !showAudioControls && (
<>
{!showCalloutControls && blockType !== 'table' && }
diff --git a/packages/akiradocs/src/lib/renderers/BlockRenderer.tsx b/packages/akiradocs/src/lib/renderers/BlockRenderer.tsx
index e81b244..6247349 100644
--- a/packages/akiradocs/src/lib/renderers/BlockRenderer.tsx
+++ b/packages/akiradocs/src/lib/renderers/BlockRenderer.tsx
@@ -6,23 +6,15 @@ import { List } from "@/components/blocks/ListBlock"
import { Blockquote } from "@/components/blocks/BlockquoteBlock"
import { Divider } from "@/components/blocks/DividerBlock"
import { CodeBlock } from "@/components/blocks/CodeBlock"
-// import { Image } from '../blocks/Image'
-// import { Table } from '../blocks/Table'
-// import { ToggleList } from '../blocks/ToggleList'
import { CheckList } from "@/components/blocks/CheckListBlock"
-// import { Video } from '../blocks/Video'
-// import { Audio } from '../blocks/Audio'
-// import { File } from '../blocks/File'
-// import { Emoji } from '../blocks/Emoji'
import { Callout } from "@/components/blocks/CalloutBlock"
-import { cn } from '@/lib/utils'
-import { ErrorBoundary } from 'react-error-boundary'
import { ImageBlock } from "@/components/blocks/ImageBlock"
import { Table } from '@/components/blocks/TableBlock'
import { VideoBlock } from "@/components/blocks/VideoBlock"
import { AudioBlock } from "@/components/blocks/AudioBlock"
import { FileBlock } from "@/components/blocks/FileBlock"
import { SpacerBlock } from "@/components/blocks/SpacerBlock"
+import { ButtonBlock } from "@/components/blocks/ButtonBlock"
interface ImageBlockContent {
url: string;
@@ -65,20 +57,16 @@ export function BlockRenderer({ block, isEditing, onUpdate }: BlockRendererProps
);
case 'heading':
- const hasStrong = block.content.includes('');
return (
onUpdate?.(block.id, content)}
>
- {hasStrong ? block.content.replace(/<\/?strong>/g, '') : block.content}
+ {block.content}
);
case 'list':
@@ -243,6 +231,16 @@ export function BlockRenderer({ block, isEditing, onUpdate }: BlockRendererProps
onUpdate={(size) => onUpdate?.(block.id, size)}
/>
);
+ case 'button':
+ return (
+ onUpdate?.(block.id, content)}
+ align={block.metadata?.align}
+ />
+ );
default:
return null
}
diff --git a/packages/akiradocs/src/types/Block.ts b/packages/akiradocs/src/types/Block.ts
index ec9f5fc..044cc58 100644
--- a/packages/akiradocs/src/types/Block.ts
+++ b/packages/akiradocs/src/types/Block.ts
@@ -14,7 +14,8 @@ export type BlockType =
| 'file'
| 'callout'
| 'spacer'
- | 'apiReference';
+ | 'apiReference'
+ | 'button';
export interface Block {
id: string;
@@ -44,6 +45,12 @@ export interface Block {
align?: 'left' | 'center' | 'right'; // For alignment
type?: 'info' | 'warning' | 'success' | 'error'; // For callouts
title?: string; // For callouts
+ buttonUrl?: string;
+ buttonStyle?: {
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
+ size?: 'default' | 'sm' | 'lg';
+ radius?: 'none' | 'sm' | 'md' | 'lg' | 'full';
+ };
};
}
diff --git a/packages/editor/src/app/editMode/[...slug]/page.tsx b/packages/editor/src/app/editMode/[...slug]/page.tsx
index 03e3046..d43e4b1 100644
--- a/packages/editor/src/app/editMode/[...slug]/page.tsx
+++ b/packages/editor/src/app/editMode/[...slug]/page.tsx
@@ -173,6 +173,18 @@ export default function ArticleEditorContent({ params }: { params: Promise<{ slu
newBlock.content = JSON.stringify([{ text: '', checked: false }]);
}
+ if (newBlock.type === 'button') {
+ newBlock.content = 'Click me'
+ newBlock.metadata = {
+ buttonUrl: '#',
+ buttonStyle: {
+ variant: 'default',
+ size: 'default',
+ radius: 'md'
+ }
+ }
+ }
+
if (afterId === 'new') {
setBlocks([newBlock])
} else {
diff --git a/packages/editor/src/components/blocks/ButtonBlock.tsx b/packages/editor/src/components/blocks/ButtonBlock.tsx
new file mode 100644
index 0000000..a54e8b1
--- /dev/null
+++ b/packages/editor/src/components/blocks/ButtonBlock.tsx
@@ -0,0 +1,103 @@
+import { cn } from "@/lib/utils"
+import { Button, buttonVariants } from "@/components/ui/button"
+import { useState } from "react"
+
+interface ButtonBlockProps {
+ content: string
+ metadata?: {
+ buttonUrl?: string
+ buttonStyle?: {
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
+ size?: 'default' | 'sm' | 'lg'
+ radius?: 'none' | 'sm' | 'md' | 'lg' | 'full'
+ }
+ }
+ isEditing?: boolean
+ onUpdate?: (content: string) => void
+ align?: 'left' | 'center' | 'right'
+}
+
+export function ButtonBlock({
+ content,
+ metadata,
+ isEditing,
+ onUpdate,
+ align = 'left'
+}: ButtonBlockProps) {
+ const [isFocused, setIsFocused] = useState(false);
+ const buttonStyle = metadata?.buttonStyle || {}
+ const url = metadata?.buttonUrl || '#'
+
+ const buttonClasses = cn(
+ "relative",
+ {
+ 'rounded-none': buttonStyle.radius === 'none',
+ 'rounded-sm': buttonStyle.radius === 'sm',
+ 'rounded-md': buttonStyle.radius === 'md',
+ 'rounded-lg': buttonStyle.radius === 'lg',
+ 'rounded-full': buttonStyle.radius === 'full',
+ 'w-full': buttonStyle.size === 'lg',
+ 'w-24': buttonStyle.size === 'sm',
+ 'w-auto': buttonStyle.size === 'default'
+ }
+ )
+
+ const wrapperClasses = cn(
+ 'w-full',
+ {
+ 'text-left': align === 'left',
+ 'text-center': align === 'center',
+ 'text-right': align === 'right'
+ }
+ )
+
+ if (isEditing) {
+ return (
+
+
+
+ onUpdate?.(e.target.value)}
+ onFocus={() => setIsFocused(true)}
+ onBlur={() => setIsFocused(false)}
+ className={cn(
+ buttonVariants({
+ variant: buttonStyle.variant || 'default',
+ size: buttonStyle.size || 'default'
+ }),
+ buttonClasses,
+ "absolute inset-0 border-0 outline-none",
+ !isFocused && "opacity-0"
+ )}
+ placeholder="Button text..."
+ />
+
+
+ )
+ }
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/packages/editor/src/components/blocks/SortableBlock.tsx b/packages/editor/src/components/blocks/SortableBlock.tsx
index 9afa175..020e810 100644
--- a/packages/editor/src/components/blocks/SortableBlock.tsx
+++ b/packages/editor/src/components/blocks/SortableBlock.tsx
@@ -35,6 +35,12 @@ interface SortableBlockProps {
align?: 'left' | 'center' | 'right'
type?: 'info' | 'warning' | 'success' | 'error'
title?: string
+ buttonUrl?: string
+ buttonStyle?: {
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
+ size?: 'default' | 'sm' | 'lg'
+ radius?: 'none' | 'sm' | 'md' | 'lg' | 'full'
+ }
}
}
updateBlock: (id: string, content: string) => void
@@ -514,6 +520,21 @@ export function SortableBlock({
updateBlock(block.id, JSON.stringify(updatedContent));
}
}}
+ showButtonControls={block.type === 'button'}
+ buttonMetadata={{
+ url: block.metadata?.buttonUrl,
+ style: block.metadata?.buttonStyle
+ }}
+ onButtonMetadataChange={(metadata) => {
+ updateBlockMetadata(block.id, {
+ ...block.metadata,
+ buttonUrl: metadata.buttonUrl,
+ buttonStyle: {
+ ...block.metadata?.buttonStyle,
+ ...metadata.buttonStyle
+ }
+ })
+ }}
/>
)}
, label: 'File', description: 'Upload or link to a file.', group: 'Media' },
{ type: 'checkList', icon: , label: 'To-do list', description: 'Track tasks with a to-do list.', group: 'Basic' },
{ type: 'spacer', icon: , label: 'Spacing', description: 'Add vertical space between blocks.', group: 'Basic' },
+ { type: 'button', icon: , label: 'Button', description: 'Add a clickable button with link.', group: 'Basic' },
]
const filteredOptions = blockOptions.filter((option) =>
diff --git a/packages/editor/src/components/editor/BlockFormatToolbar.tsx b/packages/editor/src/components/editor/BlockFormatToolbar.tsx
index 0d16b41..e575d98 100644
--- a/packages/editor/src/components/editor/BlockFormatToolbar.tsx
+++ b/packages/editor/src/components/editor/BlockFormatToolbar.tsx
@@ -76,6 +76,23 @@ interface BlockFormatToolbarProps {
caption: string;
alignment: 'left' | 'center' | 'right';
}>) => void;
+ showButtonControls?: boolean;
+ buttonMetadata?: {
+ url?: string;
+ style?: {
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
+ size?: 'default' | 'sm' | 'lg';
+ radius?: 'none' | 'sm' | 'md' | 'lg' | 'full';
+ };
+ };
+ onButtonMetadataChange?: (metadata: Partial<{
+ buttonUrl: string;
+ buttonStyle: {
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
+ size?: 'default' | 'sm' | 'lg';
+ radius?: 'none' | 'sm' | 'md' | 'lg' | 'full';
+ };
+ }>) => void;
}
export function BlockFormatToolbar({
@@ -119,6 +136,9 @@ export function BlockFormatToolbar({
showAudioControls = false,
audioContent,
onAudioMetadataChange,
+ showButtonControls = false,
+ buttonMetadata,
+ onButtonMetadataChange,
}: BlockFormatToolbarProps) {
if (blockType === 'file' || blockType === 'spacer') {
return null;
@@ -133,7 +153,7 @@ export function BlockFormatToolbar({
"bg-popover border shadow-md rounded-md",
className
)}>
- {!showImageControls && !showCodeControls && !showVideoControls && !showAudioControls && blockType !== 'table' && (
+ {!showImageControls && !showCodeControls && !showVideoControls && !showAudioControls && !showButtonControls && blockType !== 'table' && (
<>
)}
+ {showButtonControls && (
+ <>
+
+
+ onButtonMetadataChange?.({ buttonUrl: e.target.value })}
+ placeholder="URL"
+ className="h-7 w-32 text-xs"
+ />
+
+
+
+
+
+
+ >
+ )}
+
{!showImageControls && !showVideoControls && !showAudioControls && (
<>
{!showCalloutControls && blockType !== 'table' && }
diff --git a/packages/editor/src/lib/renderers/BlockRenderer.tsx b/packages/editor/src/lib/renderers/BlockRenderer.tsx
index 6117c55..6247349 100644
--- a/packages/editor/src/lib/renderers/BlockRenderer.tsx
+++ b/packages/editor/src/lib/renderers/BlockRenderer.tsx
@@ -6,23 +6,15 @@ import { List } from "@/components/blocks/ListBlock"
import { Blockquote } from "@/components/blocks/BlockquoteBlock"
import { Divider } from "@/components/blocks/DividerBlock"
import { CodeBlock } from "@/components/blocks/CodeBlock"
-// import { Image } from '../blocks/Image'
-// import { Table } from '../blocks/Table'
-// import { ToggleList } from '../blocks/ToggleList'
import { CheckList } from "@/components/blocks/CheckListBlock"
-// import { Video } from '../blocks/Video'
-// import { Audio } from '../blocks/Audio'
-// import { File } from '../blocks/File'
-// import { Emoji } from '../blocks/Emoji'
import { Callout } from "@/components/blocks/CalloutBlock"
-import { cn } from '@/lib/utils'
-import { ErrorBoundary } from 'react-error-boundary'
import { ImageBlock } from "@/components/blocks/ImageBlock"
import { Table } from '@/components/blocks/TableBlock'
import { VideoBlock } from "@/components/blocks/VideoBlock"
import { AudioBlock } from "@/components/blocks/AudioBlock"
import { FileBlock } from "@/components/blocks/FileBlock"
import { SpacerBlock } from "@/components/blocks/SpacerBlock"
+import { ButtonBlock } from "@/components/blocks/ButtonBlock"
interface ImageBlockContent {
url: string;
@@ -239,6 +231,16 @@ export function BlockRenderer({ block, isEditing, onUpdate }: BlockRendererProps
onUpdate={(size) => onUpdate?.(block.id, size)}
/>
);
+ case 'button':
+ return (
+ onUpdate?.(block.id, content)}
+ align={block.metadata?.align}
+ />
+ );
default:
return null
}
diff --git a/packages/editor/src/types/Block.ts b/packages/editor/src/types/Block.ts
index ec9f5fc..044cc58 100644
--- a/packages/editor/src/types/Block.ts
+++ b/packages/editor/src/types/Block.ts
@@ -14,7 +14,8 @@ export type BlockType =
| 'file'
| 'callout'
| 'spacer'
- | 'apiReference';
+ | 'apiReference'
+ | 'button';
export interface Block {
id: string;
@@ -44,6 +45,12 @@ export interface Block {
align?: 'left' | 'center' | 'right'; // For alignment
type?: 'info' | 'warning' | 'success' | 'error'; // For callouts
title?: string; // For callouts
+ buttonUrl?: string;
+ buttonStyle?: {
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
+ size?: 'default' | 'sm' | 'lg';
+ radius?: 'none' | 'sm' | 'md' | 'lg' | 'full';
+ };
};
}