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';
+ };
};
}