@tristan2828/ui-foundation 3.20.1 → 3.22.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/add-an-entity.md +9 -4
- package/conventions/docs/cell-patterns.md +13 -4
- package/conventions/docs/design-language.md +22 -0
- package/dist/components/app/data-table.js +21 -13
- package/dist/components/app/markdown.js +38 -20
- package/dist/components/app/milkdown-editor.js +189 -31
- package/dist/components/app/prose-look.d.ts +32 -0
- package/dist/components/app/prose-look.js +48 -0
- package/dist/components/app/rich-text-format-items.d.ts +13 -1
- package/dist/components/app/rich-text-format-items.js +43 -10
- package/dist/components/app/rich-text-formats.d.ts +15 -2
- package/dist/components/app/rich-text-formats.js +99 -10
- package/dist/components/app/rich-text-node-views.d.ts +31 -0
- package/dist/components/app/rich-text-node-views.js +79 -0
- package/dist/components/app/rich-text-shortcuts.d.ts +12 -0
- package/dist/components/app/rich-text-shortcuts.js +52 -0
- package/dist/components/app/rich-text-slash-menu.js +4 -1
- package/dist/components/app/rich-text-slash.js +26 -10
- package/dist/components/app/rich-text-table-menu.d.ts +20 -0
- package/dist/components/app/rich-text-table-menu.js +53 -0
- package/dist/components/app/rich-text-table.d.ts +19 -0
- package/dist/components/app/rich-text-table.js +162 -0
- package/dist/components/app/rich-text-toolbar.d.ts +11 -2
- package/dist/components/app/rich-text-toolbar.js +14 -23
- package/dist/components/app/rich-text-widgets.d.ts +19 -0
- package/dist/components/app/rich-text-widgets.js +34 -0
- package/dist/components/app/secondary-filters.d.ts +18 -0
- package/dist/components/app/secondary-filters.js +32 -0
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/combobox.js +7 -1
- package/dist/components/ui/dropdown-menu.d.ts +29 -0
- package/dist/components/ui/dropdown-menu.js +51 -0
- package/dist/hooks/use-table-url-state.d.ts +1 -1
- package/dist/hooks/use-table-url-state.js +6 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/markdown-callout.d.ts +38 -0
- package/dist/lib/markdown-callout.js +103 -0
- package/package.json +1 -1
- package/styles/index.css +17 -0
- package/styles/theme.css +30 -0
|
@@ -189,7 +189,12 @@ is a filled-in example while the Widgets demo is still here).
|
|
|
189
189
|
- `src/routes/widgets/widgets-table.tsx` →
|
|
190
190
|
`src/routes/<entity>/<entity>-table.tsx` — thin consumer of the
|
|
191
191
|
foundation's `<DataTable>`. Swap the type, columns, and toolbar
|
|
192
|
-
filters.
|
|
192
|
+
filters. More than three filters (search, saved views aside): keep
|
|
193
|
+
the most used in the toolbar and put the rest inside
|
|
194
|
+
`<SecondaryFilters>`, as the template does with Extra Categories,
|
|
195
|
+
Progress and In Stock. Pass it each filter that's on as an
|
|
196
|
+
`ActiveFilter` (its chip text and how to take it off) and an
|
|
197
|
+
`onClear` that clears them all in one `setFilters` call.
|
|
193
198
|
- `src/routes/widgets/widget-view.tsx` →
|
|
194
199
|
`src/routes/<entity>/<entity>-view.tsx` — thin consumer of the
|
|
195
200
|
foundation's `<EntityView>`. Swap the title, badges and sections for
|
|
@@ -275,9 +280,9 @@ is a filled-in example while the Widgets demo is still here).
|
|
|
275
280
|
`<a target="_blank" rel="noreferrer">` with an `ExternalLinkIcon`
|
|
276
281
|
and a visually hidden "(opens in a new tab)".
|
|
277
282
|
- **Long text the plan marks Markdown** renders through the
|
|
278
|
-
foundation's `<Markdown>`: headings, lists, tables
|
|
279
|
-
opening in a new tab; raw HTML shows
|
|
280
|
-
as markup. The app needs no Markdown dependency of its own. Plain
|
|
283
|
+
foundation's `<Markdown>`: headings, lists, tables, callouts
|
|
284
|
+
(`> [!NOTE]`) and links, links opening in a new tab; raw HTML shows
|
|
285
|
+
as the characters typed, never as markup. The app needs no Markdown dependency of its own. Plain
|
|
281
286
|
long text renders as text (`whitespace-pre-line` keeps its line
|
|
282
287
|
breaks).
|
|
283
288
|
- **Quick actions: only the fields the plan's `Quick actions` line
|
|
@@ -740,15 +740,24 @@ const price = editInPlace({
|
|
|
740
740
|
(a count shows from 80% of it, in the destructive text tone once over).
|
|
741
741
|
Its keys: Ctrl/Cmd+K adds or edits a link (web and email addresses
|
|
742
742
|
only), Ctrl/Cmd+Shift+Enter ticks the task item the caret is in, and
|
|
743
|
-
Markdown pasted as plain text arrives formatted.
|
|
744
|
-
|
|
743
|
+
Markdown pasted as plain text arrives formatted. Notion's block keys
|
|
744
|
+
work: Ctrl+Shift+1 to 3 for headings, +4 tasks, +5 bullets, +6 numbers,
|
|
745
|
+
+8 a code block, +0 back to text (Cmd+Option on a Mac), and
|
|
746
|
+
Ctrl/Cmd+Shift+S strikes through. Ctrl/Cmd+Enter stays the save, in a
|
|
747
|
+
table too. A formatting toolbar sits above the text (bold to link, each
|
|
745
748
|
button naming its shortcut), and a smaller one floats over selected
|
|
746
749
|
words (bold, italic, strikethrough, code, link); Alt+F10 reaches the
|
|
747
750
|
floating one, else the fixed one. Both are part of the field, so moving
|
|
748
751
|
to them doesn't save, and Esc hides the floating one before it gives up
|
|
749
752
|
the edit. "/" typed at a line's start lists the blocks the line can
|
|
750
|
-
become (headings, lists, quote, code block
|
|
751
|
-
|
|
753
|
+
become (three headings, lists, quote, callout, code block, divider,
|
|
754
|
+
table); typing filters ("/warn" finds a warning callout), Enter or Tab
|
|
755
|
+
picks, and Esc closes the list before it gives up the edit. A callout
|
|
756
|
+
is GitHub's `> [!NOTE]` (note, tip, important, warning, caution), its
|
|
757
|
+
icon a menu of kinds. In a table, Tab in the last cell adds a row,
|
|
758
|
+
Enter goes down a column (and out from the last row), bars under and
|
|
759
|
+
beside it add a row or a column, and the toolbar's Table menu inserts
|
|
760
|
+
rows and columns, aligns a column and deletes.
|
|
752
761
|
`toolbar={false}` leaves out the toolbars and the slash menu where a
|
|
753
762
|
value is short and the shortcuts are enough.
|
|
754
763
|
- **The save is the single-field save** (`useRecordUpdate` with
|
|
@@ -172,6 +172,28 @@ height and weight together, from tokens in the package's `styles/theme.css`:
|
|
|
172
172
|
- A shadcn primitive keeps its own sizes (`CardTitle`, `Button`). Don't put
|
|
173
173
|
a role on one: its built-in size classes would then compete with the role.
|
|
174
174
|
|
|
175
|
+
### Written text (notes)
|
|
176
|
+
|
|
177
|
+
Long text written as Markdown (`<Markdown>` and `RichTextEditor`) has its
|
|
178
|
+
own three heading roles, so `#`, `##` and `###` in a note never look
|
|
179
|
+
alike. They're for headings inside the text only, never a screen's own
|
|
180
|
+
outline (that stays `type-page-title` and `type-section-title`):
|
|
181
|
+
|
|
182
|
+
| Class | For | Size / weight |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| `type-heading-1` | `#` in a note, with a rule under it | 26px / 700 |
|
|
185
|
+
| `type-heading-2` | `##` | 20px / 600 |
|
|
186
|
+
| `type-heading-3` | `###` | 17px / 600 |
|
|
187
|
+
|
|
188
|
+
The same text has two colour tokens of its own: `--link` (`text-link`), a
|
|
189
|
+
link's colour, blue as well as underlined (5.59:1 on white, 8.90:1 on the
|
|
190
|
+
dark page), and `--rule` (`bg-rule`), the 2px line a `---` divider draws,
|
|
191
|
+
stronger than `--border`'s hairline. A callout (`> [!NOTE]`) is a 10% wash
|
|
192
|
+
of its kind's tone behind ordinary text, with the icon in the tone's text
|
|
193
|
+
shade: note `info`, tip `success`, important category slot 2, warning
|
|
194
|
+
`warning`, caution `destructive`. The package draws all of it; an app
|
|
195
|
+
only renders `<Markdown>` or the editor.
|
|
196
|
+
|
|
175
197
|
## Density
|
|
176
198
|
|
|
177
199
|
Tables have three densities, set with `data-density` on **any ancestor**
|
|
@@ -37,20 +37,27 @@ function leafColumnIds(columns) {
|
|
|
37
37
|
function TableSkeleton({ columnCount }) {
|
|
38
38
|
return (_jsx("div", { className: "flex flex-col gap-2 p-2", "data-state": "loading", children: Array.from({ length: 5 }, (_, row) => (_jsx("div", { className: "flex gap-4", children: Array.from({ length: columnCount }, (_, col) => (_jsx(Skeleton, { className: "h-8 flex-1" }, col))) }, row))) }));
|
|
39
39
|
}
|
|
40
|
-
// The pinned cell gets
|
|
40
|
+
// The pinned cell gets an opaque background, not `bg-inherit`: the zebra
|
|
41
41
|
// stripe (table.tsx) is a *translucent* color, so inheriting it would let
|
|
42
42
|
// the columns scrolling behind show through at 40% opacity — exactly the
|
|
43
|
-
// bleed-through a solid background avoids.
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
// bleed-through a solid background avoids. On an even row it's the stripe
|
|
44
|
+
// pre-mixed onto the page (--table-stripe), so the stripe runs the whole
|
|
45
|
+
// row's width (issue #92); `:where()` keeps that rule's specificity at the
|
|
46
|
+
// background's, so the hover below still wins on every row. The pinned
|
|
47
|
+
// cell carries the hover highlight. --accent is only ~3% off the page
|
|
48
|
+
// background, so a solid left edge marker carries the signal; an inset
|
|
49
|
+
// shadow rather than a border, so nothing reflows.
|
|
50
|
+
const PINNED_CELL_CLASS = 'z-10 bg-background [:where(tbody>tr:nth-child(even))>&]:bg-(--table-stripe) group-hover/row:bg-accent';
|
|
51
|
+
const PINNED_COLUMN_CLASS = `sticky left-0 border-r ${PINNED_CELL_CLASS} group-hover/row:shadow-[inset_3px_0_0_0_var(--primary)]`;
|
|
50
52
|
// The right-edge mirror, opaque for the same reason. Its edge marker points
|
|
51
53
|
// inward from the right, so the two pinned columns bracket the row.
|
|
52
|
-
const PINNED_LAST_COLUMN_CLASS =
|
|
53
|
-
|
|
54
|
+
const PINNED_LAST_COLUMN_CLASS = `sticky right-0 border-l ${PINNED_CELL_CLASS} group-hover/row:shadow-[inset_-3px_0_0_0_var(--primary)]`;
|
|
55
|
+
// A pinned first column's content, capped below md at 45% of the table's
|
|
56
|
+
// scroll area (`cqw`: the container is an inline-size query container)
|
|
57
|
+
// and wrapping, so a long title can't widen the column past the screen
|
|
58
|
+
// and leave every other column scrolling underneath it (issue #91).
|
|
59
|
+
// Wider screens are unchanged: the column sizes to its content.
|
|
60
|
+
const PINNED_FIRST_CONTENT_CLASS = 'max-md:max-w-[45cqw] max-md:whitespace-normal max-md:break-words';
|
|
54
61
|
// Hover highlights the pinned cell, not the whole row. A row-level hover
|
|
55
62
|
// cannot be made consistent against the zebra stripe: `tr:nth-child(even)`
|
|
56
63
|
// is specificity (0,2,1) and `tr:hover` is (0,2,0), so the stripe wins and
|
|
@@ -169,9 +176,10 @@ export function DataTable({ columns, data, total, page, pageSize, onPageChange,
|
|
|
169
176
|
// scrollable (wheel, trackpad, drag, and the sync effect); it
|
|
170
177
|
// just no longer draws a bar directly under the last row.
|
|
171
178
|
containerClassName: cn(pinFirstColumn &&
|
|
172
|
-
'[scrollbar-width:none] [&::-webkit-scrollbar]:hidden'), children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => (_jsx(TableRow, { children: headerGroup.headers.map((header, index) => {
|
|
179
|
+
'@container [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'), children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => (_jsx(TableRow, { children: headerGroup.headers.map((header, index) => {
|
|
173
180
|
const canSort = header.column.getCanSort();
|
|
174
181
|
const sortDirection = header.column.getIsSorted();
|
|
182
|
+
const content = header.isPlaceholder ? null : canSort ? (_jsxs("button", { type: "button", className: cn('flex items-center gap-1 text-foreground', header.column.columnDef.meta?.align === 'center' && 'w-full justify-center'), onClick: header.column.getToggleSortingHandler(), children: [flexRender(header.column.columnDef.header, header.getContext()), sortDirection === 'asc' ? (_jsx(ArrowUpIcon, { className: "size-3.5 text-muted-foreground" })) : sortDirection === 'desc' ? (_jsx(ArrowDownIcon, { className: "size-3.5 text-muted-foreground" })) : (_jsx(ArrowUpDownIcon, { className: "size-3.5 text-muted-foreground/50" }))] })) : (flexRender(header.column.columnDef.header, header.getContext()));
|
|
175
183
|
return (_jsx(TableHead, { className: cn(pinFirstColumn && index === 0 && PINNED_COLUMN_CLASS, pinLastColumn &&
|
|
176
184
|
index === headerGroup.headers.length - 1 &&
|
|
177
185
|
PINNED_LAST_COLUMN_CLASS, header.column.columnDef.meta?.align === 'center' && 'text-center'), "aria-sort": !canSort
|
|
@@ -180,8 +188,8 @@ export function DataTable({ columns, data, total, page, pageSize, onPageChange,
|
|
|
180
188
|
? 'ascending'
|
|
181
189
|
: sortDirection === 'desc'
|
|
182
190
|
? 'descending'
|
|
183
|
-
: 'none', children:
|
|
191
|
+
: 'none', children: pinFirstColumn && index === 0 ? (_jsx("div", { className: PINNED_FIRST_CONTENT_CLASS, children: content })) : (content) }, header.id));
|
|
184
192
|
}) }, headerGroup.id))) }), _jsx(TableBody, { children: table.getRowModel().rows.map((row) => (_jsx(TableRow, { className: cn((pinFirstColumn || pinLastColumn) && ROW_GROUP_CLASS), children: row.getVisibleCells().map((cell, index) => (_jsx(TableCell, { className: cn(pinFirstColumn && index === 0 && PINNED_COLUMN_CLASS, pinLastColumn &&
|
|
185
193
|
index === row.getVisibleCells().length - 1 &&
|
|
186
|
-
PINNED_LAST_COLUMN_CLASS, cell.column.columnDef.meta?.align === 'center' && 'text-center'), children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))) }, row.id))) })] }), pinFirstColumn && isOverflowing && (_jsx("div", { ref: barRef, "aria-hidden": "true", tabIndex: -1, "data-slot": "bottom-scrollbar", className: "sticky bottom-0 z-20 h-4 overflow-x-auto border-t border-border bg-background", children: _jsx("div", { style: { width: scrollWidth, height: 1 } }) })), _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("p", { className: "type-body text-muted-foreground", children: [total === 0 ? 0 : (page - 1) * pageSize + 1, '–', Math.min(page * pageSize, total), " of ", total] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "icon-sm", "aria-label": "Previous page", disabled: page <= 1, onClick: () => onPageChange(page - 1), children: _jsx(ChevronLeftIcon, {}) }), _jsxs("span", { className: "type-body text-muted-foreground", children: ["Page ", page, " of ", pageCount] }), _jsx(Button, { variant: "outline", size: "icon-sm", "aria-label": "Next page", disabled: page >= pageCount, onClick: () => onPageChange(page + 1), children: _jsx(ChevronRightIcon, {}) })] })] })] }))] }));
|
|
194
|
+
PINNED_LAST_COLUMN_CLASS, cell.column.columnDef.meta?.align === 'center' && 'text-center'), children: pinFirstColumn && index === 0 ? (_jsx("div", { className: PINNED_FIRST_CONTENT_CLASS, children: flexRender(cell.column.columnDef.cell, cell.getContext()) })) : (flexRender(cell.column.columnDef.cell, cell.getContext())) }, cell.id))) }, row.id))) })] }), pinFirstColumn && isOverflowing && (_jsx("div", { ref: barRef, "aria-hidden": "true", tabIndex: -1, "data-slot": "bottom-scrollbar", className: "sticky bottom-0 z-20 h-4 overflow-x-auto border-t border-border bg-background", children: _jsx("div", { style: { width: scrollWidth, height: 1 } }) })), _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("p", { className: "type-body text-muted-foreground", children: [total === 0 ? 0 : (page - 1) * pageSize + 1, '–', Math.min(page * pageSize, total), " of ", total] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "icon-sm", "aria-label": "Previous page", disabled: page <= 1, onClick: () => onPageChange(page - 1), children: _jsx(ChevronLeftIcon, {}) }), _jsxs("span", { className: "type-body text-muted-foreground", children: ["Page ", page, " of ", pageCount] }), _jsx(Button, { variant: "outline", size: "icon-sm", "aria-label": "Next page", disabled: page >= pageCount, onClick: () => onPageChange(page + 1), children: _jsx(ChevronRightIcon, {}) })] })] })] }))] }));
|
|
187
195
|
}
|
|
@@ -4,7 +4,12 @@ import remarkGfm from 'remark-gfm';
|
|
|
4
4
|
import { cn } from 'cn';
|
|
5
5
|
import { ExternalLinkIcon, SquareCheckIcon, SquareIcon } from 'lucide-react';
|
|
6
6
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table.js';
|
|
7
|
-
|
|
7
|
+
import { CALLOUT_KINDS, remarkCallouts } from '../../lib/markdown-callout.js';
|
|
8
|
+
import { CALLOUT_BOX, CALLOUT_LOOK, headingClass, PROSE } from './prose-look.js';
|
|
9
|
+
const REMARK_PLUGINS = [remarkGfm, remarkCallouts];
|
|
10
|
+
function isCalloutKind(value) {
|
|
11
|
+
return CALLOUT_KINDS.includes(value);
|
|
12
|
+
}
|
|
8
13
|
// Every element react-markdown hands over carries `node` (the syntax tree
|
|
9
14
|
// node). It isn't a DOM attribute, so it's dropped before spreading.
|
|
10
15
|
function omitNode(props) {
|
|
@@ -13,33 +18,44 @@ function omitNode(props) {
|
|
|
13
18
|
return rest;
|
|
14
19
|
}
|
|
15
20
|
const COMPONENTS = {
|
|
16
|
-
h1: (props) => _jsx("h3", { ...omitNode(props), className:
|
|
17
|
-
h2: (props) => _jsx("h4", { ...omitNode(props), className:
|
|
18
|
-
h3: (props) => _jsx("h5", { ...omitNode(props), className:
|
|
19
|
-
h4: (props) => _jsx("h6", { ...omitNode(props), className:
|
|
20
|
-
h5: (props) => _jsx("h6", { ...omitNode(props), className:
|
|
21
|
-
h6: (props) => _jsx("h6", { ...omitNode(props), className:
|
|
21
|
+
h1: (props) => _jsx("h3", { ...omitNode(props), className: cn(headingClass(1), 'text-foreground') }),
|
|
22
|
+
h2: (props) => _jsx("h4", { ...omitNode(props), className: cn(headingClass(2), 'text-foreground') }),
|
|
23
|
+
h3: (props) => _jsx("h5", { ...omitNode(props), className: cn(headingClass(3), 'text-foreground') }),
|
|
24
|
+
h4: (props) => _jsx("h6", { ...omitNode(props), className: cn(headingClass(4), 'text-foreground') }),
|
|
25
|
+
h5: (props) => _jsx("h6", { ...omitNode(props), className: cn(headingClass(5), 'text-foreground') }),
|
|
26
|
+
h6: (props) => _jsx("h6", { ...omitNode(props), className: cn(headingClass(6), 'text-foreground') }),
|
|
22
27
|
a: (props) => {
|
|
23
28
|
const { href, children, ...rest } = omitNode(props);
|
|
24
29
|
if (!href)
|
|
25
30
|
return _jsx("span", { children: children });
|
|
26
|
-
return (_jsxs("a", { ...rest, href: href, target: "_blank", rel: "noreferrer", className:
|
|
27
|
-
},
|
|
28
|
-
ul: (props) => {
|
|
29
|
-
const { className, ...rest } = omitNode(props);
|
|
30
|
-
const isTaskList = className?.includes('contains-task-list');
|
|
31
|
-
return _jsx("ul", { ...rest, className: cn('flex flex-col gap-1', isTaskList ? 'list-none' : 'list-disc pl-6') });
|
|
31
|
+
return (_jsxs("a", { ...rest, href: href, target: "_blank", rel: "noreferrer", className: PROSE.link, children: [children, _jsx(ExternalLinkIcon, { "aria-hidden": "true", className: "ml-0.5 inline size-3 align-baseline" }), _jsx("span", { className: "sr-only", children: " (opens in a new tab)" })] }));
|
|
32
32
|
},
|
|
33
|
+
// A list can mix plain and task items (`- a` then `- [ ] b` is one
|
|
34
|
+
// list): each task item drops its own bullet, the rest keep theirs. Its
|
|
35
|
+
// box is inline, so a loose list's item (its text in a <p>) lays out
|
|
36
|
+
// the same as a tight one's.
|
|
37
|
+
ul: (props) => _jsx("ul", { ...omitNode(props), className: "flex list-disc flex-col gap-1 pl-6" }),
|
|
33
38
|
ol: (props) => _jsx("ol", { ...omitNode(props), className: "flex list-decimal flex-col gap-1 pl-6" }),
|
|
34
39
|
li: (props) => {
|
|
35
40
|
const { className, ...rest } = omitNode(props);
|
|
36
|
-
return _jsx("li", { ...rest, className: cn(className?.includes('task-list-item') && '
|
|
41
|
+
return _jsx("li", { ...rest, className: cn(className?.includes('task-list-item') && '-ml-6 list-none') });
|
|
37
42
|
},
|
|
38
43
|
// A GitHub task-list box (`- [x] done`). Read-only, so a glyph with its
|
|
39
44
|
// state in words, never a checkbox someone will try to tick.
|
|
40
|
-
input: ({ checked }) => checked ? (_jsx(SquareCheckIcon, { role: "img", "aria-label": "Done", className: "
|
|
45
|
+
input: ({ checked }) => checked ? (_jsx(SquareCheckIcon, { role: "img", "aria-label": "Done", className: "mr-2 inline-block size-4 align-[-0.1875rem] text-foreground" })) : (_jsx(SquareIcon, { role: "img", "aria-label": "Not done", className: "mr-2 inline-block size-4 align-[-0.1875rem] text-muted-foreground" })),
|
|
41
46
|
blockquote: (props) => (_jsx("blockquote", { ...omitNode(props), className: "flex flex-col gap-3 border-l-2 border-border pl-4 text-muted-foreground" })),
|
|
42
|
-
hr: () => _jsx("hr", { className:
|
|
47
|
+
hr: () => _jsx("hr", { className: PROSE.rule }),
|
|
48
|
+
// A callout (remarkCallouts marks it): its kind's wash and icon, named
|
|
49
|
+
// for screen readers by the kind ("Note: …").
|
|
50
|
+
div: (props) => {
|
|
51
|
+
const { children, ...rest } = omitNode(props);
|
|
52
|
+
const kind = rest['data-callout'];
|
|
53
|
+
if (!isCalloutKind(kind))
|
|
54
|
+
return _jsx("div", { ...rest, children: children });
|
|
55
|
+
const look = CALLOUT_LOOK[kind];
|
|
56
|
+
const Icon = look.icon;
|
|
57
|
+
return (_jsxs("div", { role: "note", "data-callout": kind, className: cn(CALLOUT_BOX, look.box), children: [_jsx(Icon, { "aria-hidden": "true", className: cn('mt-0.5 size-4 shrink-0', look.tint) }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-2", children: [_jsxs("span", { className: "sr-only", children: [look.label, ": "] }), children] })] }));
|
|
58
|
+
},
|
|
43
59
|
pre: (props) => (_jsx("pre", { ...omitNode(props), className: "overflow-x-auto rounded-lg bg-muted p-3 font-mono text-foreground [&_code]:bg-transparent [&_code]:p-0" })),
|
|
44
60
|
code: (props) => _jsx("code", { ...omitNode(props), className: "rounded bg-muted px-1 py-0.5 font-mono text-foreground" }),
|
|
45
61
|
img: (props) => {
|
|
@@ -48,12 +64,14 @@ const COMPONENTS = {
|
|
|
48
64
|
},
|
|
49
65
|
// GFM tables use the package's own table, zebra stripes and density
|
|
50
66
|
// tokens included, so a table in the notes reads like every other table.
|
|
51
|
-
table: (props) => _jsx(Table, { ...omitNode(props) }),
|
|
52
|
-
thead: (props) => _jsx(TableHeader, { ...omitNode(props) }),
|
|
67
|
+
table: (props) => _jsx(Table, { ...omitNode(props), containerClassName: "rounded-lg border border-border" }),
|
|
68
|
+
thead: (props) => _jsx(TableHeader, { ...omitNode(props), className: "bg-muted" }),
|
|
53
69
|
tbody: (props) => _jsx(TableBody, { ...omitNode(props) }),
|
|
54
70
|
tr: (props) => _jsx(TableRow, { ...omitNode(props) }),
|
|
55
|
-
|
|
56
|
-
|
|
71
|
+
// Notes' cells wrap (a cell can hold a sentence) and have a line
|
|
72
|
+
// between columns, so a wide table still reads column by column.
|
|
73
|
+
th: (props) => _jsx(TableHead, { ...omitNode(props), className: "border-r border-border whitespace-normal last:border-r-0" }),
|
|
74
|
+
td: (props) => _jsx(TableCell, { ...omitNode(props), className: "border-r border-border whitespace-normal last:border-r-0" }),
|
|
57
75
|
};
|
|
58
76
|
export function Markdown({ children, className, ...rest }) {
|
|
59
77
|
return (_jsx("div", { "data-slot": "markdown", ...rest, className: cn('flex min-w-0 flex-col gap-3 type-body text-foreground break-words', className), children: _jsx(ReactMarkdown, { remarkPlugins: REMARK_PLUGINS, components: COMPONENTS, children: children }) }));
|
|
@@ -13,13 +13,22 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
13
13
|
// in the text shows as its characters (an uneditable chip, Milkdown's
|
|
14
14
|
// html node), and HTML pasted as such a chip becomes plain text.
|
|
15
15
|
// - Headings render from <h3>, like <Markdown>: the page owns <h1> and
|
|
16
|
-
// <h2
|
|
16
|
+
// <h2>; they look like <Markdown>'s (prose-look.ts). A task-list item
|
|
17
|
+
// renders a real checkbox that toggles it.
|
|
18
|
+
// - Callouts (`> [!NOTE]`, src/lib/markdown-callout.ts): a node of their
|
|
19
|
+
// own, drawn as <Markdown> draws them, the icon a menu of kinds.
|
|
20
|
+
// - Tables in a box that scrolls sideways, with Notion's "add a row" and
|
|
21
|
+
// "add a column" bars, Tab in the last cell adding a row and Enter going
|
|
22
|
+
// down a column (rich-text-table.ts); the toolbar's Table menu does the
|
|
23
|
+
// rest.
|
|
17
24
|
// - Edited blocks are written in the document's own bullet and rule marks.
|
|
18
25
|
// - Markdown pasted as plain text arrives formatted (still no HTML), and a
|
|
19
26
|
// copy out of the editor is Markdown.
|
|
20
27
|
// - Ctrl/Cmd+K adds or edits a link (a box at the caret: web and email
|
|
21
28
|
// addresses only); Ctrl/Cmd+Shift+Enter ticks the task item the caret is
|
|
22
|
-
// in. Ctrl/Cmd+Enter stays the field's save.
|
|
29
|
+
// in. Ctrl/Cmd+Enter stays the field's save, in a table too. Notion's
|
|
30
|
+
// block keys (Ctrl+Shift+1 for a heading, +5 for bullets, …:
|
|
31
|
+
// rich-text-shortcuts.ts).
|
|
23
32
|
// - A placeholder in an empty document, and a count of the Markdown's
|
|
24
33
|
// length as it nears `maxLength`.
|
|
25
34
|
// - A fixed toolbar above the text (rich-text-toolbar.tsx), and a
|
|
@@ -30,26 +39,34 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
30
39
|
// line's start lists the blocks it can become, also unless
|
|
31
40
|
// `toolbar={false}`.
|
|
32
41
|
import { useEffect, useId, useRef, useState } from 'react';
|
|
42
|
+
import { createPortal } from 'react-dom';
|
|
33
43
|
import { defaultValueCtx, Editor, editorViewCtx, editorViewOptionsCtx, parserCtx, remarkStringifyOptionsCtx, rootCtx, schemaCtx, serializerCtx, } from '@milkdown/kit/core';
|
|
34
44
|
import { history } from '@milkdown/kit/plugin/history';
|
|
35
45
|
import { commands, inputRules, keymap, markInputRules, plugins, remarkPreserveEmptyLinePlugin, schema, } from '@milkdown/kit/preset/commonmark';
|
|
36
46
|
import { gfm } from '@milkdown/kit/preset/gfm';
|
|
37
47
|
import { Fragment, Slice } from '@milkdown/kit/prose/model';
|
|
38
|
-
import { NodeSelection, Plugin } from '@milkdown/kit/prose/state';
|
|
48
|
+
import { NodeSelection, Plugin, TextSelection } from '@milkdown/kit/prose/state';
|
|
49
|
+
import { isInTable, TableMap } from '@milkdown/kit/prose/tables';
|
|
39
50
|
import { Decoration, DecorationSet } from '@milkdown/kit/prose/view';
|
|
40
|
-
import { $prose, getMarkdown } from '@milkdown/kit/utils';
|
|
51
|
+
import { $nodeSchema, $prose, $remark, getMarkdown } from '@milkdown/kit/utils';
|
|
41
52
|
import { cn } from 'cn';
|
|
42
53
|
import { Button } from '../ui/button.js';
|
|
43
54
|
import { Field, FieldError, FieldLabel } from '../ui/field.js';
|
|
44
55
|
import { Input } from '../ui/input.js';
|
|
45
56
|
import { Popover, PopoverContent } from '../ui/popover.js';
|
|
46
57
|
import { linkHref } from '../../lib/link-href.js';
|
|
58
|
+
import { calloutHandler, remarkCalloutNodes } from '../../lib/markdown-callout.js';
|
|
47
59
|
import { MarkdownMergeError, markdownStyle, mergeMarkdown } from '../../lib/markdown-merge.js';
|
|
60
|
+
import { headingClass } from './prose-look.js';
|
|
48
61
|
import { RichTextFloatingToolbar } from './rich-text-floating-toolbar.js';
|
|
49
|
-
import { toggleFormat } from './rich-text-formats.js';
|
|
62
|
+
import { tableAt, toggleFormat } from './rich-text-formats.js';
|
|
63
|
+
import { calloutKind, calloutView, tableView } from './rich-text-node-views.js';
|
|
64
|
+
import { handleShortcut } from './rich-text-shortcuts.js';
|
|
65
|
+
import { addRowAtEnd, handleTableKey, runTableAction } from './rich-text-table.js';
|
|
50
66
|
import { applySlash, closeSlash, slashItems, slashOptionId, slashPlugin, slashQuery, } from './rich-text-slash.js';
|
|
51
67
|
import { RichTextSlashMenu } from './rich-text-slash-menu.js';
|
|
52
68
|
import { RichTextToolbar } from './rich-text-toolbar.js';
|
|
69
|
+
import { CalloutKindMenu, TableBar } from './rich-text-widgets.js';
|
|
53
70
|
// The commonmark preset, minus the plugin that writes `<br />`. A $remark
|
|
54
71
|
// plugin is a pair (its options and the plugin), spread into `plugins`.
|
|
55
72
|
const LEFT_OUT = remarkPreserveEmptyLinePlugin;
|
|
@@ -61,6 +78,34 @@ const COMMONMARK = [
|
|
|
61
78
|
...keymap,
|
|
62
79
|
...plugins.filter((plugin) => !LEFT_OUT.includes(plugin)),
|
|
63
80
|
];
|
|
81
|
+
// Callouts: a node of their own, read from the quote remarkCalloutNodes
|
|
82
|
+
// marks and written back as one with its marker (calloutHandler).
|
|
83
|
+
const calloutRemark = $remark('remarkCalloutNodes', () => remarkCalloutNodes);
|
|
84
|
+
const calloutSchema = $nodeSchema('callout', () => ({
|
|
85
|
+
content: 'block+',
|
|
86
|
+
group: 'block',
|
|
87
|
+
defining: true,
|
|
88
|
+
attrs: { kind: { default: 'note' }, marker: { default: null } },
|
|
89
|
+
parseDOM: [
|
|
90
|
+
{
|
|
91
|
+
tag: 'div[data-callout]',
|
|
92
|
+
getAttrs: (dom) => ({ kind: calloutKind(dom.dataset.callout) }),
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
toDOM: (node) => ['div', { 'data-callout': String(node.attrs.kind) }, 0],
|
|
96
|
+
parseMarkdown: {
|
|
97
|
+
match: (node) => node.type === 'callout',
|
|
98
|
+
runner: (state, node, type) => {
|
|
99
|
+
state.openNode(type, { kind: calloutKind(node.kind), marker: node.marker ?? null }).next(node.children).closeNode();
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
toMarkdown: {
|
|
103
|
+
match: (node) => node.type.name === 'callout',
|
|
104
|
+
runner: (state, node) => {
|
|
105
|
+
state.openNode('callout', undefined, { kind: node.attrs.kind, marker: node.attrs.marker }).next(node.content).closeNode();
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
}));
|
|
64
109
|
// Raw HTML pasted in as Milkdown's html chip becomes the plain text it
|
|
65
110
|
// shows, so a paste can never add markup to the Markdown.
|
|
66
111
|
function htmlToText(fragment) {
|
|
@@ -78,7 +123,9 @@ function htmlToText(fragment) {
|
|
|
78
123
|
return Fragment.fromArray(nodes);
|
|
79
124
|
}
|
|
80
125
|
function headingView(node) {
|
|
81
|
-
const
|
|
126
|
+
const level = Number(node.attrs.level);
|
|
127
|
+
const dom = document.createElement(`h${Math.min(level + 2, 6)}`);
|
|
128
|
+
dom.className = headingClass(level);
|
|
82
129
|
return {
|
|
83
130
|
dom,
|
|
84
131
|
contentDOM: dom,
|
|
@@ -217,6 +264,9 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
217
264
|
const slashMenuRef = useRef(null);
|
|
218
265
|
// Opens the link box: made with the editor, used by the toolbar's Link.
|
|
219
266
|
const openLinkRef = useRef(() => { });
|
|
267
|
+
// The node views' slots (a callout's icon, a table's bars), each
|
|
268
|
+
// rendered into through a portal.
|
|
269
|
+
const [widgets, setWidgets] = useState([]);
|
|
220
270
|
// The latest props, for the editor's callbacks, which are set up once.
|
|
221
271
|
const latest = useRef({ onChange, onProblem, readOnly, placeholder });
|
|
222
272
|
useEffect(() => {
|
|
@@ -306,6 +356,33 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
306
356
|
transformPasted: (slice) => new Slice(htmlToText(slice.content), slice.openStart, slice.openEnd),
|
|
307
357
|
},
|
|
308
358
|
}));
|
|
359
|
+
// The node views' slots, by slot; React hears of a change once per
|
|
360
|
+
// task, however many node views changed in it.
|
|
361
|
+
const slots = new Map();
|
|
362
|
+
let syncQueued = false;
|
|
363
|
+
const sync = () => {
|
|
364
|
+
if (syncQueued)
|
|
365
|
+
return;
|
|
366
|
+
syncQueued = true;
|
|
367
|
+
queueMicrotask(() => {
|
|
368
|
+
syncQueued = false;
|
|
369
|
+
if (!cancelled)
|
|
370
|
+
setWidgets([...slots.values()]);
|
|
371
|
+
});
|
|
372
|
+
};
|
|
373
|
+
const registry = {
|
|
374
|
+
set: (widget) => {
|
|
375
|
+
const before = slots.get(widget.slot);
|
|
376
|
+
if (before && before.type === widget.type && (before.type !== 'callout' || before.kind === widget.kind))
|
|
377
|
+
return;
|
|
378
|
+
slots.set(widget.slot, widget);
|
|
379
|
+
sync();
|
|
380
|
+
},
|
|
381
|
+
remove: (slot) => {
|
|
382
|
+
if (slots.delete(slot))
|
|
383
|
+
sync();
|
|
384
|
+
},
|
|
385
|
+
};
|
|
309
386
|
// Opens the link box at the start of what the link applies to.
|
|
310
387
|
const openLink = (view) => {
|
|
311
388
|
if (!view.editable)
|
|
@@ -428,47 +505,66 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
428
505
|
.config((ctx) => {
|
|
429
506
|
ctx.set(rootCtx, root);
|
|
430
507
|
ctx.set(defaultValueCtx, original);
|
|
431
|
-
ctx.update(remarkStringifyOptionsCtx, (options) => ({
|
|
508
|
+
ctx.update(remarkStringifyOptionsCtx, (options) => ({
|
|
509
|
+
...options,
|
|
510
|
+
...markdownStyle(original),
|
|
511
|
+
handlers: { ...options.handlers, callout: calloutHandler },
|
|
512
|
+
}));
|
|
432
513
|
ctx.update(editorViewOptionsCtx, (options) => ({
|
|
433
514
|
...options,
|
|
434
515
|
editable: () => !latest.current.readOnly,
|
|
435
516
|
attributes,
|
|
436
|
-
// The slash menu's keys,
|
|
437
|
-
//
|
|
517
|
+
// The slash menu's keys, the editor's own shortcuts and its table
|
|
518
|
+
// keys, as view props: before Milkdown's own keymap, whose Enter
|
|
519
|
+
// would split the line (or leave a table).
|
|
438
520
|
handleKeyDown: (view, event) => {
|
|
439
521
|
const menu = slashMenuRef.current;
|
|
440
|
-
if (!menu)
|
|
441
|
-
return false;
|
|
442
|
-
const step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;
|
|
443
522
|
const plain = !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey;
|
|
444
|
-
if (
|
|
445
|
-
event.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
523
|
+
if (menu) {
|
|
524
|
+
const step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;
|
|
525
|
+
if (step && plain) {
|
|
526
|
+
event.preventDefault();
|
|
527
|
+
const count = menu.items.length;
|
|
528
|
+
setSlashActive({ key: menu.key, index: (menu.index + step + count) % count });
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
if ((event.key === 'Enter' || event.key === 'Tab') && plain) {
|
|
532
|
+
event.preventDefault();
|
|
533
|
+
applySlash(view, menu.span, menu.items[menu.index].id);
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
// Esc closes the menu only: the field's own Esc (editing in
|
|
537
|
+
// place gives up the edit) waits for the next one.
|
|
538
|
+
if (event.key === 'Escape') {
|
|
539
|
+
event.preventDefault();
|
|
540
|
+
view.dispatch(closeSlash(view.state));
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
449
543
|
}
|
|
450
|
-
if (
|
|
451
|
-
|
|
452
|
-
|
|
544
|
+
if (!view.editable)
|
|
545
|
+
return false;
|
|
546
|
+
if (handleShortcut(view.state, view.dispatch, event))
|
|
453
547
|
return true;
|
|
454
|
-
|
|
455
|
-
// Esc closes the menu only: the field's own Esc (editing in
|
|
456
|
-
// place gives up the edit) waits for the next one.
|
|
457
|
-
if (event.key === 'Escape') {
|
|
458
|
-
event.preventDefault();
|
|
459
|
-
view.dispatch(closeSlash(view.state));
|
|
548
|
+
if (handleTableKey(view.state, view.dispatch, event))
|
|
460
549
|
return true;
|
|
461
|
-
|
|
462
|
-
|
|
550
|
+
// Ctrl/Cmd+Enter in a table is the field's (a save), not
|
|
551
|
+
// Milkdown's "leave the table": taken here, so its keymap never
|
|
552
|
+
// sees it, and left to reach the field.
|
|
553
|
+
const mod = event.metaKey || event.ctrlKey;
|
|
554
|
+
return mod && !event.shiftKey && !event.altKey && event.key === 'Enter' && isInTable(view.state);
|
|
463
555
|
},
|
|
464
556
|
nodeViews: {
|
|
465
557
|
heading: headingView,
|
|
558
|
+
callout: (node, _view, getPos) => calloutView(node, getPos, registry),
|
|
559
|
+
table: (_node, _view, getPos) => tableView(getPos, registry),
|
|
466
560
|
list_item: (node, view, getPos) => node.attrs.checked == null ? undefined : taskItemView(node, view, getPos),
|
|
467
561
|
},
|
|
468
562
|
}));
|
|
469
563
|
})
|
|
470
564
|
.use(COMMONMARK)
|
|
471
565
|
.use(gfm)
|
|
566
|
+
.use(calloutRemark)
|
|
567
|
+
.use(calloutSchema)
|
|
472
568
|
.use(history)
|
|
473
569
|
.use(changes)
|
|
474
570
|
.use(slashing)
|
|
@@ -523,6 +619,53 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
523
619
|
view.focus();
|
|
524
620
|
});
|
|
525
621
|
};
|
|
622
|
+
// The editable element: where a menu over the text gives focus back
|
|
623
|
+
// (ProseMirror puts the caret back where it was as it takes it).
|
|
624
|
+
const textElement = () => editorView?.dom ?? null;
|
|
625
|
+
// A Table menu item, on the caret's table (the menu gives focus back).
|
|
626
|
+
const runTable = (action) => {
|
|
627
|
+
if (editorView)
|
|
628
|
+
runTableAction(editorView.state, editorView.dispatch, action);
|
|
629
|
+
};
|
|
630
|
+
// A callout's menu: its kind changed, or the callout taken away (its
|
|
631
|
+
// blocks kept). The menu gives focus back to the text.
|
|
632
|
+
const changeCallout = (getPos, kind) => {
|
|
633
|
+
if (!editorView)
|
|
634
|
+
return;
|
|
635
|
+
const pos = getPos();
|
|
636
|
+
const node = pos === undefined ? null : editorView.state.doc.nodeAt(pos);
|
|
637
|
+
if (pos === undefined || node?.type.name !== 'callout')
|
|
638
|
+
return;
|
|
639
|
+
const tr = editorView.state.tr;
|
|
640
|
+
if (kind)
|
|
641
|
+
tr.setNodeMarkup(pos, undefined, { ...node.attrs, kind });
|
|
642
|
+
else
|
|
643
|
+
tr.replaceWith(pos, pos + node.nodeSize, node.content).setSelection(TextSelection.near(tr.doc.resolve(pos + 1)));
|
|
644
|
+
editorView.dispatch(tr);
|
|
645
|
+
};
|
|
646
|
+
// A table's bar: the caret into its last cell, then a row at its end
|
|
647
|
+
// (the caret into its first cell) or a column at its right (the caret
|
|
648
|
+
// into its header).
|
|
649
|
+
const addToTable = (getPos, side) => {
|
|
650
|
+
if (!editorView)
|
|
651
|
+
return;
|
|
652
|
+
const pos = getPos();
|
|
653
|
+
const table = pos === undefined ? null : editorView.state.doc.nodeAt(pos);
|
|
654
|
+
if (pos === undefined || table?.type.name !== 'table')
|
|
655
|
+
return;
|
|
656
|
+
const last = TextSelection.near(editorView.state.doc.resolve(pos + table.nodeSize - 1), -1);
|
|
657
|
+
editorView.dispatch(editorView.state.tr.setSelection(last));
|
|
658
|
+
if (side === 'row')
|
|
659
|
+
addRowAtEnd(editorView.state, editorView.dispatch);
|
|
660
|
+
else if (runTableAction(editorView.state, editorView.dispatch, 'columnRight')) {
|
|
661
|
+
const added = editorView.state.doc.nodeAt(pos);
|
|
662
|
+
const map = TableMap.get(added);
|
|
663
|
+
const header = TextSelection.near(editorView.state.doc.resolve(pos + 1 + map.map[map.width - 1] + 1));
|
|
664
|
+
editorView.dispatch(editorView.state.tr.setSelection(header).scrollIntoView());
|
|
665
|
+
}
|
|
666
|
+
editorView.focus();
|
|
667
|
+
};
|
|
668
|
+
const caretTable = editorState ? tableAt(editorState)?.pos : undefined;
|
|
526
669
|
// The floating toolbar: words selected, focus in the field, the mouse up,
|
|
527
670
|
// and nothing else open over the text.
|
|
528
671
|
const selection = editorState?.selection;
|
|
@@ -551,7 +694,7 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
551
694
|
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: setField, "data-slot": "rich-text-editor", className: cn('relative flex w-full min-w-0 flex-col rounded-lg border border-input bg-transparent type-body text-foreground break-words transition-[border-color,box-shadow]', 'focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 dark:bg-input/30', 'has-[[aria-invalid=true]]:border-destructive has-[[aria-invalid=true]]:ring-destructive/20', className), children: [toolbar && (_jsx(RichTextToolbar, { ref: toolbarRef, state: editorState, readOnly: readOnly,
|
|
552
695
|
// Only once the editor is made: until then the editable element,
|
|
553
696
|
// and its id, aren't in the page (axe: aria-valid-attr-value).
|
|
554
|
-
controls: editorState ? editableId : undefined, onFormat: runFormat })), _jsx("div", { ref: rootRef, className: cn('min-h-24 px-2.5 py-2',
|
|
697
|
+
controls: editorState ? editableId : undefined, onFormat: runFormat, onTable: runTable, text: textElement })), _jsx("div", { ref: rootRef, className: cn('min-h-24 px-2.5 py-2',
|
|
555
698
|
// Read-only text is muted; the toolbar's buttons say so by being disabled.
|
|
556
699
|
'has-[[aria-readonly=true]]:text-muted-foreground',
|
|
557
700
|
// The same type roles <Markdown> uses, so text reads the same
|
|
@@ -559,11 +702,26 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
559
702
|
'[&_.ProseMirror]:flex [&_.ProseMirror]:min-h-20 [&_.ProseMirror]:flex-col [&_.ProseMirror]:gap-3',
|
|
560
703
|
// ProseMirror's own required style (prosemirror-view's CSS): spaces
|
|
561
704
|
// as typed, so one at the end of a line isn't kept as .
|
|
562
|
-
'[&_.ProseMirror]:whitespace-pre-wrap [&_.ProseMirror]:break-words', '[&
|
|
705
|
+
'[&_.ProseMirror]:whitespace-pre-wrap [&_.ProseMirror]:break-words', '[&_ul]:flex [&_ul]:list-disc [&_ul]:flex-col [&_ul]:gap-1 [&_ul]:pl-6', '[&_ol]:flex [&_ol]:list-decimal [&_ol]:flex-col [&_ol]:gap-1 [&_ol]:pl-6', '[&_li[data-item-type=task]]:-ml-6 [&_li[data-item-type=task]]:flex [&_li[data-item-type=task]]:list-none [&_li[data-item-type=task]]:items-start [&_li[data-item-type=task]]:gap-2', '[&_li[data-item-type=task]>input]:mt-1 [&_li[data-item-type=task]>input]:size-4 [&_li[data-item-type=task]>input]:accent-primary',
|
|
706
|
+
// Headings, callouts and tables are drawn by their node views;
|
|
707
|
+
// these are the elements Milkdown draws itself.
|
|
708
|
+
'[&_a]:text-link [&_a]:underline [&_a]:decoration-link/40 [&_a]:underline-offset-4', '[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground', '[&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:font-mono', '[&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:font-mono', '[&_hr]:my-2 [&_hr]:h-0.5 [&_hr]:rounded-full [&_hr]:border-0 [&_hr]:bg-rule', '[&_hr.ProseMirror-selectednode]:outline-2 [&_hr.ProseMirror-selectednode]:outline-offset-4 [&_hr.ProseMirror-selectednode]:outline-ring',
|
|
709
|
+
// A table: a header row on the muted fill, a line between every
|
|
710
|
+
// cell, cells that wrap and never shrink to nothing; selected
|
|
711
|
+
// cells (a drag across them) tinted.
|
|
712
|
+
'[&_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',
|
|
563
713
|
// Raw HTML from the text: shown as its characters, never as markup.
|
|
564
714
|
'[&_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',
|
|
565
715
|
// The placeholder, in an empty document only (a decoration).
|
|
566
|
-
'[&_p[data-placeholder]]:before:pointer-events-none [&_p[data-placeholder]]:before:float-left [&_p[data-placeholder]]:before:h-0 [&_p[data-placeholder]]:before:text-muted-foreground [&_p[data-placeholder]]:before:content-[attr(data-placeholder)]') }),
|
|
716
|
+
'[&_p[data-placeholder]]:before:pointer-events-none [&_p[data-placeholder]]:before:float-left [&_p[data-placeholder]]:before:h-0 [&_p[data-placeholder]]:before:text-muted-foreground [&_p[data-placeholder]]:before:content-[attr(data-placeholder)]') }), widgets.map((widget) => {
|
|
717
|
+
if (widget.type === 'callout')
|
|
718
|
+
return createPortal(_jsx(CalloutKindMenu, { kind: widget.kind, readOnly: readOnly, onPick: (kind) => changeCallout(widget.getPos, kind), onRemove: () => changeCallout(widget.getPos, null), text: textElement }), widget.slot, widget.key);
|
|
719
|
+
const active = caretTable !== undefined && caretTable === widget.getPos();
|
|
720
|
+
return [
|
|
721
|
+
createPortal(_jsx(TableBar, { side: "row", active: active, readOnly: readOnly, onAdd: () => addToTable(widget.getPos, 'row') }), widget.slot, `${widget.key}-row`),
|
|
722
|
+
createPortal(_jsx(TableBar, { side: "column", active: active, readOnly: readOnly, onAdd: () => addToTable(widget.getPos, 'column') }), widget.column, `${widget.key}-column`),
|
|
723
|
+
];
|
|
724
|
+
}), 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) })), floatingOpen && editorView && editorState && (_jsx(RichTextFloatingToolbar, { ref: floatingRef, view: editorView, state: editorState, field: field, controls: editableId, onFormat: runFormat, onDismiss: () => setDismissed({ from: editorState.selection.from, to: editorState.selection.to }), onReturn: returnToText, onBlur: (event) => {
|
|
567
725
|
const next = event.relatedTarget;
|
|
568
726
|
if (!editorView.dom.contains(next) && !floatingRef.current?.contains(next))
|
|
569
727
|
setFocused(false);
|