@tristan2828/ui-foundation 4.61.1 → 4.63.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 (59) hide show
  1. package/conventions/docs/cell-patterns.md +16 -11
  2. package/conventions/docs/design-language.md +132 -23
  3. package/dist/components/app/app-shell.js +28 -2
  4. package/dist/components/app/board.js +1 -1
  5. package/dist/components/app/data-table.js +1 -1
  6. package/dist/components/app/foundation-providers.d.ts +14 -1
  7. package/dist/components/app/foundation-providers.js +4 -3
  8. package/dist/components/app/markdown.d.ts +7 -2
  9. package/dist/components/app/markdown.js +15 -20
  10. package/dist/components/app/milkdown-editor.d.ts +1 -1
  11. package/dist/components/app/milkdown-editor.js +58 -17
  12. package/dist/components/app/module-card.js +1 -1
  13. package/dist/components/app/palette-provider.d.ts +14 -0
  14. package/dist/components/app/palette-provider.js +35 -0
  15. package/dist/components/app/prose-look.d.ts +7 -0
  16. package/dist/components/app/rich-text-editor.d.ts +8 -1
  17. package/dist/components/app/rich-text-editor.js +6 -0
  18. package/dist/components/app/rich-text-format-items.js +1 -1
  19. package/dist/components/app/rich-text-node-views.d.ts +9 -3
  20. package/dist/components/app/rich-text-node-views.js +12 -8
  21. package/dist/components/app/rich-text-table-grips.d.ts +31 -0
  22. package/dist/components/app/rich-text-table-grips.js +216 -0
  23. package/dist/components/app/rich-text-table.d.ts +19 -2
  24. package/dist/components/app/rich-text-table.js +232 -1
  25. package/dist/components/app/rich-text-toolbar.d.ts +4 -9
  26. package/dist/components/app/rich-text-toolbar.js +9 -11
  27. package/dist/components/app/rich-text-widgets.js +2 -2
  28. package/dist/components/app/widget-card.js +2 -2
  29. package/dist/components/ui/badge.d.ts +1 -1
  30. package/dist/components/ui/badge.js +5 -2
  31. package/dist/components/ui/button.js +10 -10
  32. package/dist/components/ui/card.js +3 -3
  33. package/dist/components/ui/checkbox.js +1 -1
  34. package/dist/components/ui/combobox.js +1 -1
  35. package/dist/components/ui/dialog.js +1 -1
  36. package/dist/components/ui/input-group.js +1 -1
  37. package/dist/components/ui/input.js +1 -1
  38. package/dist/components/ui/progress.d.ts +7 -0
  39. package/dist/components/ui/progress.js +19 -0
  40. package/dist/components/ui/select.js +1 -1
  41. package/dist/components/ui/sheet.js +1 -1
  42. package/dist/components/ui/slider.d.ts +3 -0
  43. package/dist/components/ui/slider.js +12 -0
  44. package/dist/components/ui/textarea.js +1 -1
  45. package/dist/hooks/use-palette.d.ts +14 -0
  46. package/dist/hooks/use-palette.js +7 -0
  47. package/dist/index.d.ts +3 -1
  48. package/dist/index.js +2 -0
  49. package/dist/lib/markdown-table.d.ts +28 -0
  50. package/dist/lib/markdown-table.js +106 -0
  51. package/dist/lib/palettes.d.ts +21 -0
  52. package/dist/lib/palettes.js +74 -0
  53. package/package.json +1 -1
  54. package/styles/index.css +152 -0
  55. package/styles/theme.css +321 -11
  56. package/vite/index.d.ts +7 -1
  57. package/vite/pwa.js +21 -7
  58. package/dist/components/app/rich-text-table-menu.d.ts +0 -20
  59. package/dist/components/app/rich-text-table-menu.js +0 -53
@@ -20,11 +20,16 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
20
20
  // task-list item renders a real checkbox that toggles it.
21
21
  // - Callouts (`> [!NOTE]`, src/lib/markdown-callout.ts): a node of their
22
22
  // own, drawn as <Markdown> draws them, the icon a menu of kinds.
23
- // - Tables in a box that scrolls sideways (a tab stop the arrow keys
24
- // scroll while it does: rich-text-node-views.ts), with Notion's "add a row" and
25
- // "add a column" bars, Tab in the last cell adding a row and Enter going
26
- // down a column (rich-text-table.ts); the toolbar's Table menu does the
27
- // rest.
23
+ // - Tables whose columns fit their words, in a box that scrolls sideways
24
+ // (a tab stop the arrow keys scroll while it does: rich-text-node-views.ts),
25
+ // in one of two looks (`tableLook`: styles/index.css "Tables in written
26
+ // text"). Notion's "add a row" and "add a column" bars, and its grips: one
27
+ // on the column and one on the row the pointer or the caret is in, each
28
+ // a menu to insert, move, delete, align, sort and colour
29
+ // (rich-text-table-grips.tsx); right-click or Shift+F10 in a cell opens
30
+ // the cell's. Tab in the last cell adds a row and Enter goes down a
31
+ // column (rich-text-table.ts). Colours and a header column are kept as
32
+ // Markdown writes them (src/lib/markdown-table.ts).
28
33
  // - Edited blocks are written in the document's own bullet and rule marks.
29
34
  // - Markdown pasted as plain text arrives formatted (still no HTML), and a
30
35
  // copy out of the editor is Markdown.
@@ -86,7 +91,8 @@ import { calloutKind, calloutView, tableView } from './rich-text-node-views.js';
86
91
  import { droppedFiles, imageUploadPlugin, pastedImages, uploadImages, uploadPlaceholders } from './rich-text-image-upload.js';
87
92
  import { handleShortcut } from './rich-text-shortcuts.js';
88
93
  import { splitTaskItem, taskInputRule } from './rich-text-tasks.js';
89
- import { addRowAtEnd, handleTableKey, runTableAction } from './rich-text-table.js';
94
+ import { addRowAtEnd, colorTableCells, handleTableKey, runTableAction, tableLook as tableLookPlugin } from './rich-text-table.js';
95
+ import { TABLE_MENU_EVENT, TableGrips } from './rich-text-table-grips.js';
90
96
  import { applySlash, closeSlash, slashItems, slashOptionId, slashPlugin, slashQuery, } from './rich-text-slash.js';
91
97
  import { RichTextSlashMenu } from './rich-text-slash-menu.js';
92
98
  import { RichTextToolbar } from './rich-text-toolbar.js';
@@ -161,6 +167,9 @@ const highlightSchema = $markSchema('highlight', () => ({
161
167
  },
162
168
  },
163
169
  }));
170
+ // A table's cell colours and header column, drawn from the Markdown that
171
+ // keeps them (rich-text-table.ts tableLook).
172
+ const tableDecorations = $prose(() => tableLookPlugin());
164
173
  function emptyImageTitles(node) {
165
174
  if (node.type === 'image' && node.title == null)
166
175
  node.title = '';
@@ -357,7 +366,7 @@ function applyLink(view, target, href) {
357
366
  }
358
367
  view.dispatch(tr.scrollIntoView());
359
368
  }
360
- export default function MilkdownEditor({ id, defaultValue, onChange, onProblem, readOnly = false, autoFocus = false, placeholder, maxLength, toolbar = true, inline = false, upload, imageAccept, imageMaxSize, headingLevel = DEFAULT_HEADING_LEVEL, headingFrom = 'hash', foldKey, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, }) {
369
+ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem, readOnly = false, autoFocus = false, placeholder, maxLength, toolbar = true, inline = false, upload, imageAccept, imageMaxSize, headingLevel = DEFAULT_HEADING_LEVEL, headingFrom = 'hash', foldKey, tableLook = 'compact', className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, }) {
361
370
  const rootRef = useRef(null);
362
371
  const editorRef = useRef(null);
363
372
  const [linkBox, setLinkBox] = useState(null);
@@ -753,6 +762,16 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
753
762
  return true;
754
763
  if (handleTableKey(view.state, view.dispatch, event))
755
764
  return true;
765
+ // Shift+F10 or the menu key in a table: the cell's menu, from its grips.
766
+ if (((event.key === 'F10' && event.shiftKey) || event.key === 'ContextMenu') && isInTable(view.state)) {
767
+ const at = view.domAtPos(view.state.selection.from).node;
768
+ const frame = (at instanceof Element ? at : at.parentElement)?.closest('[data-slot=rich-text-table]');
769
+ if (frame) {
770
+ event.preventDefault();
771
+ frame.dispatchEvent(new CustomEvent(TABLE_MENU_EVENT));
772
+ return true;
773
+ }
774
+ }
756
775
  // Enter in a task item: the next one, unticked (rich-text-tasks.ts).
757
776
  if (event.key === 'Enter' && plain && !event.isComposing && splitTaskItem(view.state, view.dispatch))
758
777
  return true;
@@ -778,6 +797,7 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
778
797
  .use(calloutSchema)
779
798
  .use(highlightRemark)
780
799
  .use(highlightSchema)
800
+ .use(tableDecorations)
781
801
  .use(imageTitles)
782
802
  .use(history)
783
803
  .use(changes)
@@ -861,10 +881,33 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
861
881
  // The editable element: where a menu over the text gives focus back
862
882
  // (ProseMirror puts the caret back where it was as it takes it).
863
883
  const textElement = () => editorView?.dom ?? null;
864
- // A Table menu item, on the caret's table (the menu gives focus back).
865
- const runTable = (action) => {
866
- if (editorView)
867
- runTableAction(editorView.state, editorView.dispatch, action);
884
+ // A grip's menu item, on the cell it was opened for: the caret into
885
+ // that cell, the action, and focus back in the text.
886
+ const caretInto = (getPos, cell) => {
887
+ if (!editorView)
888
+ return false;
889
+ const pos = getPos();
890
+ const table = pos === undefined ? null : editorView.state.doc.nodeAt(pos);
891
+ if (pos === undefined || table?.type.name !== 'table')
892
+ return false;
893
+ const map = TableMap.get(table);
894
+ if (cell.row >= map.height || cell.column >= map.width)
895
+ return false;
896
+ const at = pos + 1 + map.map[cell.row * map.width + cell.column] + 1;
897
+ editorView.dispatch(editorView.state.tr.setSelection(TextSelection.near(editorView.state.doc.resolve(at))));
898
+ return true;
899
+ };
900
+ const runTableAt = (getPos, cell, action) => {
901
+ if (!editorView || !caretInto(getPos, cell))
902
+ return;
903
+ runTableAction(editorView.state, editorView.dispatch, action);
904
+ editorView.focus();
905
+ };
906
+ const colorTableAt = (getPos, cell, scope, color) => {
907
+ if (!editorView || !caretInto(getPos, cell))
908
+ return;
909
+ colorTableCells(editorView.state, editorView.dispatch, scope, color);
910
+ editorView.focus();
868
911
  };
869
912
  // A callout's menu: its kind changed, or the callout taken away (its
870
913
  // blocks kept). The menu gives focus back to the text.
@@ -946,14 +989,14 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
946
989
  setLinkBox(null);
947
990
  editorRef.current?.action((ctx) => ctx.get(editorViewCtx).focus());
948
991
  };
949
- return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: setField, "data-slot": "rich-text-editor", "data-full-screen": fullScreen || undefined, onKeyDown: leaveFullScreen, className: cn(fullScreen ? 'fixed inset-0 z-50 h-dvh rounded-none! border-0! bg-background! ring-0!' : 'relative', 'flex w-full min-w-0 flex-col bg-transparent type-body text-foreground break-words',
992
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: setField, "data-slot": "rich-text-editor", "data-table-look": tableLook, "data-full-screen": fullScreen || undefined, onKeyDown: leaveFullScreen, className: cn(fullScreen ? 'fixed inset-0 z-50 h-dvh rounded-none! border-0! bg-background! ring-0!' : 'relative', 'flex w-full min-w-0 flex-col bg-transparent type-body text-foreground break-words',
950
993
  // Inline, it's the text itself: the caret shows where it's
951
994
  // edited, and a refusal's reason under it says what's wrong.
952
995
  !inline &&
953
996
  'rounded-lg border border-input transition-[border-color,box-shadow] focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 dark:bg-input/30', !inline && 'has-[[aria-invalid=true]]:border-destructive has-[[aria-invalid=true]]:ring-destructive/20', className), children: [fixedToolbar && (_jsx(RichTextToolbar, { ref: toolbarRef, state: editorState, readOnly: readOnly,
954
997
  // Only once the editor is made: until then the editable element,
955
998
  // and its id, aren't in the page (axe: aria-valid-attr-value).
956
- controls: editorState ? editableId : undefined, onFormat: runFormat, onTable: runTable, onImage: upload ? pickImages : undefined, onFullScreen: toggleFullScreen, fullScreen: fullScreen, text: textElement })), upload && (_jsx("input", { ref: imageInputRef, type: "file", accept: acceptAttribute(imageAccept), multiple: true, hidden: true, tabIndex: -1, onChange: (event) => {
999
+ controls: editorState ? editableId : undefined, onFormat: runFormat, onImage: upload ? pickImages : undefined, onFullScreen: toggleFullScreen, fullScreen: fullScreen, text: textElement })), upload && (_jsx("input", { ref: imageInputRef, type: "file", accept: acceptAttribute(imageAccept), multiple: true, hidden: true, tabIndex: -1, onChange: (event) => {
957
1000
  const files = Array.from(event.target.files ?? []);
958
1001
  // Cleared, so picking the same file again still counts.
959
1002
  event.target.value = '';
@@ -981,10 +1024,6 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
981
1024
  // (ProseMirror's own separator images, around an image alone on
982
1025
  // its line, are left alone.)
983
1026
  '[&_img[src]]:inline-block [&_img[src]]:h-auto [&_img[src]]:max-w-full [&_img[src]]:rounded-md [&_img[src]]:align-bottom', '[&_img.ProseMirror-selectednode]:outline-2 [&_img.ProseMirror-selectednode]:outline-offset-2 [&_img.ProseMirror-selectednode]:outline-ring',
984
- // A table: a header row on the muted fill, a line between every
985
- // cell, cells that wrap and never shrink to nothing; selected
986
- // cells (a drag across them) tinted.
987
- '[&_table]:w-full [&_table]:border-collapse', '[&_:is(td,th)]:min-w-28 [&_:is(td,th)]:border-r [&_:is(td,th)]:border-b [&_:is(td,th)]:border-border [&_:is(td,th)]:px-(--table-cell-px) [&_:is(td,th)]:py-(--table-cell-py) [&_:is(td,th)]:align-top', '[&_:is(td,th):last-child]:border-r-0 [&_tr:last-child>td]:border-b-0', '[&_th]:bg-muted [&_th]:text-left [&_th]:type-label', '[&_.selectedCell]:bg-info/15',
988
1027
  // Raw HTML from the text: shown as its characters, never as markup.
989
1028
  '[&_span[data-type=html]]:rounded [&_span[data-type=html]]:bg-muted [&_span[data-type=html]]:px-1 [&_span[data-type=html]]:font-mono [&_span[data-type=html]]:text-foreground',
990
1029
  // The placeholder, in an empty document only (a decoration).
@@ -995,6 +1034,8 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
995
1034
  return [
996
1035
  createPortal(_jsx(TableBar, { side: "row", active: active, readOnly: readOnly, onAdd: () => addToTable(widget.getPos, 'row') }), widget.slot, `${widget.key}-row`),
997
1036
  createPortal(_jsx(TableBar, { side: "column", active: active, readOnly: readOnly, onAdd: () => addToTable(widget.getPos, 'column') }), widget.column, `${widget.key}-column`),
1037
+ editorView &&
1038
+ createPortal(_jsx(TableGrips, { view: editorView, state: editorState, getPos: widget.getPos, frame: widget.frame, box: widget.box, table: widget.table, readOnly: readOnly, focused: focused, onAction: (cell, action) => runTableAt(widget.getPos, cell, action), onColor: (cell, scope, color) => colorTableAt(widget.getPos, cell, scope, color), text: textElement }), widget.grips, `${widget.key}-grips`),
998
1039
  ];
999
1040
  }), slashOpen && slash && editorView && (_jsx(RichTextSlashMenu, { id: slashListId, view: editorView, at: slash.to, field: field, items: slashMatches, active: slashIndex, onHighlight: (index) => setSlashActive({ key: slashMenuKey, index }), onPick: (format) => applySlash(editorView, slash, format) })), toolsOpen && (_jsx(RichTextToolsBar, { ref: toolsRef, state: editorState, readOnly: readOnly, fullScreen: fullScreen, controls: editableId, onFormat: runFormat, onFullScreen: toggleFullScreen, onReturn: returnToText, onBlur: (event) => {
1000
1041
  const next = event.relatedTarget;
@@ -113,7 +113,7 @@ export function ModuleCard({ title, icon: Icon, category, to, href, value, note,
113
113
  'data-slot': 'module-card-band',
114
114
  className: cn(BAND_LINK_CLASS, category && BAND_CLASS[category]),
115
115
  };
116
- const card = (_jsxs(Card, { "data-slot": "module-card", "data-state": state, className: cn('relative h-full min-w-0 gap-0 py-0', mobile === 'row' && 'max-md:hidden', className), children: [href !== undefined ? (_jsxs("a", { href: href, target: "_blank", rel: "noreferrer", ...bandProps, children: [band, _jsx(NewTabMark, {})] })) : (_jsx(Link, { to: to, ...bandProps, children: band })), _jsxs("div", { id: bodyId, "data-slot": "module-card-body", "aria-busy": state === 'loading' || undefined, className: "flex flex-1 flex-col gap-0.5 px-3 py-2 density-compact:py-1.5 max-sm:px-2", children: [state === 'error' ? (_jsxs(_Fragment, { children: [_jsxs("p", { className: "flex items-center gap-1.5 type-body text-foreground", children: [_jsx(AlertCircleIcon, { "aria-hidden": "true", className: "size-4 shrink-0 text-destructive-text" }), _jsxs("span", { className: "min-w-0", children: ["Couldn't load", error?.message && _jsxs("span", { className: "sr-only", children: [": ", error.message] })] })] }), onRetry && (_jsx(Button, { size: "sm", variant: "outline", onClick: onRetry, "aria-describedby": titleId, className: "relative z-10 mt-1 self-start", children: "Retry" }))] })) : state === 'loading' ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "sr-only", children: "Loading" }), _jsx(Skeleton, { "data-state": "loading", className: "my-0.5 h-6 w-12" })] })) : (value !== undefined && (_jsx("span", { "data-slot": "module-card-value", className: "type-numeric text-foreground", children: value }))), note && state !== 'error' && (_jsx("span", { className: "truncate type-body text-muted-foreground", title: note, children: note }))] }), action &&
116
+ const card = (_jsxs(Card, { "data-slot": "module-card", "data-state": state, className: cn('glow-edge relative h-full min-w-0 gap-0 py-0', mobile === 'row' && 'max-md:hidden', className), children: [href !== undefined ? (_jsxs("a", { href: href, target: "_blank", rel: "noreferrer", ...bandProps, children: [band, _jsx(NewTabMark, {})] })) : (_jsx(Link, { to: to, ...bandProps, children: band })), _jsxs("div", { id: bodyId, "data-slot": "module-card-body", "aria-busy": state === 'loading' || undefined, className: "flex flex-1 flex-col gap-0.5 px-3 py-2 density-compact:py-1.5 max-sm:px-2", children: [state === 'error' ? (_jsxs(_Fragment, { children: [_jsxs("p", { className: "flex items-center gap-1.5 type-body text-foreground", children: [_jsx(AlertCircleIcon, { "aria-hidden": "true", className: "size-4 shrink-0 text-destructive-text" }), _jsxs("span", { className: "min-w-0", children: ["Couldn't load", error?.message && _jsxs("span", { className: "sr-only", children: [": ", error.message] })] })] }), onRetry && (_jsx(Button, { size: "sm", variant: "outline", onClick: onRetry, "aria-describedby": titleId, className: "relative z-10 mt-1 self-start", children: "Retry" }))] })) : state === 'loading' ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "sr-only", children: "Loading" }), _jsx(Skeleton, { "data-state": "loading", className: "my-0.5 h-6 w-12" })] })) : (value !== undefined && (_jsx("span", { "data-slot": "module-card-value", className: "type-numeric text-foreground", children: value }))), note && state !== 'error' && (_jsx("span", { className: "truncate type-body text-muted-foreground", title: note, children: note }))] }), action &&
117
117
  (action.href !== undefined ? (_jsxs("a", { href: action.href, target: "_blank", rel: "noreferrer", "aria-describedby": titleId, className: ACTION_CLASS, children: [_jsx("span", { className: "truncate", children: action.label }), _jsx(NewTabMark, {})] })) : (_jsx(Link, { to: action.to, "aria-describedby": titleId, className: ACTION_CLASS, children: _jsx("span", { className: "truncate", children: action.label }) })))] }));
118
118
  if (mobile === 'tiles')
119
119
  return card;
@@ -0,0 +1,14 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type PaletteName, type PaletteStore } from '../../lib/palettes.js';
3
+ /**
4
+ * Puts the palette on <html>. FoundationProviders renders it; an app
5
+ * doesn't. Without `palette` it does nothing, and the app keeps light and
6
+ * dark.
7
+ */
8
+ export declare function PaletteProvider({ palette: defaultPalette, picker, store, children, }: {
9
+ palette?: PaletteName;
10
+ picker?: boolean;
11
+ /** For tests; the browser's localStorage by default. */
12
+ store?: PaletteStore;
13
+ children: ReactNode;
14
+ }): import("react").JSX.Element;
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useLayoutEffect, useMemo, useSyncExternalStore } from 'react';
3
+ import { PaletteContext } from '../../hooks/use-palette.js';
4
+ import { browserPaletteStore, subscribeToBrowserPaletteStore } from '../../lib/palettes.js';
5
+ /**
6
+ * Puts the palette on <html>. FoundationProviders renders it; an app
7
+ * doesn't. Without `palette` it does nothing, and the app keeps light and
8
+ * dark.
9
+ */
10
+ export function PaletteProvider({ palette: defaultPalette, picker = false, store = browserPaletteStore, children, }) {
11
+ const subscribe = useCallback((listener) => (store === browserPaletteStore ? subscribeToBrowserPaletteStore(listener) : store.subscribe(listener)), [store]);
12
+ const saved = useSyncExternalStore(subscribe, store.get, () => null);
13
+ const palette = defaultPalette ? ((picker && saved) || defaultPalette) : null;
14
+ // Before paint, so the first frame is already in the palette.
15
+ useLayoutEffect(() => {
16
+ const root = document.documentElement;
17
+ if (palette)
18
+ root.dataset.palette = palette;
19
+ else
20
+ delete root.dataset.palette;
21
+ return () => {
22
+ delete root.dataset.palette;
23
+ };
24
+ }, [palette]);
25
+ const value = useMemo(() => ({
26
+ palette,
27
+ defaultPalette: defaultPalette ?? null,
28
+ picker: Boolean(defaultPalette && picker),
29
+ setPalette: (next) => {
30
+ if (defaultPalette && picker)
31
+ store.set(next);
32
+ },
33
+ }), [palette, defaultPalette, picker, store]);
34
+ return _jsx(PaletteContext.Provider, { value: value, children: children });
35
+ }
@@ -84,3 +84,10 @@ export type CalloutLook = {
84
84
  export declare const CALLOUT_LOOK: Record<CalloutKind, CalloutLook>;
85
85
  /** The box every callout shares: the icon in a column, the text beside it. */
86
86
  export declare const CALLOUT_BOX = "flex gap-2.5 rounded-lg border px-3 py-2.5";
87
+ /**
88
+ * How a table in written text looks (styles/index.css "Tables in written
89
+ * text"): `compact`, a full grid with tight cells and the header row on
90
+ * the heading band, or `boxed`, a rounded box, roomier cells, a muted
91
+ * header row and zebra rows.
92
+ */
93
+ export type TableLook = 'compact' | 'boxed';
@@ -1,5 +1,5 @@
1
1
  import type { ImageUpload } from '../../lib/image-file.js';
2
- import type { HeadingFrom, HeadingLevel } from './prose-look.js';
2
+ import type { HeadingFrom, HeadingLevel, TableLook } from './prose-look.js';
3
3
  export type RichTextEditorProps = {
4
4
  /** The editable element's id, for a label's `htmlFor` or a test. */
5
5
  id?: string;
@@ -90,6 +90,13 @@ export type RichTextEditorProps = {
90
90
  * Default `true`.
91
91
  */
92
92
  foldable?: boolean;
93
+ /**
94
+ * How tables look: `'compact'` (the default), a full grid of hairlines
95
+ * with tight cells and the header row on the heading band, or
96
+ * `'boxed'`, a rounded box with roomier cells, a muted header row and
97
+ * zebra rows. The same on <Markdown>. Either way columns fit their words.
98
+ */
99
+ tableLook?: TableLook;
93
100
  className?: string;
94
101
  /** The field's label. A rich-text area must have one: this, or `aria-labelledby`. */
95
102
  'aria-label'?: string;
@@ -23,6 +23,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
23
23
  // Image button is uploaded through the app's own storage and goes in as
24
24
  // `![](url)`; while it uploads, a placeholder shows where it will go.
25
25
  //
26
+ // A table's columns fit their words. Grips on its column and row (or a
27
+ // right-click, or Shift+F10, in a cell) open menus to insert, move,
28
+ // delete, align, sort and colour; a header column and cell colours are
29
+ // kept as Markdown already writes them (src/lib/markdown-table.ts).
30
+ // `tableLook` picks compact (the default) or boxed, as on <Markdown>.
31
+ //
26
32
  // With `foldKey`, the text folds by its headings (NotesOutline lists them
27
33
  // beside it), the Markdown untouched.
28
34
  //
@@ -37,7 +37,7 @@ export const FORMAT_GROUPS = [
37
37
  { id: 'codeBlock', label: 'Code block', icon: SquareCodeIcon, keys: 'Block+8' },
38
38
  { id: 'divider', label: 'Divider', icon: MinusIcon, hint: 'or type ---' },
39
39
  ],
40
- [{ id: 'table', label: 'Table', icon: TableIcon }],
40
+ [{ id: 'table', label: 'Table', icon: TableIcon, hint: 'its grips edit it', action: true }],
41
41
  [{ id: 'link', label: 'Link', icon: LinkIcon, keys: 'Mod+K' }],
42
42
  ];
43
43
  /** Every format by its id. */
@@ -13,6 +13,11 @@ export type Widget = {
13
13
  key: string;
14
14
  slot: HTMLElement;
15
15
  column: HTMLElement;
16
+ /** The layer the grips are placed in (rich-text-table-grips.tsx), over the whole frame. */
17
+ grips: HTMLElement;
18
+ frame: HTMLElement;
19
+ box: HTMLElement;
20
+ table: HTMLTableElement;
16
21
  getPos: () => number | undefined;
17
22
  };
18
23
  /** Where node views tell the editor their slots came, changed and went. */
@@ -24,9 +29,10 @@ export declare function calloutKind(value: unknown): CalloutKind;
24
29
  /** A callout: its kind's box, its icon (a widget: the kind's menu) beside its blocks. */
25
30
  export declare function calloutView(node: ProseNode, getPos: () => number | undefined, registry: WidgetRegistry): NodeView;
26
31
  /**
27
- * A table: in a box that scrolls sideways when it's wider than the text,
28
- * with an "add a column" bar along its right and an "add a row" bar under
29
- * it (widgets), Notion's way. While it scrolls, the box is a tab stop and
32
+ * A table: in a box as wide as its columns' words that scrolls sideways
33
+ * when it's wider than the text, with an "add a column" bar along its
34
+ * right, an "add a row" bar under it and a layer over it for its grips
35
+ * (widgets), Notion's way. While it scrolls, the box is a tab stop and
30
36
  * a region named for its columns (prose-table-scroll.ts), which the arrow
31
37
  * keys, Home and End scroll; a click in it still puts the caret in the
32
38
  * cell clicked.
@@ -86,9 +86,10 @@ function scrollKey(box, event) {
86
86
  // starts, or this long for one that never clicks (a touch that pans).
87
87
  const PRESS_MS = 1000;
88
88
  /**
89
- * A table: in a box that scrolls sideways when it's wider than the text,
90
- * with an "add a column" bar along its right and an "add a row" bar under
91
- * it (widgets), Notion's way. While it scrolls, the box is a tab stop and
89
+ * A table: in a box as wide as its columns' words that scrolls sideways
90
+ * when it's wider than the text, with an "add a column" bar along its
91
+ * right, an "add a row" bar under it and a layer over it for its grips
92
+ * (widgets), Notion's way. While it scrolls, the box is a tab stop and
92
93
  * a region named for its columns (prose-table-scroll.ts), which the arrow
93
94
  * keys, Home and End scroll; a click in it still puts the caret in the
94
95
  * cell clicked.
@@ -96,17 +97,20 @@ const PRESS_MS = 1000;
96
97
  export function tableView(node, view, getPos, registry) {
97
98
  const dom = document.createElement('div');
98
99
  dom.dataset.slot = 'rich-text-table';
99
- dom.className = 'group/table grid grid-cols-[minmax(0,1fr)_auto] gap-1';
100
+ // As wide as the table (its columns fit their words), never wider than the text.
101
+ dom.className = 'group/table relative grid w-fit max-w-full grid-cols-[minmax(0,auto)_auto] gap-1';
100
102
  const scroll = document.createElement('div');
101
- scroll.className = cn('overflow-x-auto rounded-lg border border-border', TABLE_SCROLL_FOCUS);
103
+ // Its look: styles/index.css "Tables in written text".
104
+ scroll.className = cn('prose-table', TABLE_SCROLL_FOCUS);
102
105
  const table = document.createElement('table');
103
106
  const contentDOM = document.createElement('tbody');
104
107
  table.append(contentDOM);
105
108
  scroll.append(table);
106
109
  const column = slot('flex w-5');
107
110
  const row = slot('col-start-1 flex h-5');
108
- dom.append(scroll, column, row);
109
- registry.set({ type: 'table', key: nextKey(), slot: row, column, getPos });
111
+ const grips = slot('pointer-events-none absolute inset-0');
112
+ dom.append(scroll, column, row, grips);
113
+ registry.set({ type: 'table', key: nextKey(), slot: row, column, grips, frame: dom, box: scroll, table, getPos });
110
114
  const region = watchTableScroll(scroll, table, columnsOf(node));
111
115
  // A press in the box is the text's: a click in a cell puts the caret
112
116
  // there. The browser focuses the nearest focusable ancestor of what was
@@ -167,7 +171,7 @@ export function tableView(node, view, getPos, registry) {
167
171
  region.setColumns(columnsOf(next));
168
172
  return true;
169
173
  },
170
- stopEvent: (event) => row.contains(event.target) || column.contains(event.target),
174
+ stopEvent: (event) => row.contains(event.target) || column.contains(event.target) || grips.contains(event.target),
171
175
  ignoreMutation: outside(contentDOM),
172
176
  destroy: () => {
173
177
  released();
@@ -0,0 +1,31 @@
1
+ import { type EditorState } from '@milkdown/kit/prose/state';
2
+ import type { EditorView } from '@milkdown/kit/prose/view';
3
+ import { type HighlightColor } from '../../lib/markdown-highlight.js';
4
+ import { type TableAction, type TableColorScope } from './rich-text-table.js';
5
+ /** A cell by its row (0 is the header row) and column. */
6
+ export type TableCell = {
7
+ row: number;
8
+ column: number;
9
+ };
10
+ /** The event the editor sends a table's frame to open the caret's cell menu (Shift+F10). */
11
+ export declare const TABLE_MENU_EVENT = "rich-text-table-menu";
12
+ export interface TableGripsProps {
13
+ view: EditorView;
14
+ /** The editor's state, so the grips follow each change. */
15
+ state: EditorState | null;
16
+ getPos: () => number | undefined;
17
+ /** The node view's frame (the grips' layer is positioned in it), its scroll box and its table. */
18
+ frame: HTMLElement;
19
+ box: HTMLElement;
20
+ table: HTMLTableElement;
21
+ readOnly: boolean;
22
+ /** Whether focus is in the field: the caret's cell has grips only then. */
23
+ focused: boolean;
24
+ /** Runs a table action on a cell (the caret put there first), focus back in the text. */
25
+ onAction: (cell: TableCell, action: TableAction) => void;
26
+ /** Colours a cell, its row or its column (null: no colour). */
27
+ onColor: (cell: TableCell, scope: TableColorScope, color: HighlightColor | null) => void;
28
+ /** The editable element, where focus goes when a menu closes. */
29
+ text: () => HTMLElement | null;
30
+ }
31
+ export declare function TableGrips({ view, state, getPos, frame, box, table, readOnly, focused, onAction, onColor, text }: TableGripsProps): import("react").JSX.Element | null;
@@ -0,0 +1,216 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // A table's grips in the rich-text editor, Notion's way: while the pointer
3
+ // is over a cell, or the caret is in one, a grip sits on the top edge of
4
+ // its column and another on the left edge of its row. Each opens that
5
+ // column's or that row's menu: insert, move and delete, the column's
6
+ // alignment and sort, the header column, and colours (rich-text-table.ts).
7
+ // A right-click in a cell, or Shift+F10 (or the menu key) with the caret
8
+ // in one, opens the cell's menu: its colour, then its row's and its
9
+ // column's items. The grips are mouse shortcuts, out of the tab order;
10
+ // the keyboard has the cell's menu.
11
+ //
12
+ // Rendered into the table node view's grip layer (rich-text-node-views.ts)
13
+ // through a portal. A menu acts on the cell it was opened for, then gives
14
+ // focus back to the text with the caret in that cell.
15
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
16
+ import { TextSelection } from '@milkdown/kit/prose/state';
17
+ import { selectedRect, TableMap } from '@milkdown/kit/prose/tables';
18
+ import { AlignCenterIcon, AlignLeftIcon, AlignRightIcon, ArrowDownAZIcon, ArrowDownIcon, ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, ArrowUpZAIcon, BetweenHorizontalEndIcon, BetweenHorizontalStartIcon, BetweenVerticalEndIcon, BetweenVerticalStartIcon, GripHorizontalIcon, GripVerticalIcon, PanelLeftIcon, Trash2Icon, } from 'lucide-react';
19
+ import { cn } from 'cn';
20
+ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '../ui/dropdown-menu.js';
21
+ import { HIGHLIGHT_COLORS } from '../../lib/markdown-highlight.js';
22
+ import { tableAt } from './rich-text-formats.js';
23
+ import { cellColorAt, columnAlignment, headerColumnOn, tableActionEnabled, tableColorEnabled, } from './rich-text-table.js';
24
+ /** The event the editor sends a table's frame to open the caret's cell menu (Shift+F10). */
25
+ export const TABLE_MENU_EVENT = 'rich-text-table-menu';
26
+ const SWATCH = {
27
+ amber: 'bg-highlight-amber',
28
+ yellow: 'bg-highlight-yellow',
29
+ green: 'bg-highlight-green',
30
+ red: 'bg-highlight-red',
31
+ };
32
+ const ROW_ITEMS = [
33
+ { action: 'rowAbove', label: 'Insert row above', icon: BetweenHorizontalStartIcon },
34
+ { action: 'rowBelow', label: 'Insert row below', icon: BetweenHorizontalEndIcon },
35
+ { action: 'moveRowUp', label: 'Move row up', icon: ArrowUpIcon },
36
+ { action: 'moveRowDown', label: 'Move row down', icon: ArrowDownIcon },
37
+ { action: 'deleteRow', label: 'Delete row', icon: Trash2Icon },
38
+ ];
39
+ const COLUMN_ITEMS = [
40
+ { action: 'columnLeft', label: 'Insert column left', icon: BetweenVerticalStartIcon },
41
+ { action: 'columnRight', label: 'Insert column right', icon: BetweenVerticalEndIcon },
42
+ { action: 'moveColumnLeft', label: 'Move column left', icon: ArrowLeftIcon },
43
+ { action: 'moveColumnRight', label: 'Move column right', icon: ArrowRightIcon },
44
+ { action: 'deleteColumn', label: 'Delete column', icon: Trash2Icon },
45
+ ];
46
+ const SORT_ITEMS = [
47
+ { action: 'sortAscending', label: 'Sort ascending', icon: ArrowDownAZIcon },
48
+ { action: 'sortDescending', label: 'Sort descending', icon: ArrowUpZAIcon },
49
+ ];
50
+ const ALIGNMENTS = [
51
+ { value: 'left', action: 'alignLeft', label: 'Left', icon: AlignLeftIcon },
52
+ { value: 'center', action: 'alignCenter', label: 'Center', icon: AlignCenterIcon },
53
+ { value: 'right', action: 'alignRight', label: 'Right', icon: AlignRightIcon },
54
+ ];
55
+ // The cell an element is in, if it's one of this table's.
56
+ function cellOf(table, target) {
57
+ const cell = target instanceof Element ? target.closest('td, th') : null;
58
+ if (!(cell instanceof HTMLTableCellElement) || cell.closest('table') !== table)
59
+ return null;
60
+ return { row: cell.parentElement.rowIndex, column: cell.cellIndex };
61
+ }
62
+ // The state with the caret put in a cell, to ask what applies there.
63
+ function stateAt(state, tablePos, cell) {
64
+ const table = state.doc.nodeAt(tablePos);
65
+ if (!table || table.type.name !== 'table')
66
+ return null;
67
+ const map = TableMap.get(table);
68
+ if (cell.row >= map.height || cell.column >= map.width)
69
+ return null;
70
+ const at = tablePos + 1 + map.map[cell.row * map.width + cell.column] + 1;
71
+ return state.apply(state.tr.setSelection(TextSelection.near(state.doc.resolve(at))));
72
+ }
73
+ export function TableGrips({ view, state, getPos, frame, box, table, readOnly, focused, onAction, onColor, text }) {
74
+ const [hover, setHover] = useState(null);
75
+ const [open, setOpen] = useState(null);
76
+ const [target, setTarget] = useState(null);
77
+ // Bumped when the layout moves without a new state (scroll, resize).
78
+ const [, setLayout] = useState(0);
79
+ const columnGrip = useRef(null);
80
+ const rowGrip = useRef(null);
81
+ const tablePos = getPos();
82
+ // The caret's cell, when it's in this table.
83
+ let caret = null;
84
+ if (focused && state && tablePos !== undefined && tableAt(state)?.pos === tablePos) {
85
+ const { top, left } = selectedRect(state);
86
+ caret = { row: top, column: left };
87
+ }
88
+ // The caret's cell for the keyboard's menu, read by a listener set up once.
89
+ const caretRef = useRef(caret);
90
+ useLayoutEffect(() => {
91
+ caretRef.current = caret;
92
+ });
93
+ const active = open ? target : (hover ?? caret);
94
+ // Pointer over a cell: its grips. Leaving the table, back to the caret's.
95
+ useEffect(() => {
96
+ const onMove = (event) => {
97
+ const cell = cellOf(table, event.target);
98
+ if (cell)
99
+ setHover((now) => (now && now.row === cell.row && now.column === cell.column ? now : cell));
100
+ };
101
+ const onLeave = () => setHover(null);
102
+ const onLayout = () => setLayout((n) => n + 1);
103
+ const onMenu = () => {
104
+ const now = caretRef.current;
105
+ if (!now)
106
+ return;
107
+ setTarget(now);
108
+ setOpen('cell');
109
+ };
110
+ const onContext = (event) => {
111
+ const cell = cellOf(table, event.target);
112
+ if (!cell || !view.editable)
113
+ return;
114
+ event.preventDefault();
115
+ setTarget(cell);
116
+ setOpen('cell');
117
+ };
118
+ frame.addEventListener('pointermove', onMove);
119
+ frame.addEventListener('pointerleave', onLeave);
120
+ frame.addEventListener(TABLE_MENU_EVENT, onMenu);
121
+ frame.addEventListener('contextmenu', onContext);
122
+ box.addEventListener('scroll', onLayout);
123
+ window.addEventListener('resize', onLayout);
124
+ const sizes = new ResizeObserver(onLayout);
125
+ sizes.observe(table);
126
+ return () => {
127
+ frame.removeEventListener('pointermove', onMove);
128
+ frame.removeEventListener('pointerleave', onLeave);
129
+ frame.removeEventListener(TABLE_MENU_EVENT, onMenu);
130
+ frame.removeEventListener('contextmenu', onContext);
131
+ box.removeEventListener('scroll', onLayout);
132
+ window.removeEventListener('resize', onLayout);
133
+ sizes.disconnect();
134
+ };
135
+ }, [frame, box, table, view, caretRef]);
136
+ // Where the grips go: the column's on the middle of its top edge (the
137
+ // header cell's), the row's on the middle of its left edge. A column
138
+ // scrolled out of the box has none. Placed on the elements themselves,
139
+ // after each render (the layout is the browser's, not React's).
140
+ useLayoutEffect(() => {
141
+ const columnButton = columnGrip.current;
142
+ const rowButton = rowGrip.current;
143
+ if (!columnButton || !rowButton)
144
+ return;
145
+ const row = active ? table.rows[active.row] : undefined;
146
+ const header = active ? table.rows[0]?.cells[active.column] : undefined;
147
+ if (!active || !row || !header) {
148
+ columnButton.hidden = true;
149
+ rowButton.hidden = true;
150
+ return;
151
+ }
152
+ const origin = frame.getBoundingClientRect();
153
+ const scroll = box.getBoundingClientRect();
154
+ const head = header.getBoundingClientRect();
155
+ const line = row.getBoundingClientRect();
156
+ const middle = head.left + head.width / 2;
157
+ columnButton.hidden = middle < scroll.left || middle > scroll.right;
158
+ columnButton.style.left = `${middle - origin.left}px`;
159
+ columnButton.style.top = `${head.top - origin.top}px`;
160
+ rowButton.hidden = false;
161
+ rowButton.style.left = `${scroll.left - origin.left}px`;
162
+ rowButton.style.top = `${line.top - origin.top + line.height / 2}px`;
163
+ });
164
+ if (readOnly || tablePos === undefined || !state)
165
+ return null;
166
+ const at = (cell) => (cell ? stateAt(state, tablePos, cell) : null);
167
+ const menuCell = open ? target : active;
168
+ const there = at(menuCell);
169
+ const enabled = (action) => (there ? tableActionEnabled(there, action) : false);
170
+ const columnName = (column) => table.rows[0]?.cells[column]?.textContent?.trim() || `column ${column + 1}`;
171
+ const openMenu = (menu) => (next) => {
172
+ if (next) {
173
+ setTarget(active);
174
+ setOpen(menu);
175
+ }
176
+ else
177
+ setOpen(null);
178
+ };
179
+ // An item runs once its menu has closed: an action that rebuilds the
180
+ // table (a move, a sort) remakes its node view, grips and menu with it,
181
+ // and focus then goes to the text, not to a menu that's gone.
182
+ const later = (act) => {
183
+ setOpen(null);
184
+ setTimeout(act);
185
+ };
186
+ const run = (action) => {
187
+ const cell = menuCell;
188
+ if (cell)
189
+ later(() => onAction(cell, action));
190
+ };
191
+ const color = (scope, value) => {
192
+ const cell = menuCell;
193
+ if (cell)
194
+ later(() => onColor(cell, scope, value));
195
+ };
196
+ const items = (list) => list.map(({ action, label, icon: Icon }) => (_jsxs(DropdownMenuItem, { disabled: !enabled(action), onClick: () => run(action), children: [_jsx(Icon, { "aria-hidden": "true" }), label] }, action)));
197
+ const colorGroup = (scope, label) => {
198
+ if (!there || !tableColorEnabled(there, scope))
199
+ return null;
200
+ const current = scope === 'cell' ? cellColorAt(there) : null;
201
+ return (_jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: label }), _jsxs(DropdownMenuRadioGroup
202
+ // A cell says its colour; a row's or a column's cells may differ.
203
+ , {
204
+ // A cell says its colour; a row's or a column's cells may differ.
205
+ value: scope === 'cell' ? (current ?? '') : null, onValueChange: (value) => color(scope, HIGHLIGHT_COLORS.includes(value) ? value : null), children: [HIGHLIGHT_COLORS.map((one) => (_jsxs(DropdownMenuRadioItem, { value: one, closeOnClick: true, children: [_jsx("span", { "aria-hidden": "true", className: cn('size-4 shrink-0 rounded-sm border border-foreground/30', SWATCH[one]) }), one[0].toUpperCase() + one.slice(1)] }, one))), _jsxs(DropdownMenuRadioItem, { value: "", closeOnClick: true, children: [_jsx("span", { "aria-hidden": "true", className: "size-4 shrink-0 rounded-sm border border-foreground/30" }), "No colour"] })] })] }));
206
+ };
207
+ const columnItems = (withColor, label) => (_jsxs(_Fragment, { children: [_jsxs(DropdownMenuGroup, { children: [label && _jsx(DropdownMenuLabel, { children: label }), items(COLUMN_ITEMS)] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Align column" }), _jsx(DropdownMenuRadioGroup, { value: there ? columnAlignment(there) : 'left', onValueChange: (value) => {
208
+ const one = ALIGNMENTS.find((alignment) => alignment.value === value);
209
+ if (one)
210
+ run(one.action);
211
+ }, children: ALIGNMENTS.map(({ value, label, icon: Icon }) => (_jsxs(DropdownMenuRadioItem, { value: value, closeOnClick: true, children: [_jsx(Icon, { "aria-hidden": "true" }), label] }, value))) })] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuGroup, { children: items(SORT_ITEMS) }), menuCell?.column === 0 && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuCheckboxItem, { checked: there ? headerColumnOn(there) : false, disabled: !enabled('headerColumn'), onCheckedChange: () => run('headerColumn'), closeOnClick: true, children: [_jsx(PanelLeftIcon, { "aria-hidden": "true" }), "Header column"] })] })), withColor && there && tableColorEnabled(there, 'column') && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), colorGroup('column', 'Colour column')] })), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: () => run('deleteTable'), children: [_jsx(Trash2Icon, { "aria-hidden": "true" }), "Delete table"] })] }));
212
+ const rowColor = there && tableColorEnabled(there, 'row');
213
+ const finalFocus = () => text() ?? true;
214
+ const grip = 'pointer-events-auto absolute flex items-center justify-center rounded-sm border border-border bg-muted text-muted-foreground shadow-xs outline-none hover:bg-primary hover:text-primary-foreground data-popup-open:bg-primary data-popup-open:text-primary-foreground';
215
+ return (_jsxs(_Fragment, { children: [_jsxs(DropdownMenu, { open: open === 'column' || open === 'cell', onOpenChange: openMenu('column'), children: [_jsx(DropdownMenuTrigger, { tabIndex: -1, "aria-label": menuCell ? `Column: ${columnName(menuCell.column)}` : 'Column', ref: columnGrip, "data-slot": "rich-text-table-column-grip", className: cn(grip, 'h-3.5 w-6 -translate-x-1/2 -translate-y-1/2'), children: _jsx(GripHorizontalIcon, { "aria-hidden": "true", className: "size-3" }) }), _jsx(DropdownMenuContent, { className: "w-56", "data-rich-text-menu": "", finalFocus: finalFocus, children: open === 'cell' ? (_jsxs(_Fragment, { children: [menuCell && menuCell.row > 0 && (_jsxs(_Fragment, { children: [colorGroup('cell', 'Cell colour'), _jsx(DropdownMenuSeparator, {})] })), _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Row" }), items(ROW_ITEMS)] }), _jsx(DropdownMenuSeparator, {}), columnItems(false, menuCell ? `Column: ${columnName(menuCell.column)}` : 'Column')] })) : (columnItems(true)) })] }), _jsxs(DropdownMenu, { open: open === 'row', onOpenChange: openMenu('row'), children: [_jsx(DropdownMenuTrigger, { tabIndex: -1, "aria-label": menuCell ? (menuCell.row === 0 ? 'Header row' : `Row ${menuCell.row}`) : 'Row', ref: rowGrip, "data-slot": "rich-text-table-row-grip", className: cn(grip, 'h-6 w-3.5 -translate-x-1/2 -translate-y-1/2'), children: _jsx(GripVerticalIcon, { "aria-hidden": "true", className: "size-3" }) }), _jsxs(DropdownMenuContent, { className: "w-56", "data-rich-text-menu": "", finalFocus: finalFocus, children: [_jsx(DropdownMenuGroup, { children: items(ROW_ITEMS) }), rowColor && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), colorGroup('row', 'Colour row')] }))] })] })] }));
216
+ }