@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,934 @@
1
+ import * as React from 'react';
2
+
3
+ import { GripVerticalIcon } from '@hugeicons/core-free-icons';
4
+ import { HugeiconsIcon } from '@hugeicons/react';
5
+ import { useDraggable, useDropLine } from '@platejs/dnd';
6
+ import { resizeLengthClampStatic } from '@platejs/resizable';
7
+ import { BlockSelectionPlugin, useBlockSelected } from '@platejs/selection/react';
8
+ import {
9
+ getTableColumnCount,
10
+ setTableColSize,
11
+ setTableMarginLeft,
12
+ setTableRowSize,
13
+ } from '@platejs/table';
14
+ import {
15
+ TablePlugin,
16
+ TableProvider,
17
+ roundCellSizeToStep,
18
+ useCellIndices,
19
+ useOverrideColSize,
20
+ useOverrideMarginLeft,
21
+ useOverrideRowSize,
22
+ useTableCellBorders,
23
+ useTableColSizes,
24
+ useTableElement,
25
+ useTableSelectionDom,
26
+ useTableValue,
27
+ } from '@platejs/table/react';
28
+ import { cn } from '@saena-io/ui/lib/utils';
29
+ import {
30
+ KEYS,
31
+ PathApi,
32
+ type TElement,
33
+ type TTableCellElement,
34
+ type TTableElement,
35
+ type TTableRowElement,
36
+ } from 'platejs';
37
+ import {
38
+ PlateElement,
39
+ type PlateElementProps,
40
+ useComposedRef,
41
+ useEditorPlugin,
42
+ useEditorRef,
43
+ useElement,
44
+ useElementSelector,
45
+ usePluginOption,
46
+ useReadOnly,
47
+ withHOC,
48
+ } from 'platejs/react';
49
+
50
+ import { blockSelectionVariants } from './block-selection';
51
+
52
+ // Table renderers ported from the PlateJS example (table-node.tsx) into SAENA. The key fix vs. the
53
+ // previous renderer: cells are <td>/<th> with p-0 and an inner editable <div> (px-3 py-2 + min-height)
54
+ // so even empty cells are clickable/editable. Features kept: column/row/margin resize controller,
55
+ // cell selection DOM, row drag handle, per-side borders, and the cell-selected overlay. The example's
56
+ // in-table floating toolbar (borders/merge/split/cell-background) is intentionally stripped — SAENA has
57
+ // its own table-toolbar.tsx for those. Palette: 'brand' → 'primary'.
58
+
59
+ type TableResizeDirection = 'bottom' | 'left' | 'right';
60
+
61
+ type TableResizeStartOptions = {
62
+ colIndex: number;
63
+ direction: TableResizeDirection;
64
+ handleKey: string;
65
+ rowIndex: number;
66
+ };
67
+
68
+ type TableResizeDragState = {
69
+ colIndex: number;
70
+ direction: TableResizeDirection;
71
+ initialPosition: number;
72
+ initialSize: number;
73
+ marginLeft: number;
74
+ rowIndex: number;
75
+ };
76
+
77
+ type TableResizeContextValue = {
78
+ disableMarginLeft: boolean;
79
+ clearResizePreview: (handleKey: string) => void;
80
+ setResizePreview: (
81
+ event: React.PointerEvent<HTMLDivElement>,
82
+ options: TableResizeStartOptions,
83
+ ) => void;
84
+ startResize: (
85
+ event: React.PointerEvent<HTMLDivElement>,
86
+ options: TableResizeStartOptions,
87
+ ) => void;
88
+ };
89
+
90
+ const TABLE_CONTROL_COLUMN_WIDTH = 8;
91
+ const TABLE_DEFAULT_COLUMN_WIDTH = 120;
92
+ const TABLE_DEFERRED_COLUMN_RESIZE_CELL_COUNT = 1200;
93
+
94
+ const TableResizeContext = React.createContext<TableResizeContextValue | null>(null);
95
+
96
+ function useTableResizeContext() {
97
+ const context = React.useContext(TableResizeContext);
98
+
99
+ if (!context) {
100
+ throw new Error('TableResizeContext is missing');
101
+ }
102
+
103
+ return context;
104
+ }
105
+
106
+ function useTableResizeController({
107
+ deferColumnResize,
108
+ dragIndicatorRef,
109
+ hoverIndicatorRef,
110
+ marginLeft,
111
+ controlColumnWidth,
112
+ tablePath,
113
+ tableRef,
114
+ wrapperRef,
115
+ }: {
116
+ deferColumnResize: boolean;
117
+ dragIndicatorRef: React.RefObject<HTMLDivElement | null>;
118
+ hoverIndicatorRef: React.RefObject<HTMLDivElement | null>;
119
+ marginLeft: number;
120
+ controlColumnWidth: number;
121
+ tablePath: number[];
122
+ tableRef: React.RefObject<HTMLTableElement | null>;
123
+ wrapperRef: React.RefObject<HTMLDivElement | null>;
124
+ }) {
125
+ const { editor, getOptions } = useEditorPlugin(TablePlugin);
126
+ const { disableMarginLeft = false, minColumnWidth = 0 } = getOptions();
127
+ const colSizes = useTableColSizes({
128
+ disableOverrides: true,
129
+ });
130
+ const effectiveColSizes = React.useMemo(
131
+ () => colSizes.map((colSize) => colSize || TABLE_DEFAULT_COLUMN_WIDTH),
132
+ [colSizes],
133
+ );
134
+ const effectiveColSizesRef = React.useRef(effectiveColSizes);
135
+ const activeHandleKeyRef = React.useRef<string | null>(null);
136
+ const activeRowElementRef = React.useRef<HTMLTableRowElement | null>(null);
137
+ const cleanupListenersRef = React.useRef<(() => void) | null>(null);
138
+ const marginLeftRef = React.useRef(marginLeft);
139
+ const dragStateRef = React.useRef<TableResizeDragState | null>(null);
140
+ const frozenRowIndicesRef = React.useRef<number[] | null>(null);
141
+ const previewHandleKeyRef = React.useRef<string | null>(null);
142
+ const overrideColSize = useOverrideColSize();
143
+ const overrideMarginLeft = useOverrideMarginLeft();
144
+ const overrideRowSize = useOverrideRowSize();
145
+
146
+ React.useEffect(() => {
147
+ effectiveColSizesRef.current = effectiveColSizes;
148
+ }, [effectiveColSizes]);
149
+
150
+ React.useEffect(() => {
151
+ marginLeftRef.current = marginLeft;
152
+ }, [marginLeft]);
153
+
154
+ const hideDeferredResizeIndicator = React.useCallback(() => {
155
+ const indicator = dragIndicatorRef.current;
156
+
157
+ if (!indicator) return;
158
+
159
+ indicator.style.display = 'none';
160
+ indicator.style.removeProperty('left');
161
+ }, [dragIndicatorRef]);
162
+
163
+ const showDeferredResizeIndicator = React.useCallback(
164
+ (offset: number) => {
165
+ const indicator = dragIndicatorRef.current;
166
+
167
+ if (!indicator) return;
168
+
169
+ indicator.style.display = 'block';
170
+ indicator.style.left = `${offset}px`;
171
+ },
172
+ [dragIndicatorRef],
173
+ );
174
+
175
+ const hideResizeIndicator = React.useCallback(() => {
176
+ const indicator = hoverIndicatorRef.current;
177
+
178
+ if (!indicator) return;
179
+
180
+ indicator.style.display = 'none';
181
+ indicator.style.removeProperty('left');
182
+ }, [hoverIndicatorRef]);
183
+
184
+ const clearFrozenRowHeights = React.useCallback(() => {
185
+ const frozenRowIndices = frozenRowIndicesRef.current;
186
+
187
+ if (!frozenRowIndices) return;
188
+
189
+ frozenRowIndicesRef.current = null;
190
+
191
+ frozenRowIndices.forEach((rowIndex) => {
192
+ overrideRowSize(rowIndex, null);
193
+ });
194
+ }, [overrideRowSize]);
195
+
196
+ const freezeRowHeights = React.useCallback(() => {
197
+ const table = tableRef.current;
198
+
199
+ if (!table || deferColumnResize) return;
200
+
201
+ clearFrozenRowHeights();
202
+
203
+ const frozenRowIndices: number[] = [];
204
+
205
+ Array.from(table.rows).forEach((row, rowIndex) => {
206
+ const height = row.getBoundingClientRect().height;
207
+
208
+ if (!height) return;
209
+
210
+ overrideRowSize(rowIndex, height);
211
+ frozenRowIndices.push(rowIndex);
212
+ });
213
+
214
+ frozenRowIndicesRef.current = frozenRowIndices;
215
+ }, [clearFrozenRowHeights, deferColumnResize, overrideRowSize, tableRef]);
216
+
217
+ const showResizeIndicatorAtOffset = React.useCallback(
218
+ (offset: number) => {
219
+ const indicator = hoverIndicatorRef.current;
220
+
221
+ if (!indicator) return;
222
+
223
+ indicator.style.display = 'block';
224
+ indicator.style.left = `${offset}px`;
225
+ },
226
+ [hoverIndicatorRef],
227
+ );
228
+
229
+ const showResizeIndicator = React.useCallback(
230
+ ({
231
+ event,
232
+ direction,
233
+ }: Pick<TableResizeStartOptions, 'direction'> & {
234
+ event: React.PointerEvent<HTMLDivElement>;
235
+ }) => {
236
+ if (direction === 'bottom') return;
237
+
238
+ const wrapper = wrapperRef.current;
239
+
240
+ if (!wrapper) return;
241
+
242
+ const handleRect = event.currentTarget.getBoundingClientRect();
243
+ const wrapperRect = wrapper.getBoundingClientRect();
244
+ const boundaryOffset = handleRect.left - wrapperRect.left + handleRect.width / 2;
245
+
246
+ showResizeIndicatorAtOffset(boundaryOffset);
247
+ },
248
+ [showResizeIndicatorAtOffset, wrapperRef],
249
+ );
250
+
251
+ const setResizePreview = React.useCallback(
252
+ (event: React.PointerEvent<HTMLDivElement>, options: TableResizeStartOptions) => {
253
+ if (activeHandleKeyRef.current) return;
254
+
255
+ previewHandleKeyRef.current = options.handleKey;
256
+ showResizeIndicator({ ...options, event });
257
+ },
258
+ [showResizeIndicator],
259
+ );
260
+
261
+ const clearResizePreview = React.useCallback(
262
+ (handleKey: string) => {
263
+ if (activeHandleKeyRef.current) return;
264
+ if (previewHandleKeyRef.current !== handleKey) return;
265
+
266
+ previewHandleKeyRef.current = null;
267
+ hideResizeIndicator();
268
+ },
269
+ [hideResizeIndicator],
270
+ );
271
+
272
+ const commitColSize = React.useCallback(
273
+ (colIndex: number, width: number) => {
274
+ setTableColSize(editor, { colIndex, width }, { at: tablePath });
275
+ setTimeout(() => overrideColSize(colIndex, null), 0);
276
+ },
277
+ [editor, overrideColSize, tablePath],
278
+ );
279
+
280
+ const commitRowSize = React.useCallback(
281
+ (rowIndex: number, height: number) => {
282
+ setTableRowSize(editor, { height, rowIndex }, { at: tablePath });
283
+ setTimeout(() => overrideRowSize(rowIndex, null), 0);
284
+ },
285
+ [editor, overrideRowSize, tablePath],
286
+ );
287
+
288
+ const commitMarginLeft = React.useCallback(
289
+ (nextMarginLeft: number) => {
290
+ setTableMarginLeft(editor, { marginLeft: nextMarginLeft }, { at: tablePath });
291
+ setTimeout(() => overrideMarginLeft(null), 0);
292
+ },
293
+ [editor, overrideMarginLeft, tablePath],
294
+ );
295
+
296
+ const getColumnBoundaryOffset = React.useCallback(
297
+ (colIndex: number, currentWidth: number) =>
298
+ controlColumnWidth +
299
+ effectiveColSizesRef.current
300
+ .slice(0, colIndex)
301
+ .reduce((total, colSize) => total + colSize, 0) +
302
+ currentWidth,
303
+ [controlColumnWidth],
304
+ );
305
+
306
+ const applyResize = React.useCallback(
307
+ (event: PointerEvent, finished: boolean) => {
308
+ const dragState = dragStateRef.current;
309
+
310
+ if (!dragState) return;
311
+
312
+ const currentPosition = dragState.direction === 'bottom' ? event.clientY : event.clientX;
313
+ const delta = currentPosition - dragState.initialPosition;
314
+
315
+ if (dragState.direction === 'bottom') {
316
+ const newHeight = roundCellSizeToStep(dragState.initialSize + delta, undefined);
317
+
318
+ if (finished) {
319
+ commitRowSize(dragState.rowIndex, newHeight);
320
+ } else {
321
+ overrideRowSize(dragState.rowIndex, newHeight);
322
+ }
323
+
324
+ return;
325
+ }
326
+
327
+ if (dragState.direction === 'left') {
328
+ const initial = effectiveColSizesRef.current[dragState.colIndex] ?? dragState.initialSize;
329
+ const complement = (width: number) => initial + dragState.marginLeft - width;
330
+ const nextMarginLeft = roundCellSizeToStep(
331
+ resizeLengthClampStatic(dragState.marginLeft + delta, {
332
+ max: complement(minColumnWidth),
333
+ min: 0,
334
+ }),
335
+ undefined,
336
+ );
337
+ const nextWidth = complement(nextMarginLeft);
338
+
339
+ if (finished) {
340
+ commitMarginLeft(nextMarginLeft);
341
+ commitColSize(dragState.colIndex, nextWidth);
342
+ } else if (deferColumnResize) {
343
+ showDeferredResizeIndicator(controlColumnWidth + (nextMarginLeft - dragState.marginLeft));
344
+ } else {
345
+ showResizeIndicatorAtOffset(controlColumnWidth + (nextMarginLeft - dragState.marginLeft));
346
+ overrideMarginLeft(nextMarginLeft);
347
+ overrideColSize(dragState.colIndex, nextWidth);
348
+ }
349
+
350
+ return;
351
+ }
352
+
353
+ const currentInitial =
354
+ effectiveColSizesRef.current[dragState.colIndex] ?? dragState.initialSize;
355
+ const nextInitial = effectiveColSizesRef.current[dragState.colIndex + 1];
356
+ const complement = (width: number) => currentInitial + (nextInitial ?? 0) - width;
357
+ const currentWidth = roundCellSizeToStep(
358
+ resizeLengthClampStatic(currentInitial + delta, {
359
+ max: nextInitial ? complement(minColumnWidth) : undefined,
360
+ min: minColumnWidth,
361
+ }),
362
+ undefined,
363
+ );
364
+ const nextWidth = nextInitial ? complement(currentWidth) : undefined;
365
+
366
+ if (finished) {
367
+ commitColSize(dragState.colIndex, currentWidth);
368
+
369
+ if (nextWidth !== undefined) {
370
+ commitColSize(dragState.colIndex + 1, nextWidth);
371
+ }
372
+ } else if (deferColumnResize) {
373
+ showDeferredResizeIndicator(getColumnBoundaryOffset(dragState.colIndex, currentWidth));
374
+ } else {
375
+ showResizeIndicatorAtOffset(getColumnBoundaryOffset(dragState.colIndex, currentWidth));
376
+ overrideColSize(dragState.colIndex, currentWidth);
377
+
378
+ if (nextWidth !== undefined) {
379
+ overrideColSize(dragState.colIndex + 1, nextWidth);
380
+ }
381
+ }
382
+ },
383
+ [
384
+ commitColSize,
385
+ commitMarginLeft,
386
+ commitRowSize,
387
+ controlColumnWidth,
388
+ deferColumnResize,
389
+ getColumnBoundaryOffset,
390
+ showDeferredResizeIndicator,
391
+ showResizeIndicatorAtOffset,
392
+ minColumnWidth,
393
+ overrideColSize,
394
+ overrideMarginLeft,
395
+ overrideRowSize,
396
+ ],
397
+ );
398
+
399
+ const stopResize = React.useCallback(() => {
400
+ cleanupListenersRef.current?.();
401
+ cleanupListenersRef.current = null;
402
+ activeHandleKeyRef.current = null;
403
+ previewHandleKeyRef.current = null;
404
+ dragStateRef.current = null;
405
+
406
+ if (activeRowElementRef.current) {
407
+ delete activeRowElementRef.current.dataset.tableResizing;
408
+ activeRowElementRef.current = null;
409
+ }
410
+
411
+ hideDeferredResizeIndicator();
412
+ hideResizeIndicator();
413
+ clearFrozenRowHeights();
414
+ }, [clearFrozenRowHeights, hideDeferredResizeIndicator, hideResizeIndicator]);
415
+
416
+ React.useEffect(() => stopResize, [stopResize]);
417
+
418
+ const startResize = React.useCallback(
419
+ (
420
+ event: React.PointerEvent<HTMLDivElement>,
421
+ { colIndex, direction, handleKey, rowIndex }: TableResizeStartOptions,
422
+ ) => {
423
+ const rowHeight = tableRef.current?.rows.item(rowIndex)?.getBoundingClientRect().height ?? 0;
424
+
425
+ dragStateRef.current = {
426
+ colIndex,
427
+ direction,
428
+ initialPosition: direction === 'bottom' ? event.clientY : event.clientX,
429
+ initialSize:
430
+ direction === 'bottom'
431
+ ? rowHeight
432
+ : (effectiveColSizesRef.current[colIndex] ?? TABLE_DEFAULT_COLUMN_WIDTH),
433
+ marginLeft: marginLeftRef.current,
434
+ rowIndex,
435
+ };
436
+ activeHandleKeyRef.current = handleKey;
437
+ previewHandleKeyRef.current = null;
438
+
439
+ const rowElement = tableRef.current?.rows.item(rowIndex) ?? null;
440
+
441
+ if (activeRowElementRef.current && activeRowElementRef.current !== rowElement) {
442
+ delete activeRowElementRef.current.dataset.tableResizing;
443
+ }
444
+
445
+ activeRowElementRef.current = rowElement;
446
+
447
+ if (rowElement) {
448
+ rowElement.dataset.tableResizing = 'true';
449
+ }
450
+
451
+ cleanupListenersRef.current?.();
452
+
453
+ if (direction !== 'bottom') {
454
+ freezeRowHeights();
455
+ }
456
+
457
+ const handlePointerMove = (pointerEvent: PointerEvent) => {
458
+ applyResize(pointerEvent, false);
459
+ };
460
+
461
+ const handlePointerEnd = (pointerEvent: PointerEvent) => {
462
+ applyResize(pointerEvent, true);
463
+ stopResize();
464
+ };
465
+
466
+ window.addEventListener('pointermove', handlePointerMove);
467
+ window.addEventListener('pointerup', handlePointerEnd);
468
+ window.addEventListener('pointercancel', handlePointerEnd);
469
+
470
+ cleanupListenersRef.current = () => {
471
+ window.removeEventListener('pointermove', handlePointerMove);
472
+ window.removeEventListener('pointerup', handlePointerEnd);
473
+ window.removeEventListener('pointercancel', handlePointerEnd);
474
+ };
475
+
476
+ if (deferColumnResize && direction !== 'bottom') {
477
+ hideResizeIndicator();
478
+ showDeferredResizeIndicator(
479
+ direction === 'left'
480
+ ? controlColumnWidth
481
+ : getColumnBoundaryOffset(
482
+ colIndex,
483
+ effectiveColSizesRef.current[colIndex] ?? TABLE_DEFAULT_COLUMN_WIDTH,
484
+ ),
485
+ );
486
+ } else {
487
+ showResizeIndicator({ direction, event });
488
+ }
489
+
490
+ event.preventDefault();
491
+ event.stopPropagation();
492
+ },
493
+ [
494
+ controlColumnWidth,
495
+ deferColumnResize,
496
+ getColumnBoundaryOffset,
497
+ hideResizeIndicator,
498
+ showDeferredResizeIndicator,
499
+ showResizeIndicator,
500
+ stopResize,
501
+ tableRef,
502
+ applyResize,
503
+ freezeRowHeights,
504
+ ],
505
+ );
506
+
507
+ return React.useMemo(
508
+ () => ({
509
+ clearResizePreview,
510
+ disableMarginLeft,
511
+ setResizePreview,
512
+ startResize,
513
+ }),
514
+ [clearResizePreview, disableMarginLeft, setResizePreview, startResize],
515
+ );
516
+ }
517
+
518
+ export const TableElement = withHOC(
519
+ TableProvider,
520
+ function TableElement({ children, ...props }: PlateElementProps<TTableElement>) {
521
+ const readOnly = useReadOnly();
522
+ const isSelectionAreaVisible = usePluginOption(BlockSelectionPlugin, 'isSelectionAreaVisible');
523
+ const hasControls = !readOnly && !isSelectionAreaVisible;
524
+ const { marginLeft, props: tableProps } = useTableElement();
525
+ const colSizes = useTableColSizes();
526
+ const controlColumnWidth = hasControls ? TABLE_CONTROL_COLUMN_WIDTH : 0;
527
+ const dragIndicatorRef = React.useRef<HTMLDivElement>(null);
528
+ const hoverIndicatorRef = React.useRef<HTMLDivElement>(null);
529
+ const deferColumnResize =
530
+ colSizes.length * props.element.children.length > TABLE_DEFERRED_COLUMN_RESIZE_CELL_COUNT;
531
+ const tablePath = useElementSelector(([, path]) => path, [], {
532
+ key: KEYS.table,
533
+ });
534
+ const tableRef = React.useRef<HTMLTableElement>(null);
535
+ const wrapperRef = React.useRef<HTMLDivElement>(null);
536
+ useTableSelectionDom(tableRef);
537
+ const resizeController = useTableResizeController({
538
+ controlColumnWidth,
539
+ deferColumnResize,
540
+ dragIndicatorRef,
541
+ hoverIndicatorRef,
542
+ marginLeft,
543
+ tablePath,
544
+ tableRef,
545
+ wrapperRef,
546
+ });
547
+ const resolvedColSizes = React.useMemo(() => {
548
+ if (colSizes.length > 0) {
549
+ return colSizes.map((colSize) => colSize || TABLE_DEFAULT_COLUMN_WIDTH);
550
+ }
551
+
552
+ return Array.from(
553
+ { length: getTableColumnCount(props.element) },
554
+ () => TABLE_DEFAULT_COLUMN_WIDTH,
555
+ );
556
+ }, [colSizes, props.element]);
557
+ const tableVariableStyle = React.useMemo(() => {
558
+ if (resolvedColSizes.length === 0) {
559
+ return;
560
+ }
561
+
562
+ return {
563
+ ...Object.fromEntries(
564
+ resolvedColSizes.map((colSize, index) => [`--table-col-${index}`, `${colSize}px`]),
565
+ ),
566
+ } as React.CSSProperties;
567
+ }, [resolvedColSizes]);
568
+ const tableStyle = React.useMemo(
569
+ () =>
570
+ ({
571
+ width: `${
572
+ resolvedColSizes.reduce((total, colSize) => total + colSize, 0) + controlColumnWidth
573
+ }px`,
574
+ }) as React.CSSProperties,
575
+ [controlColumnWidth, resolvedColSizes],
576
+ );
577
+
578
+ const isSelectingTable = useBlockSelected(props.element.id as string);
579
+
580
+ return (
581
+ <PlateElement
582
+ {...props}
583
+ className={cn(
584
+ 'overflow-x-auto py-5',
585
+ hasControls && '-ml-2 *:data-[slot=block-selection]:left-2',
586
+ )}
587
+ style={{ paddingLeft: marginLeft }}
588
+ >
589
+ <TableResizeContext.Provider value={resizeController}>
590
+ <div ref={wrapperRef} className="group/table relative w-fit" style={tableVariableStyle}>
591
+ <div
592
+ ref={dragIndicatorRef}
593
+ className="-translate-x-[1.5px] pointer-events-none absolute inset-y-0 z-36 hidden w-[3px] bg-ring/70"
594
+ contentEditable={false}
595
+ />
596
+ <div
597
+ ref={hoverIndicatorRef}
598
+ className="-translate-x-[1.5px] pointer-events-none absolute inset-y-0 z-35 hidden w-[3px] bg-ring/80"
599
+ contentEditable={false}
600
+ />
601
+ <table
602
+ ref={tableRef}
603
+ className={cn(
604
+ 'mr-0 ml-px table h-px table-fixed border-collapse',
605
+ 'data-[table-selecting=true]:[&_*::selection]:!bg-transparent',
606
+ 'data-[table-selecting=true]:[&_*::selection]:!text-inherit',
607
+ 'data-[table-selecting=true]:[&_*::-moz-selection]:!bg-transparent',
608
+ 'data-[table-selecting=true]:[&_*::-moz-selection]:!text-inherit',
609
+ 'data-[table-selecting=true]:[&_*]:!caret-transparent',
610
+ )}
611
+ style={tableStyle}
612
+ {...tableProps}
613
+ >
614
+ {resolvedColSizes.length > 0 && (
615
+ <colgroup>
616
+ {hasControls && (
617
+ <col
618
+ style={{
619
+ maxWidth: TABLE_CONTROL_COLUMN_WIDTH,
620
+ minWidth: TABLE_CONTROL_COLUMN_WIDTH,
621
+ width: TABLE_CONTROL_COLUMN_WIDTH,
622
+ }}
623
+ />
624
+ )}
625
+ {resolvedColSizes.map((colSize, index) => (
626
+ <col
627
+ key={index}
628
+ style={{
629
+ maxWidth: colSize,
630
+ minWidth: colSize,
631
+ width: colSize,
632
+ }}
633
+ />
634
+ ))}
635
+ </colgroup>
636
+ )}
637
+ <tbody className="min-w-full">{children}</tbody>
638
+ </table>
639
+
640
+ {isSelectingTable && (
641
+ <div className={blockSelectionVariants()} contentEditable={false} />
642
+ )}
643
+ </div>
644
+ </TableResizeContext.Provider>
645
+ </PlateElement>
646
+ );
647
+ },
648
+ );
649
+
650
+ export function TableRowElement({ children, ...props }: PlateElementProps<TTableRowElement>) {
651
+ const { element } = props;
652
+ const readOnly = useReadOnly();
653
+ const editor = useEditorRef();
654
+ const rowIndex = useElementSelector(([, path]) => path.at(-1) as number, [], {
655
+ key: KEYS.tr,
656
+ });
657
+ const rowSize = useElementSelector(([node]) => (node as TTableRowElement).size, [], {
658
+ key: KEYS.tr,
659
+ });
660
+ const rowSizeOverrides = useTableValue('rowSizeOverrides');
661
+ const rowMinHeight = rowSizeOverrides.get?.(rowIndex) ?? rowSize;
662
+ const isSelectionAreaVisible = usePluginOption(BlockSelectionPlugin, 'isSelectionAreaVisible');
663
+ const hasControls = !readOnly && !isSelectionAreaVisible;
664
+
665
+ const { isDragging, nodeRef, previewRef, handleRef } = useDraggable({
666
+ element,
667
+ type: element.type,
668
+ canDropNode: ({ dragEntry, dropEntry }) =>
669
+ PathApi.equals(PathApi.parent(dragEntry[1]), PathApi.parent(dropEntry[1])),
670
+ onDropHandler: (_, { dragItem }) => {
671
+ const dragElement = (dragItem as { element: TElement }).element;
672
+
673
+ if (dragElement) {
674
+ editor.tf.select(dragElement);
675
+ }
676
+ },
677
+ });
678
+
679
+ return (
680
+ <PlateElement
681
+ {...props}
682
+ ref={useComposedRef(props.ref, previewRef, nodeRef)}
683
+ as="tr"
684
+ className={cn('group/row', isDragging && 'opacity-50')}
685
+ style={
686
+ {
687
+ ...props.style,
688
+ '--tableRowMinHeight': rowMinHeight ? `${rowMinHeight}px` : undefined,
689
+ } as React.CSSProperties
690
+ }
691
+ >
692
+ {hasControls && (
693
+ <td className="w-2 min-w-2 max-w-2 select-none p-0" contentEditable={false}>
694
+ <RowDragHandle dragRef={handleRef} />
695
+ <RowDropLine />
696
+ </td>
697
+ )}
698
+
699
+ {children}
700
+ </PlateElement>
701
+ );
702
+ }
703
+
704
+ function useTableCellPresentation(element: TTableCellElement) {
705
+ const { api } = useEditorPlugin(TablePlugin);
706
+ const borders = useTableCellBorders({ element });
707
+ const { col, row } = useCellIndices();
708
+
709
+ const colSpan = api.table.getColSpan(element);
710
+ const rowSpan = api.table.getRowSpan(element);
711
+ const width = React.useMemo(() => {
712
+ const terms = Array.from(
713
+ { length: colSpan },
714
+ (_, offset) => `var(--table-col-${col + offset}, ${TABLE_DEFAULT_COLUMN_WIDTH}px)`,
715
+ );
716
+
717
+ return terms.length === 1 ? terms[0]! : `calc(${terms.join(' + ')})`;
718
+ }, [col, colSpan]);
719
+
720
+ return {
721
+ borders,
722
+ colIndex: col + colSpan - 1,
723
+ colSpan,
724
+ rowIndex: row + rowSpan - 1,
725
+ rowSpan,
726
+ width,
727
+ };
728
+ }
729
+
730
+ function RowDragHandle({ dragRef }: { dragRef: React.Ref<any> }) {
731
+ const editor = useEditorRef();
732
+ const element = useElement();
733
+
734
+ return (
735
+ <button
736
+ ref={dragRef}
737
+ type="button"
738
+ className={cn(
739
+ '-translate-y-1/2 absolute top-1/2 left-0 z-51 flex h-6 w-4 items-center justify-center rounded-md border border-input bg-background p-0',
740
+ 'cursor-grab active:cursor-grabbing',
741
+ 'opacity-0 transition-opacity duration-100 group-hover/row:opacity-100 group-data-[table-resizing=true]/row:opacity-0',
742
+ )}
743
+ onClick={() => {
744
+ editor.tf.select(element);
745
+ }}
746
+ >
747
+ <HugeiconsIcon
748
+ icon={GripVerticalIcon}
749
+ strokeWidth={2}
750
+ className="size-4 text-muted-foreground"
751
+ />
752
+ </button>
753
+ );
754
+ }
755
+
756
+ function RowDropLine() {
757
+ const { dropLine } = useDropLine();
758
+
759
+ if (!dropLine) return null;
760
+
761
+ return (
762
+ <div
763
+ className={cn(
764
+ 'absolute inset-x-0 left-2 z-50 h-0.5 bg-primary/50',
765
+ dropLine === 'top' ? '-top-px' : '-bottom-px',
766
+ )}
767
+ />
768
+ );
769
+ }
770
+
771
+ export function TableCellElement({
772
+ isHeader,
773
+ ...props
774
+ }: PlateElementProps<TTableCellElement> & {
775
+ isHeader?: boolean;
776
+ }) {
777
+ const readOnly = useReadOnly();
778
+ const element = props.element;
779
+
780
+ const tableId = useElementSelector(([node]) => node.id as string, [], {
781
+ key: KEYS.table,
782
+ });
783
+ const rowId = useElementSelector(([node]) => node.id as string, [], {
784
+ key: KEYS.tr,
785
+ });
786
+ const isSelectingTable = useBlockSelected(tableId);
787
+ const isSelectingRow = useBlockSelected(rowId) || isSelectingTable;
788
+ const isSelectionAreaVisible = usePluginOption(BlockSelectionPlugin, 'isSelectionAreaVisible');
789
+
790
+ const { borders, colIndex, colSpan, rowIndex, rowSpan, width } =
791
+ useTableCellPresentation(element);
792
+
793
+ return (
794
+ <PlateElement
795
+ {...props}
796
+ as={isHeader ? 'th' : 'td'}
797
+ className={cn(
798
+ 'relative h-full overflow-visible border-none bg-background p-0',
799
+ element.background ? 'bg-(--cellBackground)' : 'bg-background',
800
+ isHeader && 'text-left *:m-0',
801
+ 'before:size-full',
802
+ 'data-[table-cell-selected=true]:before:z-10',
803
+ 'data-[table-cell-selected=true]:before:bg-primary/5',
804
+ "before:absolute before:box-border before:select-none before:content-['']",
805
+ borders.bottom?.size && 'before:border-b before:border-b-border',
806
+ borders.right?.size && 'before:border-r before:border-r-border',
807
+ borders.left?.size && 'before:border-l before:border-l-border',
808
+ borders.top?.size && 'before:border-t before:border-t-border',
809
+ )}
810
+ style={
811
+ {
812
+ '--cellBackground': element.background,
813
+ maxWidth: width,
814
+ minWidth: width,
815
+ } as React.CSSProperties
816
+ }
817
+ attributes={{
818
+ ...props.attributes,
819
+ colSpan,
820
+ 'data-table-cell-id': element.id,
821
+ rowSpan,
822
+ }}
823
+ >
824
+ <div
825
+ className="relative z-20 box-border h-full px-3 py-2"
826
+ style={rowSpan === 1 ? { minHeight: 'var(--tableRowMinHeight, 0px)' } : undefined}
827
+ >
828
+ {props.children}
829
+ </div>
830
+
831
+ {!readOnly && !isSelectionAreaVisible && (
832
+ <TableCellResizeControls colIndex={colIndex} rowIndex={rowIndex} />
833
+ )}
834
+
835
+ {isSelectingRow && <div className={blockSelectionVariants()} contentEditable={false} />}
836
+ </PlateElement>
837
+ );
838
+ }
839
+
840
+ export function TableCellHeaderElement(props: React.ComponentProps<typeof TableCellElement>) {
841
+ return <TableCellElement {...props} isHeader />;
842
+ }
843
+
844
+ const TableCellResizeControls = React.memo(function TableCellResizeControls({
845
+ colIndex,
846
+ rowIndex,
847
+ }: {
848
+ colIndex: number;
849
+ rowIndex: number;
850
+ }) {
851
+ const { clearResizePreview, disableMarginLeft, setResizePreview, startResize } =
852
+ useTableResizeContext();
853
+ const rightHandleKey = `right:${rowIndex}:${colIndex}`;
854
+ const bottomHandleKey = `bottom:${rowIndex}:${colIndex}`;
855
+ const leftHandleKey = `left:${rowIndex}:${colIndex}`;
856
+ const isLeftHandle = colIndex === 0 && !disableMarginLeft;
857
+
858
+ return (
859
+ <div
860
+ className="group/resize pointer-events-none absolute inset-0 z-30 select-none"
861
+ contentEditable={false}
862
+ suppressContentEditableWarning={true}
863
+ >
864
+ <div
865
+ className="-top-2 -right-1 pointer-events-auto absolute z-40 h-[calc(100%_+_8px)] w-2 cursor-col-resize touch-none"
866
+ onPointerEnter={(event) => {
867
+ setResizePreview(event, {
868
+ colIndex,
869
+ direction: 'right',
870
+ handleKey: rightHandleKey,
871
+ rowIndex,
872
+ });
873
+ }}
874
+ onPointerLeave={() => {
875
+ clearResizePreview(rightHandleKey);
876
+ }}
877
+ onPointerDown={(event) => {
878
+ startResize(event, {
879
+ colIndex,
880
+ direction: 'right',
881
+ handleKey: rightHandleKey,
882
+ rowIndex,
883
+ });
884
+ }}
885
+ />
886
+ <div
887
+ className="-bottom-1 pointer-events-auto absolute left-0 z-40 h-2 w-full cursor-row-resize touch-none"
888
+ onPointerEnter={(event) => {
889
+ setResizePreview(event, {
890
+ colIndex,
891
+ direction: 'bottom',
892
+ handleKey: bottomHandleKey,
893
+ rowIndex,
894
+ });
895
+ }}
896
+ onPointerLeave={() => {
897
+ clearResizePreview(bottomHandleKey);
898
+ }}
899
+ onPointerDown={(event) => {
900
+ startResize(event, {
901
+ colIndex,
902
+ direction: 'bottom',
903
+ handleKey: bottomHandleKey,
904
+ rowIndex,
905
+ });
906
+ }}
907
+ />
908
+ {isLeftHandle && (
909
+ <div
910
+ className="-left-1 pointer-events-auto absolute top-0 z-40 h-full w-2 cursor-col-resize touch-none"
911
+ onPointerEnter={(event) => {
912
+ setResizePreview(event, {
913
+ colIndex,
914
+ direction: 'left',
915
+ handleKey: leftHandleKey,
916
+ rowIndex,
917
+ });
918
+ }}
919
+ onPointerLeave={() => {
920
+ clearResizePreview(leftHandleKey);
921
+ }}
922
+ onPointerDown={(event) => {
923
+ startResize(event, {
924
+ colIndex,
925
+ direction: 'left',
926
+ handleKey: leftHandleKey,
927
+ rowIndex,
928
+ });
929
+ }}
930
+ />
931
+ )}
932
+ </div>
933
+ );
934
+ });