@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,232 @@
1
+ import {
2
+ ArrowDown01Icon,
3
+ ArrowLeft01Icon,
4
+ ArrowRight01Icon,
5
+ ArrowUp01Icon,
6
+ CombineIcon,
7
+ Delete02Icon,
8
+ GridTableIcon,
9
+ LayoutTopIcon,
10
+ MultiplicationSignIcon,
11
+ SplitIcon,
12
+ } from '@hugeicons/core-free-icons';
13
+ import { HugeiconsIcon } from '@hugeicons/react';
14
+ import {
15
+ deleteColumn,
16
+ deleteRow,
17
+ deleteTable,
18
+ insertTable,
19
+ insertTableColumn,
20
+ insertTableRow,
21
+ mergeTableCells,
22
+ splitTableCell,
23
+ } from '@platejs/table';
24
+ import {
25
+ DropdownMenuSub,
26
+ DropdownMenuSubContent,
27
+ DropdownMenuSubTrigger,
28
+ } from '@saena-io/ui/components/dropdown-menu';
29
+ import type { ToolbarNode } from '@saena-io/ui/components/toolbar';
30
+ import { cn } from '@saena-io/ui/lib/utils';
31
+ import { useState } from 'react';
32
+ import type { RichTextEditor } from './plugins';
33
+
34
+ // The Table control as a declarative toolbar menu node (see `tableMenuNode`): an Insert-table grid picker
35
+ // plus Cell / Row / Column submenus and Delete table. Cell/Row/Column/Delete are disabled unless the caret
36
+ // is inside a table. The 8×8 grid picker is custom menu content (not a menu item), so it closes the
37
+ // dropdown via useToolbarMenuClose() after inserting. Building a similar tool = build the same node shape.
38
+
39
+ /** Flip the table's first row between header cells (th) and body cells (td) — Plate has no built-in toggle. */
40
+ function toggleHeaderRow(editor: RichTextEditor) {
41
+ const tableEntry = editor.api.node({ match: { type: 'table' } });
42
+ if (!tableEntry) return;
43
+ const [tableNode, tablePath] = tableEntry;
44
+ const firstRow = (tableNode as { children?: { children?: { type?: string }[] }[] }).children?.[0];
45
+ const firstCellType = firstRow?.children?.[0]?.type;
46
+ const nextType = firstCellType === 'th' ? 'td' : 'th';
47
+ editor.tf.setNodes(
48
+ { type: nextType },
49
+ {
50
+ at: [...tablePath, 0],
51
+ match: (n: { type?: unknown }) => n.type === 'td' || n.type === 'th',
52
+ mode: 'lowest',
53
+ },
54
+ );
55
+ }
56
+
57
+ /** The 8×8 grid-size picker; hovering sizes the selection, clicking inserts that table. Focusing the editor
58
+ * afterwards dismisses the dropdown (focus leaves the menu), so no explicit close is needed. */
59
+ function TableGridPicker({ editor }: { editor: RichTextEditor | null }) {
60
+ const [size, setSize] = useState({ rows: 0, cols: 0 });
61
+ const insertSize = (rows: number, cols: number) => {
62
+ if (!editor) return;
63
+ // Ensure an insertion point if the editor was never focused.
64
+ if (!editor.selection) {
65
+ const end = editor.api.end([]);
66
+ if (end) editor.tf.select(end);
67
+ }
68
+ insertTable(editor, { rowCount: rows, colCount: cols }, { select: true });
69
+ editor.tf.focus();
70
+ };
71
+ return (
72
+ <div className="flex flex-col gap-1.5" onMouseDown={(e) => e.preventDefault()}>
73
+ <div className="grid grid-cols-8 gap-0.5">
74
+ {Array.from({ length: 64 }, (_, i) => {
75
+ const r = Math.floor(i / 8);
76
+ const c = i % 8;
77
+ const on = r < size.rows && c < size.cols;
78
+ return (
79
+ <button
80
+ type="button"
81
+ key={`cell-${r}-${c}`}
82
+ aria-label={`${r + 1} by ${c + 1}`}
83
+ onMouseMove={() => setSize({ rows: r + 1, cols: c + 1 })}
84
+ onClick={() => insertSize(r + 1, c + 1)}
85
+ className={cn(
86
+ 'size-4 rounded-[2px] border',
87
+ on ? 'border-primary bg-primary/20' : 'border-border bg-muted/50',
88
+ )}
89
+ />
90
+ );
91
+ })}
92
+ </div>
93
+ <div className="text-center text-muted-foreground text-xs">
94
+ {size.rows} × {size.cols}
95
+ </div>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ /** The "Insert table" entry — a submenu holding the grid picker (custom content, not a menu item). */
101
+ function TableGridSubmenu({ editor }: { editor: RichTextEditor | null }) {
102
+ return (
103
+ <DropdownMenuSub>
104
+ <DropdownMenuSubTrigger>
105
+ <HugeiconsIcon icon={GridTableIcon} strokeWidth={2} className="size-4" />
106
+ Insert table
107
+ </DropdownMenuSubTrigger>
108
+ <DropdownMenuSubContent className="w-auto p-2">
109
+ <TableGridPicker editor={editor} />
110
+ </DropdownMenuSubContent>
111
+ </DropdownMenuSub>
112
+ );
113
+ }
114
+
115
+ /** Builds the Table menu node for the toolbar config. */
116
+ export function tableMenuNode(editor: RichTextEditor | null): ToolbarNode {
117
+ // "Caret inside a table" — scope to the selection's ancestors (not the whole doc).
118
+ const inTable = Boolean(editor?.selection && editor.api.above({ match: { type: 'table' } }));
119
+ const act = (fn: (e: RichTextEditor) => void) => () => {
120
+ if (!editor) return;
121
+ fn(editor);
122
+ editor.tf.focus();
123
+ };
124
+ return {
125
+ kind: 'menu',
126
+ id: 'table',
127
+ label: 'Table',
128
+ icon: GridTableIcon,
129
+ variant: 'plain',
130
+ items: [
131
+ { kind: 'custom', id: 'table-grid', collapsed: <TableGridSubmenu editor={editor} /> },
132
+ {
133
+ kind: 'menu',
134
+ id: 'table-cell',
135
+ label: 'Cell',
136
+ disabled: !inTable,
137
+ items: [
138
+ {
139
+ kind: 'button',
140
+ id: 'merge',
141
+ label: 'Merge cells',
142
+ icon: CombineIcon,
143
+ onSelect: act((e) => mergeTableCells(e)),
144
+ },
145
+ {
146
+ kind: 'button',
147
+ id: 'split',
148
+ label: 'Split cell',
149
+ icon: SplitIcon,
150
+ onSelect: act((e) => splitTableCell(e)),
151
+ },
152
+ ],
153
+ },
154
+ {
155
+ kind: 'menu',
156
+ id: 'table-row',
157
+ label: 'Row',
158
+ disabled: !inTable,
159
+ items: [
160
+ {
161
+ kind: 'button',
162
+ id: 'row-before',
163
+ label: 'Insert row before',
164
+ icon: ArrowUp01Icon,
165
+ onSelect: act((e) => insertTableRow(e, { before: true })),
166
+ },
167
+ {
168
+ kind: 'button',
169
+ id: 'row-after',
170
+ label: 'Insert row after',
171
+ icon: ArrowDown01Icon,
172
+ onSelect: act((e) => insertTableRow(e)),
173
+ },
174
+ {
175
+ kind: 'button',
176
+ id: 'row-delete',
177
+ label: 'Delete row',
178
+ icon: MultiplicationSignIcon,
179
+ onSelect: act((e) => deleteRow(e)),
180
+ },
181
+ { kind: 'separator', id: 'row-sep' },
182
+ {
183
+ kind: 'button',
184
+ id: 'row-header',
185
+ label: 'Toggle header row',
186
+ icon: LayoutTopIcon,
187
+ onSelect: act(toggleHeaderRow),
188
+ },
189
+ ],
190
+ },
191
+ {
192
+ kind: 'menu',
193
+ id: 'table-column',
194
+ label: 'Column',
195
+ disabled: !inTable,
196
+ items: [
197
+ {
198
+ kind: 'button',
199
+ id: 'col-before',
200
+ label: 'Insert column before',
201
+ icon: ArrowLeft01Icon,
202
+ onSelect: act((e) => insertTableColumn(e, { before: true })),
203
+ },
204
+ {
205
+ kind: 'button',
206
+ id: 'col-after',
207
+ label: 'Insert column after',
208
+ icon: ArrowRight01Icon,
209
+ onSelect: act((e) => insertTableColumn(e)),
210
+ },
211
+ {
212
+ kind: 'button',
213
+ id: 'col-delete',
214
+ label: 'Delete column',
215
+ icon: MultiplicationSignIcon,
216
+ onSelect: act((e) => deleteColumn(e)),
217
+ },
218
+ ],
219
+ },
220
+ { kind: 'separator', id: 'table-sep' },
221
+ {
222
+ kind: 'button',
223
+ id: 'delete-table',
224
+ label: 'Delete table',
225
+ icon: Delete02Icon,
226
+ destructive: true,
227
+ disabled: !inTable,
228
+ onSelect: act((e) => deleteTable(e)),
229
+ },
230
+ ],
231
+ };
232
+ }
@@ -0,0 +1,36 @@
1
+ import { ArrowRight01Icon } from '@hugeicons/core-free-icons';
2
+ import { HugeiconsIcon } from '@hugeicons/react';
3
+ import { useToggleButton, useToggleButtonState } from '@platejs/toggle/react';
4
+ import { Button } from '@saena-io/ui/components/button';
5
+ import { cn } from '@saena-io/ui/lib/utils';
6
+ import { PlateElement, type PlateElementProps } from 'platejs/react';
7
+
8
+ // A collapsible "toggle" block: a disclosure chevron that shows/hides the blocks indented beneath it. The
9
+ // @platejs/toggle plugin keeps per-block open/closed state (keyed by the block id the editor already assigns
10
+ // for drag-and-drop) and hides the enclosed blocks when closed. Rendered under a concrete <Plate>, so the
11
+ // toggle hooks resolve to the right editor — unlike the shared toolbar (the dual-pane controller hazard).
12
+ export function ToggleElement(props: PlateElementProps) {
13
+ const state = useToggleButtonState(props.element.id as string);
14
+ const { open, buttonProps } = useToggleButton(state);
15
+ return (
16
+ <PlateElement {...props} className="relative mb-1 pl-6">
17
+ <Button
18
+ type="button"
19
+ size="icon-sm"
20
+ variant="ghost"
21
+ title={open ? 'Collapse' : 'Expand'}
22
+ aria-label={open ? 'Collapse' : 'Expand'}
23
+ className="-left-1 absolute top-0 size-6 text-muted-foreground"
24
+ contentEditable={false}
25
+ {...buttonProps}
26
+ >
27
+ <HugeiconsIcon
28
+ icon={ArrowRight01Icon}
29
+ strokeWidth={2}
30
+ className={cn('size-4 transition-transform', open && 'rotate-90')}
31
+ />
32
+ </Button>
33
+ {props.children}
34
+ </PlateElement>
35
+ );
36
+ }
@@ -0,0 +1,41 @@
1
+ import type { ToolbarGroup, ToolbarNode } from '@saena-io/ui/components/toolbar';
2
+ import type { RichTextExtension, ToolbarItem } from './extension';
3
+
4
+ /**
5
+ * Place extension-contributed controls into the toolbar by their `slot` (see extension.ts). Items whose slot
6
+ * matches a core group are appended to it — after the built-in controls, so those keep their positions — and
7
+ * sorted by `order`; `ai` / `end` items have no core group, so they form their own trailing group (AI first,
8
+ * then a catch-all "More"). Mutates `groups`, which the toolbar rebuilds each render (no shared state).
9
+ *
10
+ * Pure (no React rendering) and free of editor/platejs imports — split out from toolbar.tsx so the placement
11
+ * rules are unit-testable in isolation.
12
+ */
13
+ export function mergeExtensionItems(groups: ToolbarGroup[], extensions: RichTextExtension[]): void {
14
+ const addonItems = extensions.flatMap((e) => e.toolbar ?? []);
15
+ if (addonItems.length === 0) return;
16
+ const toNode = (it: ToolbarItem): ToolbarNode => ({
17
+ kind: 'custom',
18
+ id: it.id,
19
+ expanded: it.render(),
20
+ });
21
+ const nodesForSlot = (slot: string) =>
22
+ addonItems
23
+ .filter((i) => i.slot === slot)
24
+ .sort((a, b) => a.order - b.order)
25
+ .map(toNode);
26
+ // Append into the matching core group (kept in place so built-in controls don't move).
27
+ for (const group of groups) {
28
+ const extra = nodesForSlot(group.id);
29
+ if (extra.length > 0) group.items = [...group.items, ...extra];
30
+ }
31
+ // Slots with no core group get a trailing group of their own.
32
+ const coreIds = new Set(groups.map((g) => g.id));
33
+ for (const { slot, label } of [
34
+ { slot: 'ai', label: 'AI' },
35
+ { slot: 'end', label: 'More' },
36
+ ] as const) {
37
+ if (coreIds.has(slot)) continue;
38
+ const items = nodesForSlot(slot);
39
+ if (items.length > 0) groups.push({ id: slot, label, collapse: 'never', items });
40
+ }
41
+ }