diff --git a/app/globals.css b/app/globals.css index fb9620a9..1b873cc6 100644 --- a/app/globals.css +++ b/app/globals.css @@ -250,6 +250,10 @@ @apply bg-input px-1.5 py-0.5 rounded-md font-mono; } +.rich-text-editor-full.ProseMirror hr { + @apply border-t border-border my-4; +} + /* Custom dropdown chevron for styled select elements on published pages */ #ybody select.appearance-none { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23737373' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); diff --git a/app/ycode/components/RichTextEditor.tsx b/app/ycode/components/RichTextEditor.tsx index 78f2f006..e4f8aa11 100644 --- a/app/ycode/components/RichTextEditor.tsx +++ b/app/ycode/components/RichTextEditor.tsx @@ -30,6 +30,7 @@ import ListItem from '@tiptap/extension-list-item'; import Heading from '@tiptap/extension-heading'; import Blockquote from '@tiptap/extension-blockquote'; import Code from '@tiptap/extension-code'; +import HorizontalRule from '@tiptap/extension-horizontal-rule'; import { cn } from '@/lib/utils'; import type { CollectionField, Collection } from '@/types'; import { @@ -374,6 +375,7 @@ const RichTextEditor = forwardRef(({ ListItem, Blockquote, Code, + HorizontalRule, RichTextImage, ]; diff --git a/lib/layer-utils.ts b/lib/layer-utils.ts index 2bb63945..7ce687aa 100644 --- a/lib/layer-utils.ts +++ b/lib/layer-utils.ts @@ -565,6 +565,7 @@ const SUBLAYER_ICON_MAP: Record = { blockquote: 'quote', richTextComponent: 'component', richTextImage: 'image', + horizontalRule: 'separator', }; /** @@ -579,6 +580,7 @@ export function contentBlockToStyleKey(block: { type: string; attrs?: Record = { listItem: 'text', blockquote: 'quote', richTextImage: 'image', + horizontalRule: 'separator', }; /** Inline mark style keys shown for all text layers */ diff --git a/lib/templates/structure.ts b/lib/templates/structure.ts index 9ada0c58..fdad1087 100644 --- a/lib/templates/structure.ts +++ b/lib/templates/structure.ts @@ -53,9 +53,9 @@ export const structureTemplates: Record = { name: 'Separator', template: { name: 'hr', - classes: ['border-t', 'border-[#d1d5db]'], + classes: ['border-t-[1px]', 'border-[#aeaeae]'], design: { - borders: { isActive: true, borderWidth: '1px 0 0 0', borderColor: '#d1d5db' }, + borders: { isActive: true, borderTopWidth: '1px', borderColor: '#aeaeae' }, } } }, diff --git a/lib/text-format-utils.ts b/lib/text-format-utils.ts index ddca2fc0..543e4d44 100644 --- a/lib/text-format-utils.ts +++ b/lib/text-format-utils.ts @@ -187,6 +187,13 @@ export const DEFAULT_TEXT_STYLES: Record = { borders: { borderRadius: '4px' }, }, }, + horizontalRule: { + label: 'Separator', + classes: 'border-t-[1px] border-[#aeaeae]', + design: { + borders: { borderTopWidth: '1px', borderColor: '#aeaeae' }, + }, + }, }; /** @@ -781,6 +788,17 @@ function renderBlock( return React.createElement('img', imgProps); } + if (block.type === 'horizontalRule') { + const hrProps: Record = { + key, + className: getTextStyleClasses(textStyles, 'horizontalRule'), + }; + if (isEditMode) { + hrProps['data-style'] = 'horizontalRule'; + } + return React.createElement('hr', hrProps); + } + // Handle embedded component blocks if (block.type === 'richTextComponent' && block.attrs?.componentId) { return renderRichTextComponentBlock(block, key, components, renderComponentBlock, ancestorComponentIds); diff --git a/lib/tiptap-utils.ts b/lib/tiptap-utils.ts index b7ad1f66..3d4d8224 100644 --- a/lib/tiptap-utils.ts +++ b/lib/tiptap-utils.ts @@ -75,6 +75,8 @@ export function extractInlineNodesFromRichText( } else if (node.type === 'richTextComponent') { // Preserve embedded component nodes as-is for block rendering result.push(node); + } else if (node.type === 'horizontalRule') { + result.push(node); } else if (node.type === 'listItem') { // List items should be handled by their parent list // But if we encounter one directly, extract its content @@ -114,7 +116,7 @@ export function contentHasBlockElements(content: any): boolean { // Handle Tiptap doc structure if (content.type === 'doc' && Array.isArray(content.content)) { return content.content.some((block: any) => - block.type === 'bulletList' || block.type === 'orderedList' || block.type === 'richTextComponent' + block.type === 'bulletList' || block.type === 'orderedList' || block.type === 'richTextComponent' || block.type === 'horizontalRule' ); }