@saena-io/create 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/index.js +9 -9
  2. package/package.json +1 -1
  3. package/template/base/package.json +44 -2
  4. package/template/base/scripts/ui-update.ts +83 -0
  5. package/template/base/src/components/ui/accordion.tsx +75 -0
  6. package/template/base/src/components/ui/alert-dialog.tsx +162 -0
  7. package/template/base/src/components/ui/alert.tsx +73 -0
  8. package/template/base/src/components/ui/app-sidebar.tsx +183 -0
  9. package/template/base/src/components/ui/aspect-ratio.tsx +22 -0
  10. package/template/base/src/components/ui/asset-input.tsx +211 -0
  11. package/template/base/src/components/ui/avatar.tsx +91 -0
  12. package/template/base/src/components/ui/badge.tsx +50 -0
  13. package/template/base/src/components/ui/breadcrumb.tsx +104 -0
  14. package/template/base/src/components/ui/button-group.tsx +78 -0
  15. package/template/base/src/components/ui/button.tsx +56 -0
  16. package/template/base/src/components/ui/calendar.tsx +205 -0
  17. package/template/base/src/components/ui/card.tsx +85 -0
  18. package/template/base/src/components/ui/carousel.tsx +232 -0
  19. package/template/base/src/components/ui/chart.tsx +337 -0
  20. package/template/base/src/components/ui/checkbox.tsx +29 -0
  21. package/template/base/src/components/ui/collapsible.tsx +15 -0
  22. package/template/base/src/components/ui/combobox.tsx +276 -0
  23. package/template/base/src/components/ui/command.tsx +190 -0
  24. package/template/base/src/components/ui/context-menu.tsx +243 -0
  25. package/template/base/src/components/ui/dialog.tsx +134 -0
  26. package/template/base/src/components/ui/direction.tsx +4 -0
  27. package/template/base/src/components/ui/drawer.tsx +120 -0
  28. package/template/base/src/components/ui/dropdown-menu.tsx +254 -0
  29. package/template/base/src/components/ui/empty.tsx +94 -0
  30. package/template/base/src/components/ui/field.tsx +222 -0
  31. package/template/base/src/components/ui/focal-point-picker.tsx +175 -0
  32. package/template/base/src/components/ui/hover-card.tsx +46 -0
  33. package/template/base/src/components/ui/input-group.tsx +149 -0
  34. package/template/base/src/components/ui/input-otp.tsx +85 -0
  35. package/template/base/src/components/ui/input.tsx +20 -0
  36. package/template/base/src/components/ui/item.tsx +188 -0
  37. package/template/base/src/components/ui/kbd.tsx +26 -0
  38. package/template/base/src/components/ui/label.tsx +20 -0
  39. package/template/base/src/components/ui/menubar.tsx +268 -0
  40. package/template/base/src/components/ui/native-select.tsx +58 -0
  41. package/template/base/src/components/ui/nav-main.tsx +70 -0
  42. package/template/base/src/components/ui/nav-projects.tsx +97 -0
  43. package/template/base/src/components/ui/nav-secondary.tsx +37 -0
  44. package/template/base/src/components/ui/nav-user.tsx +108 -0
  45. package/template/base/src/components/ui/navigation-menu.tsx +164 -0
  46. package/template/base/src/components/ui/pagination.tsx +123 -0
  47. package/template/base/src/components/ui/popover.tsx +80 -0
  48. package/template/base/src/components/ui/progress.tsx +66 -0
  49. package/template/base/src/components/ui/radio-group.tsx +36 -0
  50. package/template/base/src/components/ui/resizable.tsx +42 -0
  51. package/template/base/src/components/ui/rich-text/ai-chat-editor.tsx +20 -0
  52. package/template/base/src/components/ui/rich-text/ai-command.tsx +90 -0
  53. package/template/base/src/components/ui/rich-text/ai-copilot.tsx +67 -0
  54. package/template/base/src/components/ui/rich-text/ai-menu.tsx +456 -0
  55. package/template/base/src/components/ui/rich-text/ai-node.tsx +42 -0
  56. package/template/base/src/components/ui/rich-text/ai-toolbar-button.tsx +29 -0
  57. package/template/base/src/components/ui/rich-text/block-draggable.tsx +187 -0
  58. package/template/base/src/components/ui/rich-text/block-selection.tsx +17 -0
  59. package/template/base/src/components/ui/rich-text/code-block-node.tsx +204 -0
  60. package/template/base/src/components/ui/rich-text/codec.ts +63 -0
  61. package/template/base/src/components/ui/rich-text/extension.ts +53 -0
  62. package/template/base/src/components/ui/rich-text/ghost-text.tsx +23 -0
  63. package/template/base/src/components/ui/rich-text/import-export-toolbar.tsx +103 -0
  64. package/template/base/src/components/ui/rich-text/link.tsx +18 -0
  65. package/template/base/src/components/ui/rich-text/list-node.tsx +65 -0
  66. package/template/base/src/components/ui/rich-text/nodes.tsx +44 -0
  67. package/template/base/src/components/ui/rich-text/plugins.ts +233 -0
  68. package/template/base/src/components/ui/rich-text/rich-text-editor.tsx +82 -0
  69. package/template/base/src/components/ui/rich-text/static.tsx +117 -0
  70. package/template/base/src/components/ui/rich-text/table-node.tsx +934 -0
  71. package/template/base/src/components/ui/rich-text/table-toolbar.tsx +232 -0
  72. package/template/base/src/components/ui/rich-text/toggle-node.tsx +36 -0
  73. package/template/base/src/components/ui/rich-text/toolbar-slots.ts +41 -0
  74. package/template/base/src/components/ui/rich-text/toolbar.tsx +668 -0
  75. package/template/base/src/components/ui/rich-text/use-ai-chat.ts +35 -0
  76. package/template/base/src/components/ui/rich-text/variable-type.ts +4 -0
  77. package/template/base/src/components/ui/rich-text/variable.tsx +97 -0
  78. package/template/base/src/components/ui/scroll-area.tsx +49 -0
  79. package/template/base/src/components/ui/select.tsx +202 -0
  80. package/template/base/src/components/ui/separator.tsx +19 -0
  81. package/template/base/src/components/ui/sheet.tsx +126 -0
  82. package/template/base/src/components/ui/sidebar.tsx +695 -0
  83. package/template/base/src/components/ui/skeleton.tsx +13 -0
  84. package/template/base/src/components/ui/slider.tsx +52 -0
  85. package/template/base/src/components/ui/sonner.tsx +50 -0
  86. package/template/base/src/components/ui/spinner.tsx +18 -0
  87. package/template/base/src/components/ui/switch.tsx +30 -0
  88. package/template/base/src/components/ui/table.tsx +89 -0
  89. package/template/base/src/components/ui/tabs.tsx +73 -0
  90. package/template/base/src/components/ui/textarea.tsx +18 -0
  91. package/template/base/src/components/ui/toggle-group.tsx +85 -0
  92. package/template/base/src/components/ui/toggle.tsx +45 -0
  93. package/template/base/src/components/ui/toolbar.tsx +451 -0
  94. package/template/base/src/components/ui/tooltip.tsx +52 -0
  95. package/template/base/src/hooks/use-mobile.ts +19 -0
  96. package/template/base/src/lib/utils.ts +6 -0
  97. package/template/base/src/routes/__root.tsx +1 -1
  98. package/template/base/src/server/auth.ts +2 -2
  99. package/template/base/src/styles/globals.css +230 -0
  100. package/template/base/vite.config.ts +15 -1
@@ -0,0 +1,65 @@
1
+ import { isOrderedList } from '@platejs/list';
2
+ import { useTodoListElement, useTodoListElementState } from '@platejs/list/react';
3
+ import { Checkbox } from '@saena-io/ui/components/checkbox';
4
+ import { cn } from '@saena-io/ui/lib/utils';
5
+ import { type PlateElementProps, type RenderNodeWrapper, useReadOnly } from 'platejs/react';
6
+ import type { FC } from 'react';
7
+
8
+ // Custom list rendering that adds a TODO variant. The base list plugin draws bullets/numbers via CSS
9
+ // `list-style-type` on a <ul>/<ol><li>; CSS can't draw a checkbox, so todo items (listStyleType 'todo')
10
+ // render a Checkbox marker + a strikethrough-when-checked <li> instead. Registered as ListPlugin's
11
+ // render.belowNodes (see rich-text-editor.tsx) — for non-todo lists it falls back to the same ul/ol markup.
12
+
13
+ type ListElement = PlateElementProps['element'] & {
14
+ listStart?: number;
15
+ listStyleType?: string;
16
+ checked?: boolean;
17
+ };
18
+
19
+ const TODO_CONFIG: Record<string, { Li: FC<PlateElementProps>; Marker: FC<PlateElementProps> }> = {
20
+ todo: { Li: TodoLi, Marker: TodoMarker },
21
+ };
22
+
23
+ export const BlockList: RenderNodeWrapper = (props) => {
24
+ if (!(props.element as ListElement).listStyleType) return;
25
+ return (childProps) => <List {...childProps} />;
26
+ };
27
+
28
+ function List(props: PlateElementProps) {
29
+ const element = props.element as ListElement;
30
+ const { Li, Marker } = TODO_CONFIG[element.listStyleType ?? ''] ?? {};
31
+ const ListTag = isOrderedList(props.element) ? 'ol' : 'ul';
32
+ return (
33
+ <ListTag
34
+ className="relative m-0 p-0"
35
+ style={{ listStyleType: element.listStyleType }}
36
+ start={element.listStart}
37
+ >
38
+ {Marker ? <Marker {...props} /> : null}
39
+ {Li ? <Li {...props} /> : <li>{props.children}</li>}
40
+ </ListTag>
41
+ );
42
+ }
43
+
44
+ function TodoMarker(props: PlateElementProps) {
45
+ const state = useTodoListElementState({ element: props.element });
46
+ const { checkboxProps } = useTodoListElement(state);
47
+ const readOnly = useReadOnly();
48
+ return (
49
+ <div contentEditable={false}>
50
+ <Checkbox
51
+ className={cn('-left-6 absolute top-0.5', readOnly && 'pointer-events-none')}
52
+ {...checkboxProps}
53
+ />
54
+ </div>
55
+ );
56
+ }
57
+
58
+ function TodoLi(props: PlateElementProps) {
59
+ const checked = (props.element as ListElement).checked;
60
+ return (
61
+ <li className={cn('list-none', checked && 'text-muted-foreground line-through')}>
62
+ {props.children}
63
+ </li>
64
+ );
65
+ }
@@ -0,0 +1,44 @@
1
+ import { PlateElement, type PlateElementProps } from 'platejs/react';
2
+
3
+ // Node renderers for the rich-text editor, written on plain elements (no Radix) so they fit our
4
+ // Base-UI design system. Styling is Tailwind; PlateElement applies the editor attributes + children.
5
+
6
+ export function H1Element(props: PlateElementProps) {
7
+ return (
8
+ <PlateElement
9
+ as="h1"
10
+ className="mt-[1.6em] mb-1 pb-1 font-bold font-heading text-4xl"
11
+ {...props}
12
+ />
13
+ );
14
+ }
15
+
16
+ export function H2Element(props: PlateElementProps) {
17
+ return (
18
+ <PlateElement
19
+ as="h2"
20
+ className="mt-[1.4em] mb-1 pb-px font-heading font-semibold text-2xl tracking-tight"
21
+ {...props}
22
+ />
23
+ );
24
+ }
25
+
26
+ export function H3Element(props: PlateElementProps) {
27
+ return (
28
+ <PlateElement
29
+ as="h3"
30
+ className="mt-[1em] mb-1 pb-px font-heading font-semibold text-xl tracking-tight"
31
+ {...props}
32
+ />
33
+ );
34
+ }
35
+
36
+ export function BlockquoteElement(props: PlateElementProps) {
37
+ return (
38
+ <PlateElement
39
+ as="blockquote"
40
+ className="my-2 border-muted-foreground/30 border-l-2 pl-3 text-muted-foreground italic"
41
+ {...props}
42
+ />
43
+ );
44
+ }
@@ -0,0 +1,233 @@
1
+ import {
2
+ BlockquoteRules,
3
+ BoldRules,
4
+ CodeRules,
5
+ HeadingRules,
6
+ ItalicRules,
7
+ MarkComboRules,
8
+ StrikethroughRules,
9
+ UnderlineRules,
10
+ } from '@platejs/basic-nodes';
11
+ import {
12
+ BlockquotePlugin,
13
+ BoldPlugin,
14
+ CodePlugin,
15
+ H1Plugin,
16
+ H2Plugin,
17
+ H3Plugin,
18
+ ItalicPlugin,
19
+ StrikethroughPlugin,
20
+ UnderlinePlugin,
21
+ } from '@platejs/basic-nodes/react';
22
+ import { TextAlignPlugin } from '@platejs/basic-styles/react';
23
+ import { CodeBlockRules } from '@platejs/code-block';
24
+ import { CodeBlockPlugin, CodeLinePlugin, CodeSyntaxPlugin } from '@platejs/code-block/react';
25
+ import { DndPlugin } from '@platejs/dnd';
26
+ import { DocxPlugin } from '@platejs/docx';
27
+ import { IndentPlugin } from '@platejs/indent/react';
28
+ import { JuicePlugin } from '@platejs/juice';
29
+ import { LinkRules } from '@platejs/link';
30
+ import { LinkPlugin } from '@platejs/link/react';
31
+ import { BulletedListRules, OrderedListRules, TaskListRules } from '@platejs/list';
32
+ import { ListPlugin } from '@platejs/list/react';
33
+ import { MarkdownPlugin } from '@platejs/markdown';
34
+ import { BlockSelectionPlugin } from '@platejs/selection/react';
35
+ import {
36
+ TableCellHeaderPlugin,
37
+ TableCellPlugin,
38
+ TablePlugin,
39
+ TableRowPlugin,
40
+ } from '@platejs/table/react';
41
+ import { TogglePlugin } from '@platejs/toggle/react';
42
+ import { common, createLowlight } from 'lowlight';
43
+ import { ExitBreakPlugin, KEYS, TrailingBlockPlugin, type Value } from 'platejs';
44
+ import { usePlateEditor } from 'platejs/react';
45
+ import remarkGfm from 'remark-gfm';
46
+ import { BlockDraggable } from './block-draggable';
47
+ import { CodeBlockElement, CodeSyntaxLeaf } from './code-block-node';
48
+ import { emptyRichValue } from './codec';
49
+ import type { RichTextExtension } from './extension';
50
+ import { LinkElement } from './link';
51
+ import { BlockList } from './list-node';
52
+ import { BlockquoteElement, H1Element, H2Element, H3Element } from './nodes';
53
+ import {
54
+ TableCellElement,
55
+ TableCellHeaderElement,
56
+ TableElement,
57
+ TableRowElement,
58
+ } from './table-node';
59
+ import { ToggleElement } from './toggle-node';
60
+ import { VariablePlugin } from './variable';
61
+
62
+ // The editor's Plate plugin set + the editor factory. This module is the toolbar-free LEAF of the rich-text
63
+ // dependency graph: it imports only node renderers and platejs, never the toolbar. That's what lets the
64
+ // toolbar (and table-toolbar / import-export-toolbar) import the `RichTextEditor` type from here without a
65
+ // cycle — the toolbar is mounted by RichTextContent (see rich-text-editor.tsx), not registered as a plugin.
66
+
67
+ // Shared lowlight instance (highlight.js `common` language set) that powers code-block syntax highlighting.
68
+ // Created once at module scope and handed to CodeBlockPlugin.options.lowlight; the plugin's decorate emits
69
+ // `code_syntax` leaves carrying highlight.js class names that CodeSyntaxLeaf + globals.css (.saena-hljs) style.
70
+ const lowlight = createLowlight(common);
71
+
72
+ // The core rich-text capability set (§9, ADR-0005). This array IS the extension seam: future tool
73
+ // plugins (media, AI/copilot, tables, …) append their Plate plugins here, and matching toolbar groups
74
+ // to the toolbar. Built on our Base-UI primitives — not the Radix shadcn block.
75
+ export const coreRichTextPlugins = [
76
+ // Inline marks. Each carries markdown autoformat input rules (**bold**, _italic_, `code`, ~~strike~~)
77
+ // plus the keyboard shortcuts the bare plugins miss — strikethrough mod+shift+x and code mod+e (bold /
78
+ // italic / underline already ship their own mod+b/i/u).
79
+ BoldPlugin.configure({
80
+ inputRules: [
81
+ BoldRules.markdown({ variant: '*' }),
82
+ BoldRules.markdown({ variant: '_' }),
83
+ MarkComboRules.markdown({ variant: 'boldItalic' }),
84
+ MarkComboRules.markdown({ variant: 'boldUnderline' }),
85
+ MarkComboRules.markdown({ variant: 'boldItalicUnderline' }),
86
+ MarkComboRules.markdown({ variant: 'italicUnderline' }),
87
+ ],
88
+ }),
89
+ ItalicPlugin.configure({
90
+ inputRules: [ItalicRules.markdown({ variant: '*' }), ItalicRules.markdown({ variant: '_' })],
91
+ }),
92
+ UnderlinePlugin.configure({ inputRules: [UnderlineRules.markdown()] }),
93
+ StrikethroughPlugin.configure({
94
+ inputRules: [StrikethroughRules.markdown()],
95
+ shortcuts: { toggle: { keys: 'mod+shift+x' } },
96
+ }),
97
+ CodePlugin.configure({
98
+ inputRules: [CodeRules.markdown()],
99
+ shortcuts: { toggle: { keys: 'mod+e' } },
100
+ }),
101
+ // Headings + quote. Markdown autoformat (`# `, `## `, `### `, `> `) and a reset-on-Enter rule so Enter
102
+ // in an empty heading drops back to a paragraph. .withComponent is folded into node.component because a
103
+ // plugin can't both .configure and .withComponent.
104
+ H1Plugin.configure({
105
+ inputRules: [HeadingRules.markdown()],
106
+ node: { component: H1Element },
107
+ rules: { break: { empty: 'reset' } },
108
+ }),
109
+ H2Plugin.configure({
110
+ inputRules: [HeadingRules.markdown()],
111
+ node: { component: H2Element },
112
+ rules: { break: { empty: 'reset' } },
113
+ }),
114
+ H3Plugin.configure({
115
+ inputRules: [HeadingRules.markdown()],
116
+ node: { component: H3Element },
117
+ rules: { break: { empty: 'reset' } },
118
+ }),
119
+ BlockquotePlugin.configure({
120
+ inputRules: [BlockquoteRules.markdown()],
121
+ node: { component: BlockquoteElement },
122
+ }),
123
+ // Multi-line code block (for larger snippets) — distinct from the inline `code` mark. Typing ``` or
124
+ // pasting a fenced block creates one. `lowlight` enables highlight.js syntax highlighting; the in-block
125
+ // language picker (CodeBlockElement) sets `element.lang`, which drives the decorate-based coloring.
126
+ CodeBlockPlugin.configure({
127
+ inputRules: [CodeBlockRules.markdown({ on: 'match' })],
128
+ node: { component: CodeBlockElement },
129
+ options: { lowlight, defaultLanguage: null },
130
+ shortcuts: { toggle: { keys: 'mod+alt+8' } },
131
+ }),
132
+ CodeLinePlugin,
133
+ CodeSyntaxPlugin.withComponent(CodeSyntaxLeaf),
134
+ // Block alignment — stores `align` on the block and injects it as the `textAlign` CSS style. Targets
135
+ // the block types we render (headings + paragraphs); driven by the toolbar's align dropdown.
136
+ TextAlignPlugin.configure({
137
+ inject: {
138
+ nodeProps: {
139
+ defaultNodeValue: 'start',
140
+ nodeKey: 'align',
141
+ styleKey: 'textAlign',
142
+ validNodeValues: ['start', 'left', 'center', 'right', 'end', 'justify'],
143
+ },
144
+ targetPlugins: [...KEYS.heading, KEYS.p],
145
+ },
146
+ }),
147
+ // Inline links — add/edit/remove via the toolbar's Link popover, plus autolink on paste/space/break and
148
+ // `[text](url)` markdown. LinkElement spreads getLinkAttributes for a real href.
149
+ LinkPlugin.configure({
150
+ inputRules: [
151
+ LinkRules.markdown(),
152
+ LinkRules.autolink({ variant: 'paste' }),
153
+ LinkRules.autolink({ variant: 'space' }),
154
+ LinkRules.autolink({ variant: 'break' }),
155
+ ],
156
+ render: { node: LinkElement },
157
+ }),
158
+ // Indented lists (bulleted / numbered) — ListPlugin builds on IndentPlugin's block indentation. Both
159
+ // target headings + quote + paragraph so those blocks can be indented / turned into list items, and the
160
+ // list gains markdown autoformat (`- `, `* `, `1. `, `1) `).
161
+ IndentPlugin.configure({
162
+ inject: { targetPlugins: [...KEYS.heading, KEYS.p, KEYS.blockquote] },
163
+ options: { offset: 24 },
164
+ }),
165
+ ListPlugin.configure({
166
+ inputRules: [
167
+ BulletedListRules.markdown({ variant: '-' }),
168
+ BulletedListRules.markdown({ variant: '*' }),
169
+ OrderedListRules.markdown({ variant: '.' }),
170
+ OrderedListRules.markdown({ variant: ')' }),
171
+ // `- [ ] ` / `- [x] ` autoformat into a to-do list item.
172
+ TaskListRules.markdown({ checked: false }),
173
+ TaskListRules.markdown({ checked: true }),
174
+ ],
175
+ inject: { targetPlugins: [...KEYS.heading, KEYS.p, KEYS.blockquote] },
176
+ // Override the default ul/ol marker render so to-do list items can draw a checkbox (CSS markers can't).
177
+ render: { belowNodes: BlockList },
178
+ }),
179
+ // Collapsible toggle blocks — a disclosure chevron (ToggleElement) that hides the blocks indented beneath
180
+ // it. Builds on IndentPlugin (registered above); the plugin tracks open/closed state per block id.
181
+ TogglePlugin.withComponent(ToggleElement),
182
+ // Block selection + drag, required by the table renderers: cell-selection overlays read from
183
+ // BlockSelectionPlugin and the row drag handle uses DndPlugin (needs a single react-dnd backend at the
184
+ // editor root — see RichTextDndProvider). Registered before the table so its transforms are available.
185
+ BlockSelectionPlugin,
186
+ // Per-block drag handle (grip on hover) — render.aboveNodes wraps every block with BlockDraggable.
187
+ DndPlugin.configure({ render: { aboveNodes: BlockDraggable } }),
188
+ // Tables — insert + edit a bordered grid with column/row resize, row drag, and cell selection.
189
+ TablePlugin.withComponent(TableElement),
190
+ TableRowPlugin.withComponent(TableRowElement),
191
+ TableCellPlugin.withComponent(TableCellElement),
192
+ TableCellHeaderPlugin.withComponent(TableCellHeaderElement),
193
+ // Editing-feel behavior: Cmd+Enter escapes a nested block (table cell / quote) to a sibling paragraph,
194
+ // and a trailing empty paragraph is always kept after a terminal block (table / quote) so the caret can
195
+ // exit to normal text at the end of the document.
196
+ ExitBreakPlugin.configure({
197
+ shortcuts: { insert: { keys: 'mod+enter' }, insertBefore: { keys: 'mod+shift+enter' } },
198
+ }),
199
+ TrailingBlockPlugin,
200
+ // Document interop: Markdown serialize/deserialize (GFM tables, strikethrough, autolinks) powers
201
+ // import/export and Markdown paste; DocxPlugin + JuicePlugin let content paste in cleanly from Word.
202
+ MarkdownPlugin.configure({ options: { remarkPlugins: [remarkGfm] } }),
203
+ DocxPlugin,
204
+ JuicePlugin,
205
+ // i18n variables render as inline badges and round-trip through the stored Slate JSON.
206
+ VariablePlugin,
207
+ ];
208
+
209
+ /** Options for a rich-text editor instance. */
210
+ export interface RichTextOptions {
211
+ /** Required when several editors share a PlateController (e.g. the source + target panes). */
212
+ id?: string;
213
+ value?: Value;
214
+ readOnly?: boolean;
215
+ /** Addons contributing Plate plugins + toolbar controls (media, text styles, AI, …). Read once when the
216
+ * editor is constructed (like usePlateEditor's plugins) — to change the addon set, remount the editor via
217
+ * a React key, not by mutating this array on a live instance. */
218
+ extensions?: RichTextExtension[];
219
+ }
220
+
221
+ /** Create a rich-text editor instance — the core capability set plus any extension plugins. */
222
+ export function useRichTextEditor(opts: RichTextOptions) {
223
+ const extensionPlugins = (opts.extensions ?? []).flatMap((e) => e.platePlugins ?? []);
224
+ return usePlateEditor({
225
+ id: opts.id,
226
+ plugins: [...coreRichTextPlugins, ...extensionPlugins],
227
+ value: opts.value ?? emptyRichValue,
228
+ readOnly: opts.readOnly,
229
+ });
230
+ }
231
+
232
+ /** The concrete editor type (with the core plugins' transforms) — used to type toolbar handlers. */
233
+ export type RichTextEditor = ReturnType<typeof useRichTextEditor>;
@@ -0,0 +1,82 @@
1
+ import { cn } from '@saena-io/ui/lib/utils';
2
+ import { PlateContent } from 'platejs/react';
3
+ import type { ReactNode } from 'react';
4
+ import { DndProvider } from 'react-dnd';
5
+ import { HTML5Backend } from 'react-dnd-html5-backend';
6
+ import { coreRichTextPlugins, useRichTextEditor } from './plugins';
7
+ import { FloatingRichTextToolbar } from './toolbar';
8
+
9
+ // The rich-text editor's public surface. The Plate plugin set + editor factory live in the toolbar-free
10
+ // leaf ./plugins (so the toolbar can import the RichTextEditor type without a dependency cycle); this module
11
+ // composes them with the toolbar (FloatingRichTextToolbar, RichTextToolbar), the stored-value codec, and the
12
+ // content/DnD surfaces, and re-exports everything a consumer or addon author needs from @saena-io/ui.
13
+
14
+ export { variableNode, flattenRichText, extractPlaceholders } from './variable';
15
+ export { RichTextToolbar } from './toolbar';
16
+ export { coreRichTextPlugins, useRichTextEditor };
17
+ export { createAiCopilotExtension } from './ai-copilot';
18
+ export type { AiCopilotOptions } from './ai-copilot';
19
+ export { createAiCommandExtension } from './ai-command';
20
+ export type { AiCommandOptions } from './ai-command';
21
+ export { GhostText } from './ghost-text';
22
+ export { emptyRichValue, parseRichText, serializeRichText, richValueIsDirty } from './codec';
23
+ export type { RichTextEditor, RichTextOptions } from './plugins';
24
+ // Re-export the Plate surface consumers (and addon authors) need, so they go through @saena-io/ui (platejs
25
+ // stays an implementation detail of the design system) rather than depending on platejs directly — the
26
+ // dependency boundary that lets a feature package extend the editor without importing platejs (§13).
27
+ export { Plate, PlateController } from 'platejs/react';
28
+ export type { Value } from 'platejs';
29
+ export type { AnyPlatePlugin } from 'platejs/react';
30
+ export type { RichTextExtension, ToolbarItem, ToolbarSlot } from './extension';
31
+
32
+ /**
33
+ * The editable content surface. Place inside a <Plate editor={…}>. Renders the floating selection toolbar
34
+ * alongside the editable — it mounts here (rather than as a core plugin) so the plugin set in ./plugins
35
+ * stays toolbar-free; being inside <Plate>, its hooks still bind to this concrete editor (safe under the
36
+ * dual-pane PlateController).
37
+ */
38
+ export function RichTextContent({
39
+ placeholder,
40
+ className,
41
+ lang,
42
+ }: {
43
+ placeholder?: string;
44
+ className?: string;
45
+ /** BCP-47 language tag set on the editable so the browser spellchecks against the right dictionary
46
+ * (e.g. the translation pane gets the target locale, not the UI language). */
47
+ lang?: string;
48
+ }) {
49
+ return (
50
+ <>
51
+ <PlateContent
52
+ placeholder={placeholder}
53
+ lang={lang}
54
+ className={cn(
55
+ // Borderless, document-style surface — the host centers it in a max-width column. Grows with
56
+ // content (no inner scroll) so the surrounding area provides a single scroll.
57
+ 'min-h-32 w-full px-1 py-2 text-sm leading-relaxed outline-none',
58
+ '[&_p]:my-1.5',
59
+ // The first block keeps its element margin (headings carry a large top margin for section
60
+ // separation) — zero it so content doesn't start far down. Blocks are wrapped by the drag handle
61
+ // (.flow-root holds the real element), so reach through it.
62
+ '[&>:first-child_.flow-root>:first-child]:mt-0 [&>:first-child:not(:has(.flow-root))]:mt-0',
63
+ // Indent-list markers render `outside` by default; render them `inside` so bullets/numbers stay
64
+ // within the text column.
65
+ '[&_ul]:[list-style-position:inside] [&_ol]:[list-style-position:inside] [&_li]:my-0.5',
66
+ '[&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-xs',
67
+ className,
68
+ )}
69
+ />
70
+ <FloatingRichTextToolbar />
71
+ </>
72
+ );
73
+ }
74
+
75
+ /**
76
+ * A single react-dnd backend for the table row-drag handles. Wrap it ONCE around all Plate editors that
77
+ * share a PlateController (the source + target panes) — two HTML5Backends mounted on the same page throw,
78
+ * so we can't register the provider per-editor via DndPlugin's render.
79
+ */
80
+ export function RichTextDndProvider({ children }: { children: ReactNode }) {
81
+ return <DndProvider backend={HTML5Backend}>{children}</DndProvider>;
82
+ }
@@ -0,0 +1,117 @@
1
+ import {
2
+ BaseBlockquotePlugin,
3
+ BaseBoldPlugin,
4
+ BaseCodePlugin,
5
+ BaseHeadingPlugin,
6
+ BaseItalicPlugin,
7
+ BaseStrikethroughPlugin,
8
+ BaseUnderlinePlugin,
9
+ } from '@platejs/basic-nodes';
10
+ import { BaseCodeBlockPlugin, BaseCodeLinePlugin } from '@platejs/code-block';
11
+ import { BaseLinkPlugin } from '@platejs/link';
12
+ import { cn } from '@saena-io/ui/lib/utils';
13
+ import { KEYS, type Value, createSlatePlugin } from 'platejs';
14
+ import {
15
+ PlateStatic,
16
+ SlateElement,
17
+ type SlateElementProps,
18
+ SlateLeaf,
19
+ type SlateLeafProps,
20
+ createStaticEditor,
21
+ } from 'platejs/static';
22
+ import { parseRichText } from './codec';
23
+ import { VARIABLE_TYPE } from './variable-type';
24
+
25
+ // The deferred ADR-0005 public-site renderer: render a stored rich `Value` to HTML with NO editor in the
26
+ // bundle. It uses Plate's BASE (React-free) plugins for the node schema + static SlateElement/SlateLeaf
27
+ // components — never the editor's `/react` plugins (those call Plate hooks and need a live <Plate>, which
28
+ // is exactly what the public site must avoid). CMS public pages are the first consumer (Prose renders here).
29
+ //
30
+ // v1 covers the Prose node set (paragraphs [a built-in node], headings, quote, marks, link, code, the i18n
31
+ // variable badge). Indent-based lists and tables get PlateStatic's defaults for now; their bespoke static
32
+ // components are an additive follow-up — the base-kit architecture means adding them never forces a rewrite.
33
+
34
+ // The i18n variable node, defined React-free so the static editor knows it's an inline void without pulling
35
+ // the editor's VariablePlugin (which imports platejs/react).
36
+ const baseVariablePlugin = createSlatePlugin({
37
+ key: VARIABLE_TYPE,
38
+ node: { type: VARIABLE_TYPE, isElement: true, isInline: true, isVoid: true },
39
+ });
40
+
41
+ const staticPlugins = [
42
+ BaseBoldPlugin,
43
+ BaseItalicPlugin,
44
+ BaseUnderlinePlugin,
45
+ BaseStrikethroughPlugin,
46
+ BaseCodePlugin,
47
+ BaseHeadingPlugin,
48
+ BaseBlockquotePlugin,
49
+ BaseLinkPlugin,
50
+ BaseCodeBlockPlugin,
51
+ BaseCodeLinePlugin,
52
+ baseVariablePlugin,
53
+ ];
54
+
55
+ // `as` is the HTML tag the node renders to (SlateElement/SlateLeaf accept an intrinsic tag name); all our
56
+ // node mappings below pass a plain tag string, so type it as such rather than the broad ElementType.
57
+ type Tag = keyof HTMLElementTagNameMap;
58
+ function El(as: Tag, className?: string) {
59
+ return (props: SlateElementProps) => <SlateElement as={as} className={className} {...props} />;
60
+ }
61
+ function Leaf(as: Tag, className?: string) {
62
+ return (props: SlateLeafProps) => <SlateLeaf as={as} className={className} {...props} />;
63
+ }
64
+
65
+ /** The i18n variable as a static inline badge (mirrors the editor's VariableElement, no selection state). */
66
+ function VariableStatic(props: SlateElementProps) {
67
+ const name = (props.element as { name?: string }).name ?? '';
68
+ return (
69
+ <SlateElement as="span" {...props}>
70
+ <span className="mx-0.5 inline-flex select-none items-center rounded bg-primary px-1.5 py-px font-medium font-mono text-[0.8em] text-primary-foreground">
71
+ {name}
72
+ </span>
73
+ {props.children}
74
+ </SlateElement>
75
+ );
76
+ }
77
+
78
+ const staticComponents = {
79
+ [KEYS.p]: El('p', 'my-1.5 leading-relaxed'),
80
+ [KEYS.h1]: El('h1', 'mt-8 mb-3 font-bold text-3xl'),
81
+ [KEYS.h2]: El('h2', 'mt-6 mb-2 font-semibold text-2xl'),
82
+ [KEYS.h3]: El('h3', 'mt-4 mb-2 font-semibold text-xl'),
83
+ [KEYS.blockquote]: El('blockquote', 'my-3 border-muted-foreground/30 border-l-2 pl-4 italic'),
84
+ [KEYS.codeBlock]: El('pre', 'my-3 overflow-auto rounded bg-muted p-3 font-mono text-sm'),
85
+ [KEYS.codeLine]: El('div'),
86
+ [KEYS.a]: El('a', 'font-medium text-primary underline underline-offset-2'),
87
+ [VARIABLE_TYPE]: VariableStatic,
88
+ [KEYS.bold]: Leaf('strong'),
89
+ [KEYS.italic]: Leaf('em'),
90
+ [KEYS.underline]: Leaf('u'),
91
+ [KEYS.strikethrough]: Leaf('s'),
92
+ [KEYS.code]: Leaf('code', 'rounded bg-muted px-1 py-0.5 font-mono text-xs'),
93
+ };
94
+
95
+ /**
96
+ * Render a stored rich value for the public site — editor-free (ADR-0005). Safe in a Server Component.
97
+ *
98
+ * Accepts either an already-parsed `Value` or the raw stored string (serialized Slate JSON, the shape the
99
+ * codec persists); a string is parsed here with the plain-text fallback so a legacy plain row still renders.
100
+ */
101
+ export function RichTextStatic({
102
+ value,
103
+ className,
104
+ }: { value: Value | string; className?: string }) {
105
+ const doc = typeof value === 'string' ? parseRichText(value, { plainTextFallback: true }) : value;
106
+ const editor = createStaticEditor({
107
+ plugins: staticPlugins,
108
+ value: doc,
109
+ components: staticComponents,
110
+ // Disable Plate's NodeId plugin for the static render. By default it assigns each block a fresh
111
+ // `nanoid(10)` id during editor init (→ `data-block-id` / `data-slate-id`), which runs independently on
112
+ // the server and on the client, producing different ids and an SSR hydration mismatch. A read-only public
113
+ // render needs no node ids (no selection / DnD), so turning it off makes the markup deterministic.
114
+ nodeId: false,
115
+ });
116
+ return <PlateStatic editor={editor} className={cn('text-sm', className)} />;
117
+ }