@tristan2828/ui-foundation 3.20.1 → 3.21.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 +3 -3
- package/conventions/docs/cell-patterns.md +13 -4
- package/conventions/docs/design-language.md +22 -0
- 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/ui/badge.d.ts +1 -1
- package/dist/components/ui/dropdown-menu.d.ts +29 -0
- package/dist/components/ui/dropdown-menu.js +51 -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 +24 -0
|
@@ -275,9 +275,9 @@ is a filled-in example while the Widgets demo is still here).
|
|
|
275
275
|
`<a target="_blank" rel="noreferrer">` with an `ExternalLinkIcon`
|
|
276
276
|
and a visually hidden "(opens in a new tab)".
|
|
277
277
|
- **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
|
|
278
|
+
foundation's `<Markdown>`: headings, lists, tables, callouts
|
|
279
|
+
(`> [!NOTE]`) and links, links opening in a new tab; raw HTML shows
|
|
280
|
+
as the characters typed, never as markup. The app needs no Markdown dependency of its own. Plain
|
|
281
281
|
long text renders as text (`whitespace-pre-line` keeps its line
|
|
282
282
|
breaks).
|
|
283
283
|
- **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**
|
|
@@ -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);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type LucideIcon } from 'lucide-react';
|
|
2
|
+
import type { CalloutKind } from '../../lib/markdown-callout.js';
|
|
3
|
+
/**
|
|
4
|
+
* Classes for written text's own elements. No colours on the headings:
|
|
5
|
+
* <Markdown> adds the text colour, and the editor's read-only text is
|
|
6
|
+
* muted as a whole.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PROSE: {
|
|
9
|
+
/** `#`: the largest, with a rule under it, so it reads as the top of the outline. */
|
|
10
|
+
readonly heading1: "type-heading-1 mt-4 border-b border-border pb-1.5 first:mt-0";
|
|
11
|
+
readonly heading2: "type-heading-2 mt-3 first:mt-0";
|
|
12
|
+
readonly heading3: "type-heading-3 mt-1 first:mt-0";
|
|
13
|
+
/** `####` and deeper: a label, as before. */
|
|
14
|
+
readonly heading4: "type-label mt-1 first:mt-0";
|
|
15
|
+
/** Coloured as well as underlined. */
|
|
16
|
+
readonly link: "text-link underline decoration-link/40 underline-offset-4 transition-colors hover:decoration-link";
|
|
17
|
+
/** `---`: a 2px line in the rule tone, with room around it. */
|
|
18
|
+
readonly rule: "my-2 h-0.5 rounded-full border-0 bg-rule";
|
|
19
|
+
};
|
|
20
|
+
/** A heading's classes by its Markdown level (1 for `#`). */
|
|
21
|
+
export declare function headingClass(level: number): string;
|
|
22
|
+
export type CalloutLook = {
|
|
23
|
+
label: string;
|
|
24
|
+
icon: LucideIcon;
|
|
25
|
+
/** The box's wash and border. */
|
|
26
|
+
box: string;
|
|
27
|
+
/** The icon's tint. */
|
|
28
|
+
tint: string;
|
|
29
|
+
};
|
|
30
|
+
export declare const CALLOUT_LOOK: Record<CalloutKind, CalloutLook>;
|
|
31
|
+
/** The box every callout shares: the icon in a column, the text beside it. */
|
|
32
|
+
export declare const CALLOUT_BOX = "flex gap-2.5 rounded-lg border px-3 py-2.5";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// How written text looks, the same in <Markdown> and in the rich-text
|
|
2
|
+
// editor: its headings, links and rules (PROSE), and each kind of callout
|
|
3
|
+
// (src/lib/markdown-callout.ts says what one is): its name, its icon, and
|
|
4
|
+
// its colours. A callout is a wash of the kind's tone behind ordinary text,
|
|
5
|
+
// the icon in the tone's text shade, so the text keeps the page's
|
|
6
|
+
// contrast and the icon clears 3:1 against the wash. Note is the default
|
|
7
|
+
// (what "Callout" inserts).
|
|
8
|
+
import { InfoIcon, LightbulbIcon, MessageSquareWarningIcon, OctagonAlertIcon, TriangleAlertIcon, } from 'lucide-react';
|
|
9
|
+
/**
|
|
10
|
+
* Classes for written text's own elements. No colours on the headings:
|
|
11
|
+
* <Markdown> adds the text colour, and the editor's read-only text is
|
|
12
|
+
* muted as a whole.
|
|
13
|
+
*/
|
|
14
|
+
export const PROSE = {
|
|
15
|
+
/** `#`: the largest, with a rule under it, so it reads as the top of the outline. */
|
|
16
|
+
heading1: 'type-heading-1 mt-4 border-b border-border pb-1.5 first:mt-0',
|
|
17
|
+
heading2: 'type-heading-2 mt-3 first:mt-0',
|
|
18
|
+
heading3: 'type-heading-3 mt-1 first:mt-0',
|
|
19
|
+
/** `####` and deeper: a label, as before. */
|
|
20
|
+
heading4: 'type-label mt-1 first:mt-0',
|
|
21
|
+
/** Coloured as well as underlined. */
|
|
22
|
+
link: 'text-link underline decoration-link/40 underline-offset-4 transition-colors hover:decoration-link',
|
|
23
|
+
/** `---`: a 2px line in the rule tone, with room around it. */
|
|
24
|
+
rule: 'my-2 h-0.5 rounded-full border-0 bg-rule',
|
|
25
|
+
};
|
|
26
|
+
/** A heading's classes by its Markdown level (1 for `#`). */
|
|
27
|
+
export function headingClass(level) {
|
|
28
|
+
return level === 1 ? PROSE.heading1 : level === 2 ? PROSE.heading2 : level === 3 ? PROSE.heading3 : PROSE.heading4;
|
|
29
|
+
}
|
|
30
|
+
export const CALLOUT_LOOK = {
|
|
31
|
+
note: { label: 'Note', icon: InfoIcon, box: 'bg-info/10 border-info/25', tint: 'text-info-text' },
|
|
32
|
+
tip: { label: 'Tip', icon: LightbulbIcon, box: 'bg-success/10 border-success/25', tint: 'text-success-text' },
|
|
33
|
+
important: {
|
|
34
|
+
label: 'Important',
|
|
35
|
+
icon: MessageSquareWarningIcon,
|
|
36
|
+
box: 'bg-category-2/10 border-category-2/25',
|
|
37
|
+
tint: 'text-category-2',
|
|
38
|
+
},
|
|
39
|
+
warning: { label: 'Warning', icon: TriangleAlertIcon, box: 'bg-warning/10 border-warning/25', tint: 'text-warning-text' },
|
|
40
|
+
caution: {
|
|
41
|
+
label: 'Caution',
|
|
42
|
+
icon: OctagonAlertIcon,
|
|
43
|
+
box: 'bg-destructive/10 border-destructive/25',
|
|
44
|
+
tint: 'text-destructive-text',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
/** The box every callout shares: the icon in a column, the text beside it. */
|
|
48
|
+
export const CALLOUT_BOX = 'flex gap-2.5 rounded-lg border px-3 py-2.5';
|
|
@@ -4,9 +4,21 @@ export type FormatItem = {
|
|
|
4
4
|
id: FormatId;
|
|
5
5
|
label: string;
|
|
6
6
|
icon: LucideIcon;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Its shortcut, as `aria-keyshortcuts` writes it with Mod for Ctrl/Cmd,
|
|
9
|
+
* and Block for Notion's block keys (Ctrl+Shift, or Cmd+Option on a Mac).
|
|
10
|
+
*/
|
|
8
11
|
keys?: string;
|
|
9
12
|
/** More for the tooltip than the shortcut. */
|
|
10
13
|
hint?: string;
|
|
11
14
|
};
|
|
12
15
|
export declare const FORMAT_GROUPS: FormatItem[][];
|
|
16
|
+
/** Every format by its id. */
|
|
17
|
+
export declare const FORMAT_ITEMS: Map<FormatId, FormatItem>;
|
|
18
|
+
/**
|
|
19
|
+
* A callout of each kind but the default, for the slash menu: "Warning
|
|
20
|
+
* callout". Not on the toolbar (its Callout makes a note).
|
|
21
|
+
*/
|
|
22
|
+
export declare const CALLOUT_VARIANTS: FormatItem[];
|
|
23
|
+
/** Notion's block keys: the digit after Ctrl+Shift (Cmd+Option on a Mac), and its format. */
|
|
24
|
+
export declare const BLOCK_KEYS: Record<string, FormatId>;
|