@tristan2828/ui-foundation 4.61.0 → 4.62.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.
- package/conventions/docs/cell-patterns.md +16 -11
- package/dist/components/app/markdown.d.ts +7 -2
- package/dist/components/app/markdown.js +15 -20
- package/dist/components/app/milkdown-editor.d.ts +1 -1
- package/dist/components/app/milkdown-editor.js +61 -19
- package/dist/components/app/prose-look.d.ts +7 -0
- package/dist/components/app/rich-text-editor.d.ts +13 -2
- package/dist/components/app/rich-text-editor.js +36 -2
- package/dist/components/app/rich-text-format-items.js +1 -1
- package/dist/components/app/rich-text-node-views.d.ts +9 -3
- package/dist/components/app/rich-text-node-views.js +12 -8
- package/dist/components/app/rich-text-table-grips.d.ts +31 -0
- package/dist/components/app/rich-text-table-grips.js +216 -0
- package/dist/components/app/rich-text-table.d.ts +19 -2
- package/dist/components/app/rich-text-table.js +232 -1
- package/dist/components/app/rich-text-toolbar.d.ts +4 -9
- package/dist/components/app/rich-text-toolbar.js +9 -11
- package/dist/components/app/rich-text-widgets.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/lib/markdown-table.d.ts +28 -0
- package/dist/lib/markdown-table.js +106 -0
- package/package.json +1 -1
- package/styles/index.css +92 -0
- package/dist/components/app/rich-text-table-menu.d.ts +0 -20
- package/dist/components/app/rich-text-table-menu.js +0 -53
|
@@ -26,15 +26,15 @@
|
|
|
26
26
|
- [A variant: a pressed icon inside another value's cell](#a-variant-a-pressed-icon-inside-another-values-cell) · lines 565–648
|
|
27
27
|
- [16. Title linking to the record's view](#16-title-linking-to-the-records-view) · lines 650–685
|
|
28
28
|
- [17. Quick action on the view](#17-quick-action-on-the-view) · lines 687–760
|
|
29
|
-
- [18. Editable value](#18-editable-value) · lines 762–
|
|
30
|
-
- [A list of sub-items](#a-list-of-sub-items) · lines
|
|
31
|
-
- [In a table's rows](#in-a-tables-rows) · lines
|
|
32
|
-
- [A table of a few short columns: `width="content"`](#a-table-of-a-few-short-columns-widthcontent) · lines
|
|
33
|
-
- [19. Stage circle for an ordered status](#19-stage-circle-for-an-ordered-status) · lines
|
|
34
|
-
- [20. Count linking to the related records](#20-count-linking-to-the-related-records) · lines
|
|
35
|
-
- [21. Rows grouped under headings, and a pill filter](#21-rows-grouped-under-headings-and-a-pill-filter) · lines
|
|
36
|
-
- [22. A highlighted row, and rows that are done](#22-a-highlighted-row-and-rows-that-are-done) · lines
|
|
37
|
-
- [23. A Group menu, and a record read beside the list](#23-a-group-menu-and-a-record-read-beside-the-list) · lines
|
|
29
|
+
- [18. Editable value](#18-editable-value) · lines 762–1226
|
|
30
|
+
- [A list of sub-items](#a-list-of-sub-items) · lines 1042–1127
|
|
31
|
+
- [In a table's rows](#in-a-tables-rows) · lines 1129–1214
|
|
32
|
+
- [A table of a few short columns: `width="content"`](#a-table-of-a-few-short-columns-widthcontent) · lines 1216–1226
|
|
33
|
+
- [19. Stage circle for an ordered status](#19-stage-circle-for-an-ordered-status) · lines 1228–1294
|
|
34
|
+
- [20. Count linking to the related records](#20-count-linking-to-the-related-records) · lines 1296–1360
|
|
35
|
+
- [21. Rows grouped under headings, and a pill filter](#21-rows-grouped-under-headings-and-a-pill-filter) · lines 1362–1430
|
|
36
|
+
- [22. A highlighted row, and rows that are done](#22-a-highlighted-row-and-rows-that-are-done) · lines 1432–1460
|
|
37
|
+
- [23. A Group menu, and a record read beside the list](#23-a-group-menu-and-a-record-read-beside-the-list) · lines 1462–1538
|
|
38
38
|
|
|
39
39
|
<!-- /index -->
|
|
40
40
|
|
|
@@ -1002,8 +1002,13 @@ const price = editInPlace({
|
|
|
1002
1002
|
is GitHub's `> [!NOTE]` (note, tip, important, warning, caution), its
|
|
1003
1003
|
icon a menu of kinds. In a table, Tab in the last cell adds a row,
|
|
1004
1004
|
Enter goes down a column (and out from the last row), bars under and
|
|
1005
|
-
beside it add a row or a column, and the
|
|
1006
|
-
|
|
1005
|
+
beside it add a row or a column, and a grip on the hovered column and
|
|
1006
|
+
row opens its menu: insert, move and delete, align, sort (rewriting the
|
|
1007
|
+
rows), a header column (its first cells bold) and cell colours (a
|
|
1008
|
+
cell's whole text in a highlight); a right-click or Shift+F10 in a cell
|
|
1009
|
+
opens the cell's. Columns fit their words. `tableLook="boxed"` (on the
|
|
1010
|
+
editor and on `<Markdown>`) gives a roomier boxed table with zebra rows
|
|
1011
|
+
instead of the compact grid. Selected words can be
|
|
1007
1012
|
highlighted in four colours (amber, yellow, green, red) from the
|
|
1008
1013
|
toolbars' Highlight menu or with "/highlight", and un-highlighted;
|
|
1009
1014
|
they're saved as `==words==` (`=={green}words==` for the others), plain
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ComponentProps } from 'react';
|
|
2
|
-
import { type HeadingFrom, type HeadingLevel } from './prose-look.js';
|
|
2
|
+
import { type HeadingFrom, type HeadingLevel, type TableLook } from './prose-look.js';
|
|
3
3
|
export type MarkdownProps = {
|
|
4
4
|
/** The Markdown source. Raw HTML in it is shown as text, never rendered. */
|
|
5
5
|
children: string;
|
|
@@ -44,6 +44,11 @@ export type MarkdownProps = {
|
|
|
44
44
|
* are pictures and the text stays read-only.
|
|
45
45
|
*/
|
|
46
46
|
onTasksChange?: (markdown: string) => void | Promise<unknown>;
|
|
47
|
+
/**
|
|
48
|
+
* How tables look: `'compact'` (the default) or `'boxed'`, as on
|
|
49
|
+
* RichTextEditor. Either way columns fit their words.
|
|
50
|
+
*/
|
|
51
|
+
tableLook?: TableLook;
|
|
47
52
|
className?: string;
|
|
48
53
|
} & Omit<ComponentProps<'div'>, 'children'>;
|
|
49
|
-
export declare function Markdown({ children, headingLevel, headingFrom, foldKey: foldKeyProp, foldable, onTasksChange: onTasksChangeProp, className, ...rest }: MarkdownProps): import("react").JSX.Element;
|
|
54
|
+
export declare function Markdown({ children, headingLevel, headingFrom, foldKey: foldKeyProp, foldable, onTasksChange: onTasksChangeProp, tableLook, className, ...rest }: MarkdownProps): import("react").JSX.Element;
|
|
@@ -23,6 +23,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
23
23
|
// sit: `#` and `##` on a tinted band, `###` a small uppercase label, and
|
|
24
24
|
// the text under them lined up with the words in the band (prose-look.ts).
|
|
25
25
|
//
|
|
26
|
+
// A table's columns fit their words (a cell wraps at 18rem; a right-
|
|
27
|
+
// aligned column, numbers, never wraps), in one of two looks
|
|
28
|
+
// (`tableLook`, as on RichTextEditor: styles/index.css "Tables in written
|
|
29
|
+
// text"). A cell whose whole text is one highlight is painted in it, and
|
|
30
|
+
// a first column of bold cells is row headers (src/lib/markdown-table.ts).
|
|
26
31
|
// A table wider than the text scrolls sideways, and while it does, its
|
|
27
32
|
// box is a tab stop, named for its columns, that the arrow keys scroll
|
|
28
33
|
// (prose-table-scroll.ts). Its cells wrap first, so that's only a table
|
|
@@ -60,11 +65,11 @@ import remarkGfm from 'remark-gfm';
|
|
|
60
65
|
import { cn } from 'cn';
|
|
61
66
|
import { ChevronDownIcon, ExternalLinkIcon, SquareCheckIcon, SquareIcon } from 'lucide-react';
|
|
62
67
|
import { Checkbox } from '../ui/checkbox.js';
|
|
63
|
-
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table.js';
|
|
64
68
|
import { useFolds } from '../../hooks/fold-store.js';
|
|
65
69
|
import { CALLOUT_KINDS, remarkCallouts } from '../../lib/markdown-callout.js';
|
|
66
70
|
import { rehypeFold, toggleFold } from '../../lib/markdown-fold.js';
|
|
67
71
|
import { isHighlightColor, remarkHighlights } from '../../lib/markdown-highlight.js';
|
|
72
|
+
import { rehypeTables } from '../../lib/markdown-table.js';
|
|
68
73
|
import { toggleTaskAt } from '../../lib/markdown-tasks.js';
|
|
69
74
|
import { textTopHeading } from '../../lib/markdown-top-heading.js';
|
|
70
75
|
import { CALLOUT_BOX, CALLOUT_LOOK, DEFAULT_HEADING_LEVEL, FOLD_BUTTON, FOLD_HEADING, headingBand, headingClass, headingTag, PROSE, PROSE_INSET, } from './prose-look.js';
|
|
@@ -87,10 +92,9 @@ function omitNode(props) {
|
|
|
87
92
|
delete rest.node;
|
|
88
93
|
return rest;
|
|
89
94
|
}
|
|
90
|
-
// A GFM table in
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// renames it.
|
|
95
|
+
// A GFM table in its box that scrolls sideways: a tab stop and a named
|
|
96
|
+
// region only while the table overflows it. Watched again after each
|
|
97
|
+
// render, so a header row with new words renames it.
|
|
94
98
|
function NotesTable({ hidden, ...props }) {
|
|
95
99
|
const boxRef = useRef(null);
|
|
96
100
|
useEffect(() => {
|
|
@@ -101,9 +105,7 @@ function NotesTable({ hidden, ...props }) {
|
|
|
101
105
|
const region = watchTableScroll(box, table, headerTexts(table));
|
|
102
106
|
return () => region.destroy();
|
|
103
107
|
});
|
|
104
|
-
return (_jsx(
|
|
105
|
-
// Folded away, the box goes with the table.
|
|
106
|
-
containerClassName: cn('rounded-lg border border-border', TABLE_SCROLL_FOCUS, hidden && 'hidden') }));
|
|
108
|
+
return (_jsx("div", { ref: boxRef, "data-slot": "table-container", className: cn('prose-table', TABLE_SCROLL_FOCUS, hidden && 'hidden'), children: _jsx("table", { ...props }) }));
|
|
107
109
|
}
|
|
108
110
|
// Ticks a task item by where it starts in the text: given only with
|
|
109
111
|
// `onTasksChange`. Each task item hands its box its own start and words.
|
|
@@ -211,16 +213,9 @@ const COMPONENTS = {
|
|
|
211
213
|
const { alt, ...rest } = omitNode(props);
|
|
212
214
|
return _jsx("img", { ...rest, alt: alt ?? '', loading: "lazy", className: "h-auto max-w-full rounded-md" });
|
|
213
215
|
},
|
|
214
|
-
// GFM tables
|
|
215
|
-
//
|
|
216
|
+
// GFM tables: drawn by styles/index.css "Tables in written text"
|
|
217
|
+
// (rehypeTables marks coloured cells and row headers).
|
|
216
218
|
table: (props) => _jsx(NotesTable, { ...omitNode(props) }),
|
|
217
|
-
thead: (props) => _jsx(TableHeader, { ...omitNode(props), className: "bg-muted" }),
|
|
218
|
-
tbody: (props) => _jsx(TableBody, { ...omitNode(props) }),
|
|
219
|
-
tr: (props) => _jsx(TableRow, { ...omitNode(props) }),
|
|
220
|
-
// Notes' cells wrap (a cell can hold a sentence) and have a line
|
|
221
|
-
// between columns, so a wide table still reads column by column.
|
|
222
|
-
th: (props) => _jsx(TableHead, { ...omitNode(props), className: "border-r border-border whitespace-normal last:border-r-0" }),
|
|
223
|
-
td: (props) => _jsx(TableCell, { ...omitNode(props), className: "border-r border-border whitespace-normal last:border-r-0" }),
|
|
224
219
|
};
|
|
225
220
|
// The components for each `headingLevel` and level that takes it (`#`,
|
|
226
221
|
// or the text's highest with `headingFrom="text"`), made once each.
|
|
@@ -236,7 +231,7 @@ function componentsFor(top, textTop) {
|
|
|
236
231
|
}
|
|
237
232
|
return components;
|
|
238
233
|
}
|
|
239
|
-
export function Markdown({ children, headingLevel = DEFAULT_HEADING_LEVEL, headingFrom = 'hash', foldKey: foldKeyProp, foldable = true, onTasksChange: onTasksChangeProp, className, ...rest }) {
|
|
234
|
+
export function Markdown({ children, headingLevel = DEFAULT_HEADING_LEVEL, headingFrom = 'hash', foldKey: foldKeyProp, foldable = true, onTasksChange: onTasksChangeProp, tableLook = 'compact', className, ...rest }) {
|
|
240
235
|
// On paper (EntityView's Print notes): every heading plain and open,
|
|
241
236
|
// every task box a picture.
|
|
242
237
|
const printing = usePrinting();
|
|
@@ -245,7 +240,7 @@ export function Markdown({ children, headingLevel = DEFAULT_HEADING_LEVEL, headi
|
|
|
245
240
|
const textTop = useMemo(() => (headingFrom === 'text' ? textTopHeading(children) : 1), [headingFrom, children]);
|
|
246
241
|
const components = useMemo(() => componentsFor(headingLevel, textTop), [headingLevel, textTop]);
|
|
247
242
|
const [folded, setFolded] = useFolds(foldKey);
|
|
248
|
-
const rehypePlugins = useMemo(() => (foldKey === undefined ? [] : [[rehypeFold, { folded }]]), [foldKey, folded]);
|
|
243
|
+
const rehypePlugins = useMemo(() => (foldKey === undefined ? [rehypeTables] : [rehypeTables, [rehypeFold, { folded }]]), [foldKey, folded]);
|
|
249
244
|
const toggle = useCallback((id) => setFolded(toggleFold(folded, id)), [folded, setFolded]);
|
|
250
245
|
// Ticks not yet back as `children`: shown, and built on by the next
|
|
251
246
|
// tick, so two quick ticks on a slow save both land. Kept while
|
|
@@ -261,5 +256,5 @@ export function Markdown({ children, headingLevel = DEFAULT_HEADING_LEVEL, headi
|
|
|
261
256
|
if (saving && typeof saving.then === 'function')
|
|
262
257
|
saving.then(undefined, () => setPending((current) => (current?.sent.includes(next) ? null : current)));
|
|
263
258
|
}, [onTasksChange, children, shown, live]);
|
|
264
|
-
return (_jsx("div", { "data-slot": "markdown", "data-fold-scope": foldKey, ...rest, className: cn('flex min-w-0 flex-col gap-3 type-body text-foreground break-words', PROSE_INSET, className), children: _jsx(FoldContext.Provider, { value: foldKey === undefined ? null : toggle, children: _jsx(TaskToggleContext.Provider, { value: onTasksChange ? tickTask : null, children: _jsx(ReactMarkdown, { remarkPlugins: REMARK_PLUGINS, rehypePlugins: rehypePlugins, components: components, children: onTasksChange ? shown : children }) }) }) }));
|
|
259
|
+
return (_jsx("div", { "data-slot": "markdown", "data-table-look": tableLook, "data-fold-scope": foldKey, ...rest, className: cn('flex min-w-0 flex-col gap-3 type-body text-foreground break-words', PROSE_INSET, className), children: _jsx(FoldContext.Provider, { value: foldKey === undefined ? null : toggle, children: _jsx(TaskToggleContext.Provider, { value: onTasksChange ? tickTask : null, children: _jsx(ReactMarkdown, { remarkPlugins: REMARK_PLUGINS, rehypePlugins: rehypePlugins, components: components, children: onTasksChange ? shown : children }) }) }) }));
|
|
265
260
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { RichTextEditorProps } from './rich-text-editor.js';
|
|
2
|
-
export default function MilkdownEditor({ id, defaultValue, onChange, onProblem, readOnly, autoFocus, placeholder, maxLength, toolbar, inline, upload, imageAccept, imageMaxSize, headingLevel, headingFrom, foldKey, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, }: RichTextEditorProps): import("react").JSX.Element;
|
|
2
|
+
export default function MilkdownEditor({ id, defaultValue, onChange, onProblem, readOnly, autoFocus, placeholder, maxLength, toolbar, inline, upload, imageAccept, imageMaxSize, headingLevel, headingFrom, foldKey, tableLook, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, }: RichTextEditorProps): import("react").JSX.Element;
|
|
@@ -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
|
|
24
|
-
// scroll while it does: rich-text-node-views.ts),
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
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)
|
|
@@ -814,8 +834,9 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
814
834
|
void editor.destroy();
|
|
815
835
|
root.replaceChildren();
|
|
816
836
|
};
|
|
817
|
-
// Made once per mount
|
|
818
|
-
//
|
|
837
|
+
// Made once per mount. A new defaultValue the editor didn't report
|
|
838
|
+
// remounts it (RichTextEditor's key), so it isn't read here.
|
|
839
|
+
// Attributes and read-only follow below.
|
|
819
840
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
820
841
|
}, []);
|
|
821
842
|
// Read-only and the ARIA attributes follow their props.
|
|
@@ -860,10 +881,33 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
860
881
|
// The editable element: where a menu over the text gives focus back
|
|
861
882
|
// (ProseMirror puts the caret back where it was as it takes it).
|
|
862
883
|
const textElement = () => editorView?.dom ?? null;
|
|
863
|
-
// A
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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();
|
|
867
911
|
};
|
|
868
912
|
// A callout's menu: its kind changed, or the callout taken away (its
|
|
869
913
|
// blocks kept). The menu gives focus back to the text.
|
|
@@ -945,14 +989,14 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
945
989
|
setLinkBox(null);
|
|
946
990
|
editorRef.current?.action((ctx) => ctx.get(editorViewCtx).focus());
|
|
947
991
|
};
|
|
948
|
-
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',
|
|
949
993
|
// Inline, it's the text itself: the caret shows where it's
|
|
950
994
|
// edited, and a refusal's reason under it says what's wrong.
|
|
951
995
|
!inline &&
|
|
952
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,
|
|
953
997
|
// Only once the editor is made: until then the editable element,
|
|
954
998
|
// and its id, aren't in the page (axe: aria-valid-attr-value).
|
|
955
|
-
controls: editorState ? editableId : undefined, onFormat: runFormat,
|
|
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) => {
|
|
956
1000
|
const files = Array.from(event.target.files ?? []);
|
|
957
1001
|
// Cleared, so picking the same file again still counts.
|
|
958
1002
|
event.target.value = '';
|
|
@@ -980,10 +1024,6 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
980
1024
|
// (ProseMirror's own separator images, around an image alone on
|
|
981
1025
|
// its line, are left alone.)
|
|
982
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',
|
|
983
|
-
// A table: a header row on the muted fill, a line between every
|
|
984
|
-
// cell, cells that wrap and never shrink to nothing; selected
|
|
985
|
-
// cells (a drag across them) tinted.
|
|
986
|
-
'[&_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',
|
|
987
1027
|
// Raw HTML from the text: shown as its characters, never as markup.
|
|
988
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',
|
|
989
1029
|
// The placeholder, in an empty document only (a decoration).
|
|
@@ -994,6 +1034,8 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
994
1034
|
return [
|
|
995
1035
|
createPortal(_jsx(TableBar, { side: "row", active: active, readOnly: readOnly, onAdd: () => addToTable(widget.getPos, 'row') }), widget.slot, `${widget.key}-row`),
|
|
996
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`),
|
|
997
1039
|
];
|
|
998
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) => {
|
|
999
1041
|
const next = event.relatedTarget;
|
|
@@ -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,9 +1,13 @@
|
|
|
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;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* The Markdown it opens with. A later value the editor didn't report
|
|
8
|
+
* (a record loaded after it mounted, a reset) replaces the text; the
|
|
9
|
+
* value it reported, passed back, changes nothing.
|
|
10
|
+
*/
|
|
7
11
|
defaultValue: string;
|
|
8
12
|
/** The Markdown to save, after every edit: untouched blocks as originally written. */
|
|
9
13
|
onChange: (markdown: string) => void;
|
|
@@ -86,6 +90,13 @@ export type RichTextEditorProps = {
|
|
|
86
90
|
* Default `true`.
|
|
87
91
|
*/
|
|
88
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;
|
|
89
100
|
className?: string;
|
|
90
101
|
/** The field's label. A rich-text area must have one: this, or `aria-labelledby`. */
|
|
91
102
|
'aria-label'?: string;
|
|
@@ -23,16 +23,50 @@ 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
|
// ``; 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
|
//
|
|
35
|
+
// `defaultValue` is read when it mounts, then followed: a new value the
|
|
36
|
+
// editor didn't report itself (a form's loaded record arriving after the
|
|
37
|
+
// editor, a reset) replaces the text, as if it opened with it. Its own
|
|
38
|
+
// edits, coming back as the value, change nothing, so the caret stays
|
|
39
|
+
// where it is. A caller that never feeds `onChange` back keeps its text.
|
|
40
|
+
//
|
|
29
41
|
// The editor (Milkdown) is loaded on first use, so a page that only reads
|
|
30
42
|
// Markdown never downloads it. Until it arrives, a placeholder the size of
|
|
31
43
|
// the field shows.
|
|
32
|
-
import { lazy, Suspense } from 'react';
|
|
44
|
+
import { lazy, Suspense, useState } from 'react';
|
|
33
45
|
import { cn } from 'cn';
|
|
34
46
|
import { Skeleton } from '../ui/skeleton.js';
|
|
35
47
|
const MilkdownEditor = lazy(() => import('./milkdown-editor.js'));
|
|
48
|
+
// When the editor opens again: each time `defaultValue` changes to
|
|
49
|
+
// Markdown it didn't report. Adjusted while rendering (React's "storing
|
|
50
|
+
// information from previous renders"), so the new text never shows a
|
|
51
|
+
// frame late.
|
|
52
|
+
function useOpenings(defaultValue) {
|
|
53
|
+
const [seen, setSeen] = useState(defaultValue);
|
|
54
|
+
const [reported, setReported] = useState(defaultValue);
|
|
55
|
+
const [opening, setOpening] = useState(0);
|
|
56
|
+
if (defaultValue !== seen) {
|
|
57
|
+
setSeen(defaultValue);
|
|
58
|
+
if (defaultValue !== reported) {
|
|
59
|
+
setReported(defaultValue);
|
|
60
|
+
setOpening(opening + 1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { opening, report: setReported };
|
|
64
|
+
}
|
|
36
65
|
export function RichTextEditor(props) {
|
|
37
|
-
|
|
66
|
+
const { opening, report } = useOpenings(props.defaultValue);
|
|
67
|
+
const { onChange } = props;
|
|
68
|
+
return (_jsx(Suspense, { fallback: _jsxs("div", { "data-slot": "rich-text-editor", "data-state": "loading", className: cn('flex w-full flex-col', !props.inline && 'rounded-lg border border-input', props.className), children: [(props.toolbar ?? true) === true && _jsx("div", { className: "h-9 border-b border-input" }), _jsxs("div", { className: cn('flex flex-col gap-2', props.inline ? 'py-0.5' : 'min-h-24 px-2.5 py-2'), children: [_jsx(Skeleton, { className: "h-4 w-3/4" }), _jsx(Skeleton, { className: "h-4 w-1/2" })] })] }), children: _jsx(MilkdownEditor, { ...props, onChange: (markdown) => {
|
|
69
|
+
report(markdown);
|
|
70
|
+
onChange(markdown);
|
|
71
|
+
}, foldKey: props.foldable === false ? undefined : props.foldKey }, opening) }));
|
|
38
72
|
}
|
|
@@ -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
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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
|
|
90
|
-
*
|
|
91
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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;
|