@tristan2828/ui-foundation 4.31.0 → 4.33.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/AGENTS.md +2 -1
- package/conventions/docs/add-an-entity.md +13 -2
- package/conventions/docs/cell-patterns.md +8 -0
- package/conventions/docs/design-language.md +97 -6
- package/dist/components/app/data-table.d.ts +21 -1
- package/dist/components/app/data-table.js +20 -4
- package/dist/components/app/list-editor-rows.d.ts +18 -0
- package/dist/components/app/list-editor-rows.js +23 -0
- package/dist/components/app/list-editor.d.ts +25 -3
- package/dist/components/app/list-editor.js +61 -5
- package/dist/components/app/markdown.d.ts +10 -2
- package/dist/components/app/markdown.js +80 -11
- package/dist/components/app/milkdown-editor.d.ts +1 -1
- package/dist/components/app/milkdown-editor.js +19 -10
- package/dist/components/app/module-card.d.ts +53 -0
- package/dist/components/app/module-card.js +95 -0
- package/dist/components/app/prose-look.d.ts +26 -6
- package/dist/components/app/prose-look.js +24 -6
- package/dist/components/app/prose-table-scroll.d.ts +33 -0
- package/dist/components/app/prose-table-scroll.js +87 -0
- package/dist/components/app/rich-text-editor.d.ts +10 -0
- package/dist/components/app/rich-text-editor.js +5 -0
- package/dist/components/app/rich-text-node-views.d.ts +6 -3
- package/dist/components/app/rich-text-node-views.js +108 -5
- package/dist/components/app/widget-card.js +13 -4
- package/dist/components/ui/collapsible.d.ts +5 -0
- package/dist/components/ui/collapsible.js +12 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/package.json +1 -1
- package/styles/index.css +14 -0
- package/styles/theme.css +5 -0
|
@@ -1,11 +1,50 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Long text written as Markdown, rendered to read: headings, lists, links,
|
|
3
|
+
// tables (GitHub-flavoured), code and quotes, in the design language's
|
|
4
|
+
// type roles and tokens. A record's notes on its view (EntityView) are the
|
|
5
|
+
// first use.
|
|
6
|
+
//
|
|
7
|
+
// Safe by default, and kept that way on purpose:
|
|
8
|
+
// - Raw HTML is never rendered. react-markdown escapes it, so `<b>` in the
|
|
9
|
+
// text shows as the characters `<b>`, never as markup, and nothing the
|
|
10
|
+
// author typed silently disappears. Never add rehype-raw.
|
|
11
|
+
// - URLs go through react-markdown's default urlTransform, which drops
|
|
12
|
+
// `javascript:` and other unsafe protocols. A link left with no URL
|
|
13
|
+
// renders as its text.
|
|
14
|
+
// - Links open in a new tab, so following one never leaves the record.
|
|
15
|
+
//
|
|
16
|
+
// Headings start at <h3> by default: the page owns the <h1> (the
|
|
17
|
+
// record's title) and each section its <h2>, so a `# Heading` in the text
|
|
18
|
+
// sits under them instead of competing with the page's own outline. Text
|
|
19
|
+
// that sits elsewhere says so with `headingLevel`, the level `#` renders
|
|
20
|
+
// as (2 straight under the page's <h1>), each level below it one deeper,
|
|
21
|
+
// never past <h6>, so the page's outline never skips a level (axe's
|
|
22
|
+
// heading-order). They look like what they are in the text, wherever they
|
|
23
|
+
// sit: `#` and `##` on a tinted band, `###` a small uppercase label, and
|
|
24
|
+
// the text under them lined up with the words in the band (prose-look.ts).
|
|
25
|
+
//
|
|
26
|
+
// A table wider than the text scrolls sideways, and while it does, its
|
|
27
|
+
// box is a tab stop, named for its columns, that the arrow keys scroll
|
|
28
|
+
// (prose-table-scroll.ts). Its cells wrap first, so that's only a table
|
|
29
|
+
// with many columns, or a long word in one.
|
|
30
|
+
//
|
|
31
|
+
// An image (``, one the rich-text editor uploaded) stays inside
|
|
32
|
+
// the text's width, loads only as it nears the screen, and keeps its alt
|
|
33
|
+
// text; with none (``) it's decorative to a screen reader. Its
|
|
34
|
+
// address goes through the same urlTransform: a web address or a path on
|
|
35
|
+
// the site (`/api/files/7/content`), never `javascript:` or `data:`.
|
|
36
|
+
//
|
|
37
|
+
// A quote that starts `[!NOTE]` (GitHub's alert syntax) is a callout: a
|
|
38
|
+
// tinted box with its kind's icon (src/lib/markdown-callout.ts).
|
|
39
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
2
40
|
import ReactMarkdown, {} from 'react-markdown';
|
|
3
41
|
import remarkGfm from 'remark-gfm';
|
|
4
42
|
import { cn } from 'cn';
|
|
5
43
|
import { ExternalLinkIcon, SquareCheckIcon, SquareIcon } from 'lucide-react';
|
|
6
44
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table.js';
|
|
7
45
|
import { CALLOUT_KINDS, remarkCallouts } from '../../lib/markdown-callout.js';
|
|
8
|
-
import { CALLOUT_BOX, CALLOUT_LOOK, headingClass, PROSE, PROSE_INSET } from './prose-look.js';
|
|
46
|
+
import { CALLOUT_BOX, CALLOUT_LOOK, DEFAULT_HEADING_LEVEL, headingBand, headingClass, headingTag, PROSE, PROSE_INSET, } from './prose-look.js';
|
|
47
|
+
import { headerTexts, TABLE_SCROLL_FOCUS, watchTableScroll } from './prose-table-scroll.js';
|
|
9
48
|
const REMARK_PLUGINS = [remarkGfm, remarkCallouts];
|
|
10
49
|
function isCalloutKind(value) {
|
|
11
50
|
return CALLOUT_KINDS.includes(value);
|
|
@@ -17,14 +56,31 @@ function omitNode(props) {
|
|
|
17
56
|
delete rest.node;
|
|
18
57
|
return rest;
|
|
19
58
|
}
|
|
20
|
-
//
|
|
59
|
+
// A GFM table in the package's own table, in its box that scrolls
|
|
60
|
+
// sideways: a tab stop and a named region only while the table overflows
|
|
61
|
+
// it. Watched again after each render, so a header row with new words
|
|
62
|
+
// renames it.
|
|
63
|
+
function NotesTable(props) {
|
|
64
|
+
const boxRef = useRef(null);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const box = boxRef.current;
|
|
67
|
+
if (!box)
|
|
68
|
+
return;
|
|
69
|
+
const table = box.querySelector('table');
|
|
70
|
+
const region = watchTableScroll(box, table, headerTexts(table));
|
|
71
|
+
return () => region.destroy();
|
|
72
|
+
});
|
|
73
|
+
return (_jsx(Table, { ...props, containerRef: boxRef, containerClassName: cn('rounded-lg border border-border', TABLE_SCROLL_FOCUS) }));
|
|
74
|
+
}
|
|
75
|
+
// A heading by its Markdown level: the element the page's outline gives
|
|
76
|
+
// it (headingTag), the look its level has. The text's colour first, so a
|
|
77
|
+
// level's own (`###`'s muted label) wins.
|
|
78
|
+
function heading(level, top) {
|
|
79
|
+
const Tag = headingTag(level, top);
|
|
80
|
+
return (props) => (_jsx(Tag, { ...omitNode(props), "data-band": headingBand(level) || undefined, className: cn('text-foreground', headingClass(level)) }));
|
|
81
|
+
}
|
|
82
|
+
// Everything but the headings, which follow `headingLevel` (componentsFor).
|
|
21
83
|
const COMPONENTS = {
|
|
22
|
-
h1: (props) => _jsx("h3", { ...omitNode(props), className: cn('text-foreground', headingClass(1)) }),
|
|
23
|
-
h2: (props) => _jsx("h4", { ...omitNode(props), className: cn('text-foreground', headingClass(2)) }),
|
|
24
|
-
h3: (props) => _jsx("h5", { ...omitNode(props), className: cn('text-foreground', headingClass(3)) }),
|
|
25
|
-
h4: (props) => _jsx("h6", { ...omitNode(props), className: cn('text-foreground', headingClass(4)) }),
|
|
26
|
-
h5: (props) => _jsx("h6", { ...omitNode(props), className: cn('text-foreground', headingClass(5)) }),
|
|
27
|
-
h6: (props) => _jsx("h6", { ...omitNode(props), className: cn('text-foreground', headingClass(6)) }),
|
|
28
84
|
a: (props) => {
|
|
29
85
|
const { href, children, ...rest } = omitNode(props);
|
|
30
86
|
if (!href)
|
|
@@ -65,7 +121,7 @@ const COMPONENTS = {
|
|
|
65
121
|
},
|
|
66
122
|
// GFM tables use the package's own table, zebra stripes and density
|
|
67
123
|
// tokens included, so a table in the notes reads like every other table.
|
|
68
|
-
table: (props) => _jsx(
|
|
124
|
+
table: (props) => _jsx(NotesTable, { ...omitNode(props) }),
|
|
69
125
|
thead: (props) => _jsx(TableHeader, { ...omitNode(props), className: "bg-muted" }),
|
|
70
126
|
tbody: (props) => _jsx(TableBody, { ...omitNode(props) }),
|
|
71
127
|
tr: (props) => _jsx(TableRow, { ...omitNode(props) }),
|
|
@@ -74,6 +130,19 @@ const COMPONENTS = {
|
|
|
74
130
|
th: (props) => _jsx(TableHead, { ...omitNode(props), className: "border-r border-border whitespace-normal last:border-r-0" }),
|
|
75
131
|
td: (props) => _jsx(TableCell, { ...omitNode(props), className: "border-r border-border whitespace-normal last:border-r-0" }),
|
|
76
132
|
};
|
|
77
|
-
|
|
78
|
-
|
|
133
|
+
// The components for each `headingLevel`, made once each.
|
|
134
|
+
const BY_LEVEL = new Map();
|
|
135
|
+
function componentsFor(top) {
|
|
136
|
+
let components = BY_LEVEL.get(top);
|
|
137
|
+
if (!components) {
|
|
138
|
+
components = { ...COMPONENTS };
|
|
139
|
+
for (const level of [1, 2, 3, 4, 5, 6])
|
|
140
|
+
components[`h${level}`] = heading(level, top);
|
|
141
|
+
BY_LEVEL.set(top, components);
|
|
142
|
+
}
|
|
143
|
+
return components;
|
|
144
|
+
}
|
|
145
|
+
export function Markdown({ children, headingLevel = DEFAULT_HEADING_LEVEL, className, ...rest }) {
|
|
146
|
+
const components = useMemo(() => componentsFor(headingLevel), [headingLevel]);
|
|
147
|
+
return (_jsx("div", { "data-slot": "markdown", ...rest, className: cn('flex min-w-0 flex-col gap-3 type-body text-foreground break-words', PROSE_INSET, className), children: _jsx(ReactMarkdown, { remarkPlugins: REMARK_PLUGINS, components: components, children: children }) }));
|
|
79
148
|
}
|
|
@@ -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, 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, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, }: RichTextEditorProps): import("react").JSX.Element;
|
|
@@ -13,11 +13,15 @@ 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
|
|
17
|
-
//
|
|
16
|
+
// <h2>. `headingLevel` moves them to where the field sits in the page's
|
|
17
|
+
// outline, as it does <Markdown>'s; only the element moves, never the
|
|
18
|
+
// level the Markdown is saved with (a node view draws it, from the
|
|
19
|
+
// node's own level). They look like <Markdown>'s (prose-look.ts). A
|
|
20
|
+
// task-list item renders a real checkbox that toggles it.
|
|
18
21
|
// - Callouts (`> [!NOTE]`, src/lib/markdown-callout.ts): a node of their
|
|
19
22
|
// own, drawn as <Markdown> draws them, the icon a menu of kinds.
|
|
20
|
-
// - Tables in a box that scrolls sideways
|
|
23
|
+
// - Tables in a box that scrolls sideways (a tab stop the arrow keys
|
|
24
|
+
// scroll while it does: rich-text-node-views.ts), with Notion's "add a row" and
|
|
21
25
|
// "add a column" bars, Tab in the last cell adding a row and Enter going
|
|
22
26
|
// down a column (rich-text-table.ts); the toolbar's Table menu does the
|
|
23
27
|
// rest.
|
|
@@ -66,7 +70,7 @@ import { linkHref, linkRefusal } from '../../lib/link-href.js';
|
|
|
66
70
|
import { passEscapeThrough } from './escape-passthrough.js';
|
|
67
71
|
import { calloutHandler, remarkCalloutNodes } from '../../lib/markdown-callout.js';
|
|
68
72
|
import { MarkdownMergeError, markdownStyle, mergeMarkdown } from '../../lib/markdown-merge.js';
|
|
69
|
-
import { headingClass } from './prose-look.js';
|
|
73
|
+
import { DEFAULT_HEADING_LEVEL, headingBand, headingClass, headingTag } from './prose-look.js';
|
|
70
74
|
import { RichTextFloatingToolbar } from './rich-text-floating-toolbar.js';
|
|
71
75
|
import { tableAt, toggleFormat } from './rich-text-formats.js';
|
|
72
76
|
import { calloutKind, calloutView, tableView } from './rich-text-node-views.js';
|
|
@@ -138,10 +142,14 @@ function htmlToText(fragment) {
|
|
|
138
142
|
});
|
|
139
143
|
return Fragment.fromArray(nodes);
|
|
140
144
|
}
|
|
141
|
-
|
|
145
|
+
// A heading: the element `headingLevel` gives its level, the look its
|
|
146
|
+
// level has. Marked as a band for PROSE_INSET (`#` and `##`).
|
|
147
|
+
function headingView(node, top) {
|
|
142
148
|
const level = Number(node.attrs.level);
|
|
143
|
-
const dom = document.createElement(
|
|
149
|
+
const dom = document.createElement(headingTag(level, top));
|
|
144
150
|
dom.className = headingClass(level);
|
|
151
|
+
if (headingBand(level))
|
|
152
|
+
dom.dataset.band = '';
|
|
145
153
|
return {
|
|
146
154
|
dom,
|
|
147
155
|
contentDOM: dom,
|
|
@@ -253,7 +261,7 @@ function applyLink(view, target, href) {
|
|
|
253
261
|
}
|
|
254
262
|
view.dispatch(tr.scrollIntoView());
|
|
255
263
|
}
|
|
256
|
-
export default function MilkdownEditor({ id, defaultValue, onChange, onProblem, readOnly = false, autoFocus = false, placeholder, maxLength, toolbar = true, inline = false, upload, imageAccept, imageMaxSize, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, }) {
|
|
264
|
+
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, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, }) {
|
|
257
265
|
const rootRef = useRef(null);
|
|
258
266
|
const editorRef = useRef(null);
|
|
259
267
|
const [linkBox, setLinkBox] = useState(null);
|
|
@@ -616,9 +624,10 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
616
624
|
return mod && !event.shiftKey && !event.altKey && event.key === 'Enter' && isInTable(view.state);
|
|
617
625
|
},
|
|
618
626
|
nodeViews: {
|
|
619
|
-
|
|
627
|
+
// The level when it mounts, as with defaultValue.
|
|
628
|
+
heading: (node) => headingView(node, headingLevel),
|
|
620
629
|
callout: (node, _view, getPos) => calloutView(node, getPos, registry),
|
|
621
|
-
table: (
|
|
630
|
+
table: (node, view, getPos) => tableView(node, view, getPos, registry),
|
|
622
631
|
list_item: (node, view, getPos) => node.attrs.checked == null ? undefined : taskItemView(node, view, getPos),
|
|
623
632
|
},
|
|
624
633
|
}));
|
|
@@ -792,7 +801,7 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
792
801
|
'[&_.ProseMirror]:flex [&_.ProseMirror]:flex-col [&_.ProseMirror]:gap-3',
|
|
793
802
|
// PROSE_INSET, on the editable element: with a heading's band
|
|
794
803
|
// in the text, the rest lines up with the words in it.
|
|
795
|
-
'[&_.ProseMirror:has(
|
|
804
|
+
'[&_.ProseMirror:has(>[data-band])>:not([data-band])]:mx-3', !inline && '[&_.ProseMirror]:min-h-20',
|
|
796
805
|
// ProseMirror's own required style (prosemirror-view's CSS): spaces
|
|
797
806
|
// as typed, so one at the end of a line isn't kept as .
|
|
798
807
|
'[&_.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',
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type To } from 'react-router';
|
|
3
|
+
import type { AppError } from '../../api/contracts.js';
|
|
4
|
+
import type { IconComponent } from './icon-component.js';
|
|
5
|
+
/** One of the twelve categorical slots (design-language.md "Categorical colour"). */
|
|
6
|
+
export type CategorySlot = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
|
7
|
+
/** Where a module card's band goes: a route in the app, or another site. */
|
|
8
|
+
export type ModuleCardPlace = {
|
|
9
|
+
to: To;
|
|
10
|
+
href?: never;
|
|
11
|
+
} | {
|
|
12
|
+
href: string;
|
|
13
|
+
to?: never;
|
|
14
|
+
};
|
|
15
|
+
/** A link on a module card: a route in the app (`to`), or another site (`href`, opens in a new tab). */
|
|
16
|
+
export type ModuleCardLink = {
|
|
17
|
+
label: string;
|
|
18
|
+
} & ModuleCardPlace;
|
|
19
|
+
export type ModuleCardProps = ModuleCardPlace & {
|
|
20
|
+
/** The place's name, on the band. */
|
|
21
|
+
title: string;
|
|
22
|
+
/**
|
|
23
|
+
* The place's icon: a Lucide icon, or a brand mark for another site.
|
|
24
|
+
* Decorative: the title names the place.
|
|
25
|
+
*/
|
|
26
|
+
icon: IconComponent;
|
|
27
|
+
/** The slot the icon and the band's wash take. Without one, the icon is muted and the band plain. */
|
|
28
|
+
category?: CategorySlot;
|
|
29
|
+
/** The live number: `5`, or a short string (`'1–10 m'`). Omitted: no number. */
|
|
30
|
+
value?: number | string;
|
|
31
|
+
/** A few words under the number: "open", "due today", "Opens its own app". */
|
|
32
|
+
note?: string;
|
|
33
|
+
/** One link at the foot of the card (hidden on a phone, where the tile is the link). */
|
|
34
|
+
action?: ModuleCardLink;
|
|
35
|
+
/** True while the number loads. */
|
|
36
|
+
isLoading?: boolean;
|
|
37
|
+
error?: AppError | null;
|
|
38
|
+
onRetry?: () => void;
|
|
39
|
+
className?: string;
|
|
40
|
+
};
|
|
41
|
+
export declare function ModuleCard({ title, icon: Icon, category, to, href, value, note, action, isLoading, error, onRetry, className, }: ModuleCardProps): import("react").JSX.Element;
|
|
42
|
+
export interface ModuleStripProps {
|
|
43
|
+
/** Names the strip's navigation landmark: "Places". */
|
|
44
|
+
'aria-label': string;
|
|
45
|
+
/** ModuleCards, one per place. */
|
|
46
|
+
children: ReactNode;
|
|
47
|
+
className?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A hub's places, a ModuleCard each: equal columns of 170px or more that
|
|
51
|
+
* wrap, and three columns of tiles on a phone.
|
|
52
|
+
*/
|
|
53
|
+
export declare function ModuleStrip({ 'aria-label': label, children, className }: ModuleStripProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// A place on a hub page (issue #224): one of the app's modules, or a site
|
|
3
|
+
// outside it, as a small card in a strip across the top of the page.
|
|
4
|
+
//
|
|
5
|
+
// - The band: the place's icon, tinted with its categorical slot, and its
|
|
6
|
+
// name, on a 10% wash of the same slot over the card. The band is the
|
|
7
|
+
// link to the place (`to`, or `href` for another site, which opens in a
|
|
8
|
+
// new tab with the external-link mark).
|
|
9
|
+
// - The body: one live number (`value`, in type-numeric's tabular figures)
|
|
10
|
+
// and a short `note` under it ("open", "due today"). No `value` means no
|
|
11
|
+
// number, not an empty state: a place outside the app has none.
|
|
12
|
+
// - The foot: one `action` link ("Today", "Start").
|
|
13
|
+
//
|
|
14
|
+
// States work like WidgetCard's, for the number only: a skeleton the
|
|
15
|
+
// number's size while loading, and on an error a short line and Retry
|
|
16
|
+
// inside the card, so one failing place never hides the others. The band
|
|
17
|
+
// and the action stay usable in every state.
|
|
18
|
+
//
|
|
19
|
+
// ModuleStrip lays the cards out: equal columns of 170px or more that
|
|
20
|
+
// wrap (six to eight across a desktop), and on a phone three columns of
|
|
21
|
+
// smaller tiles with no action, each tile a whole link (the band link's
|
|
22
|
+
// target stretched over the card) at least 44px tall. Retry sits above
|
|
23
|
+
// that target. data-density="compact" tightens the band and the body.
|
|
24
|
+
import { Children, useId } from 'react';
|
|
25
|
+
import { Link } from 'react-router';
|
|
26
|
+
import { AlertCircleIcon, ExternalLinkIcon } from 'lucide-react';
|
|
27
|
+
import { cn } from 'cn';
|
|
28
|
+
import { Button } from '../ui/button.js';
|
|
29
|
+
import { Card } from '../ui/card.js';
|
|
30
|
+
import { Skeleton } from '../ui/skeleton.js';
|
|
31
|
+
// Whole class names per slot, so Tailwind generates them.
|
|
32
|
+
const ICON_CLASS = {
|
|
33
|
+
1: 'text-category-1',
|
|
34
|
+
2: 'text-category-2',
|
|
35
|
+
3: 'text-category-3',
|
|
36
|
+
4: 'text-category-4',
|
|
37
|
+
5: 'text-category-5',
|
|
38
|
+
6: 'text-category-6',
|
|
39
|
+
7: 'text-category-7',
|
|
40
|
+
8: 'text-category-8',
|
|
41
|
+
9: 'text-category-9',
|
|
42
|
+
10: 'text-category-10',
|
|
43
|
+
11: 'text-category-11',
|
|
44
|
+
12: 'text-category-12',
|
|
45
|
+
};
|
|
46
|
+
// The band's wash: the slot at 10% over the card, so foreground text on
|
|
47
|
+
// it keeps its contrast (axe measures it in both themes).
|
|
48
|
+
const BAND_CLASS = {
|
|
49
|
+
1: 'bg-category-1/10',
|
|
50
|
+
2: 'bg-category-2/10',
|
|
51
|
+
3: 'bg-category-3/10',
|
|
52
|
+
4: 'bg-category-4/10',
|
|
53
|
+
5: 'bg-category-5/10',
|
|
54
|
+
6: 'bg-category-6/10',
|
|
55
|
+
7: 'bg-category-7/10',
|
|
56
|
+
8: 'bg-category-8/10',
|
|
57
|
+
9: 'bg-category-9/10',
|
|
58
|
+
10: 'bg-category-10/10',
|
|
59
|
+
11: 'bg-category-11/10',
|
|
60
|
+
12: 'bg-category-12/10',
|
|
61
|
+
};
|
|
62
|
+
// The band link. On a phone its ::after covers the whole card, so the tile
|
|
63
|
+
// is one target; the focus ring moves to that ::after with it.
|
|
64
|
+
const BAND_LINK_CLASS = 'group/band flex min-h-9 items-center gap-2 px-3 py-1.5 type-label text-foreground outline-none ' +
|
|
65
|
+
'focus-visible:ring-3 focus-visible:ring-inset focus-visible:ring-ring/50 ' +
|
|
66
|
+
'density-compact:min-h-8 density-compact:py-1 ' +
|
|
67
|
+
'max-sm:gap-1.5 max-sm:px-2 max-sm:after:absolute max-sm:after:inset-0 max-sm:after:rounded-xl ' +
|
|
68
|
+
'max-sm:focus-visible:ring-0 max-sm:focus-visible:after:ring-3 max-sm:focus-visible:after:ring-inset ' +
|
|
69
|
+
'max-sm:focus-visible:after:ring-ring/50';
|
|
70
|
+
const ACTION_CLASS = 'flex items-center gap-1 border-t border-border px-3 py-2 type-label text-foreground underline ' +
|
|
71
|
+
'underline-offset-4 outline-none hover:decoration-2 focus-visible:ring-3 focus-visible:ring-inset ' +
|
|
72
|
+
'focus-visible:ring-ring/50 density-compact:py-1.5 max-sm:hidden';
|
|
73
|
+
function NewTabMark() {
|
|
74
|
+
return (_jsxs(_Fragment, { children: [_jsx(ExternalLinkIcon, { "aria-hidden": "true", className: "size-3.5 shrink-0 text-muted-foreground" }), _jsx("span", { className: "sr-only", children: " (opens in a new tab)" })] }));
|
|
75
|
+
}
|
|
76
|
+
export function ModuleCard({ title, icon: Icon, category, to, href, value, note, action, isLoading = false, error, onRetry, className, }) {
|
|
77
|
+
const titleId = useId();
|
|
78
|
+
const bodyId = useId();
|
|
79
|
+
const state = error ? 'error' : isLoading ? 'loading' : 'success';
|
|
80
|
+
const band = (_jsxs(_Fragment, { children: [_jsx(Icon, { "aria-hidden": "true", className: cn('size-4 shrink-0', category ? ICON_CLASS[category] : 'text-muted-foreground') }), _jsx("span", { id: titleId, className: "min-w-0 flex-1 truncate underline-offset-4 group-hover/band:underline", children: title })] }));
|
|
81
|
+
const bandProps = {
|
|
82
|
+
'aria-describedby': bodyId,
|
|
83
|
+
'data-slot': 'module-card-band',
|
|
84
|
+
className: cn(BAND_LINK_CLASS, category && BAND_CLASS[category]),
|
|
85
|
+
};
|
|
86
|
+
return (_jsxs(Card, { "data-slot": "module-card", "data-state": state, className: cn('relative h-full min-w-0 gap-0 py-0', className), children: [href !== undefined ? (_jsxs("a", { href: href, target: "_blank", rel: "noreferrer", ...bandProps, children: [band, _jsx(NewTabMark, {})] })) : (_jsx(Link, { to: to, ...bandProps, children: band })), _jsxs("div", { id: bodyId, "data-slot": "module-card-body", "aria-busy": state === 'loading' || undefined, className: "flex flex-1 flex-col gap-0.5 px-3 py-2 density-compact:py-1.5 max-sm:px-2", children: [state === 'error' ? (_jsxs(_Fragment, { children: [_jsxs("p", { className: "flex items-center gap-1.5 type-body text-foreground", children: [_jsx(AlertCircleIcon, { "aria-hidden": "true", className: "size-4 shrink-0 text-destructive-text" }), _jsxs("span", { className: "min-w-0", children: ["Couldn't load", error?.message && _jsxs("span", { className: "sr-only", children: [": ", error.message] })] })] }), onRetry && (_jsx(Button, { size: "sm", variant: "outline", onClick: onRetry, "aria-describedby": titleId, className: "relative z-10 mt-1 self-start", children: "Retry" }))] })) : state === 'loading' ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "sr-only", children: "Loading" }), _jsx(Skeleton, { "data-state": "loading", className: "my-0.5 h-6 w-12" })] })) : (value !== undefined && (_jsx("span", { "data-slot": "module-card-value", className: "type-numeric text-foreground", children: value }))), note && state !== 'error' && (_jsx("span", { className: "truncate type-body text-muted-foreground", title: note, children: note }))] }), action &&
|
|
87
|
+
(action.href !== undefined ? (_jsxs("a", { href: action.href, target: "_blank", rel: "noreferrer", "aria-describedby": titleId, className: ACTION_CLASS, children: [_jsx("span", { className: "truncate", children: action.label }), _jsx(NewTabMark, {})] })) : (_jsx(Link, { to: action.to, "aria-describedby": titleId, className: ACTION_CLASS, children: _jsx("span", { className: "truncate", children: action.label }) })))] }));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A hub's places, a ModuleCard each: equal columns of 170px or more that
|
|
91
|
+
* wrap, and three columns of tiles on a phone.
|
|
92
|
+
*/
|
|
93
|
+
export function ModuleStrip({ 'aria-label': label, children, className }) {
|
|
94
|
+
return (_jsx("nav", { "aria-label": label, "data-slot": "module-strip", className: className, children: _jsx("ul", { className: "grid grid-cols-3 gap-2 sm:grid-cols-[repeat(auto-fill,minmax(--spacing(42.5),1fr))]", children: Children.map(children, (child) => child && _jsx("li", { className: "min-w-0", children: child })) }) }));
|
|
95
|
+
}
|
|
@@ -27,13 +27,33 @@ export declare const PROSE: {
|
|
|
27
27
|
readonly rule: "my-2 h-0.5 rounded-full border-0 bg-rule";
|
|
28
28
|
};
|
|
29
29
|
/**
|
|
30
|
-
* On the text's container: when it has a band (
|
|
31
|
-
*
|
|
32
|
-
* padding, so the text lines up with the
|
|
33
|
-
* band stays flush, where the section's
|
|
34
|
-
* list's own indent and a table's or
|
|
30
|
+
* On the text's container: when it has a band (a `#` or `##`, marked
|
|
31
|
+
* `data-band`, whatever element its `headingLevel` makes it), everything
|
|
32
|
+
* else is inset by the band's 12px padding, so the text lines up with the
|
|
33
|
+
* words in the band. Text with no band stays flush, where the section's
|
|
34
|
+
* own text starts. A margin, so a list's own indent and a table's or
|
|
35
|
+
* callout's box keep theirs.
|
|
35
36
|
*/
|
|
36
|
-
export declare const PROSE_INSET = "[&:has(
|
|
37
|
+
export declare const PROSE_INSET = "[&:has(>[data-band])>:not([data-band])]:mx-3";
|
|
38
|
+
/**
|
|
39
|
+
* The HTML level a heading in the text can render as. `headingLevel` on
|
|
40
|
+
* <Markdown> and the editor is the one `#` renders as: where the text
|
|
41
|
+
* sits in the page's outline (2 straight under the page's <h1>, 3 under a
|
|
42
|
+
* section's <h2>, the default).
|
|
43
|
+
*/
|
|
44
|
+
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
45
|
+
/** `#` renders as an <h3>: the page owns the <h1> and each section its <h2>. */
|
|
46
|
+
export declare const DEFAULT_HEADING_LEVEL: HeadingLevel;
|
|
47
|
+
/**
|
|
48
|
+
* The element a heading renders as, by its Markdown level (1 for `#`) and
|
|
49
|
+
* the level `#` renders as: `##` one below it, `###` two below, and so on,
|
|
50
|
+
* never past <h6>. Only the element follows the page; the look (the band,
|
|
51
|
+
* the label) stays the Markdown level's (headingClass), so a `#` looks
|
|
52
|
+
* like a `#` wherever it sits.
|
|
53
|
+
*/
|
|
54
|
+
export declare function headingTag(level: number, top?: HeadingLevel): `h${HeadingLevel}`;
|
|
55
|
+
/** Whether a heading sits on a band (`#` and `##`): the text beside it is inset (PROSE_INSET). */
|
|
56
|
+
export declare function headingBand(level: number): boolean;
|
|
37
57
|
/** A heading's classes by its Markdown level (1 for `#`). */
|
|
38
58
|
export declare function headingClass(level: number): string;
|
|
39
59
|
export type CalloutLook = {
|
|
@@ -33,13 +33,31 @@ export const PROSE = {
|
|
|
33
33
|
rule: 'my-2 h-0.5 rounded-full border-0 bg-rule',
|
|
34
34
|
};
|
|
35
35
|
/**
|
|
36
|
-
* On the text's container: when it has a band (
|
|
37
|
-
*
|
|
38
|
-
* padding, so the text lines up with the
|
|
39
|
-
* band stays flush, where the section's
|
|
40
|
-
* list's own indent and a table's or
|
|
36
|
+
* On the text's container: when it has a band (a `#` or `##`, marked
|
|
37
|
+
* `data-band`, whatever element its `headingLevel` makes it), everything
|
|
38
|
+
* else is inset by the band's 12px padding, so the text lines up with the
|
|
39
|
+
* words in the band. Text with no band stays flush, where the section's
|
|
40
|
+
* own text starts. A margin, so a list's own indent and a table's or
|
|
41
|
+
* callout's box keep theirs.
|
|
41
42
|
*/
|
|
42
|
-
export const PROSE_INSET = '[&:has(
|
|
43
|
+
export const PROSE_INSET = '[&:has(>[data-band])>:not([data-band])]:mx-3';
|
|
44
|
+
/** `#` renders as an <h3>: the page owns the <h1> and each section its <h2>. */
|
|
45
|
+
export const DEFAULT_HEADING_LEVEL = 3;
|
|
46
|
+
/**
|
|
47
|
+
* The element a heading renders as, by its Markdown level (1 for `#`) and
|
|
48
|
+
* the level `#` renders as: `##` one below it, `###` two below, and so on,
|
|
49
|
+
* never past <h6>. Only the element follows the page; the look (the band,
|
|
50
|
+
* the label) stays the Markdown level's (headingClass), so a `#` looks
|
|
51
|
+
* like a `#` wherever it sits.
|
|
52
|
+
*/
|
|
53
|
+
export function headingTag(level, top = DEFAULT_HEADING_LEVEL) {
|
|
54
|
+
const markdown = Math.max(1, Math.floor(level) || 1);
|
|
55
|
+
return `h${Math.min(top + markdown - 1, 6)}`;
|
|
56
|
+
}
|
|
57
|
+
/** Whether a heading sits on a band (`#` and `##`): the text beside it is inset (PROSE_INSET). */
|
|
58
|
+
export function headingBand(level) {
|
|
59
|
+
return level <= 2;
|
|
60
|
+
}
|
|
43
61
|
/** A heading's classes by its Markdown level (1 for `#`). */
|
|
44
62
|
export function headingClass(level) {
|
|
45
63
|
return level === 1 ? PROSE.heading1 : level === 2 ? PROSE.heading2 : level === 3 ? PROSE.heading3 : PROSE.heading4;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** The box's focus ring, from the semantic tokens: on the box's own classes. */
|
|
2
|
+
export declare const TABLE_SCROLL_FOCUS = "outline-none focus-visible:ring-3 focus-visible:ring-ring/50";
|
|
3
|
+
/**
|
|
4
|
+
* The region's accessible name: "Table, scrolls sideways", then the
|
|
5
|
+
* header row's first few columns ("…: Part, Width, Depth"). Empty
|
|
6
|
+
* headers are skipped; with none, the phrase alone.
|
|
7
|
+
*/
|
|
8
|
+
export declare function tableScrollName(columns: readonly string[]): string;
|
|
9
|
+
/** What the box needs of its element: as much as tests can fake. */
|
|
10
|
+
type ScrollBox = Pick<HTMLElement, 'scrollWidth' | 'clientWidth' | 'setAttribute' | 'removeAttribute' | 'getAttribute'>;
|
|
11
|
+
export type TableScrollRegion = {
|
|
12
|
+
/** The header row's texts, for the name: called again when they change (the editor's edits). */
|
|
13
|
+
setColumns: (columns: readonly string[]) => void;
|
|
14
|
+
/** Checks again whether the table overflows. The observers call it; a test can too. */
|
|
15
|
+
check: () => void;
|
|
16
|
+
/**
|
|
17
|
+
* Takes the tab stop off until `resume`, the rest kept: for a press in
|
|
18
|
+
* the editor's box, whose focus belongs to the text, not the box.
|
|
19
|
+
*/
|
|
20
|
+
suspend: () => void;
|
|
21
|
+
resume: () => void;
|
|
22
|
+
/** Stops watching. The box keeps whatever it last was. */
|
|
23
|
+
destroy: () => void;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Watches a table's scroll box: a tab stop and a named region while the
|
|
27
|
+
* table is wider than the box, nothing while it fits. `table` is what
|
|
28
|
+
* scrolls in it (observed for its size; the box alone when absent).
|
|
29
|
+
*/
|
|
30
|
+
export declare function watchTableScroll(box: ScrollBox, table?: Element | null, columns?: readonly string[]): TableScrollRegion;
|
|
31
|
+
/** A rendered table's header row, as texts: the header cells of its first row. */
|
|
32
|
+
export declare function headerTexts(table: Element | null | undefined): string[];
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// A table in written text, <Markdown>'s or the editor's, sits in a box
|
|
2
|
+
// that scrolls sideways when the table is wider than the text. A box that
|
|
3
|
+
// scrolls has to be reachable from the keyboard (axe's
|
|
4
|
+
// scrollable-region-focusable), or someone without a mouse can never see
|
|
5
|
+
// its right-hand columns. So, only while the table overflows, the box is
|
|
6
|
+
// a tab stop and a named region: "Table, scrolls sideways", then its
|
|
7
|
+
// columns, which tell two tables on a page apart (two regions with one
|
|
8
|
+
// name are axe's landmark-unique) and say which table it is. With the
|
|
9
|
+
// arrow keys it then scrolls. A table that fits is left alone: no tab
|
|
10
|
+
// stop and no region, nothing to reach that does nothing.
|
|
11
|
+
//
|
|
12
|
+
// It follows the table: a ResizeObserver on the box (the text's width
|
|
13
|
+
// changing) and on the table (a column added, a cell's text growing)
|
|
14
|
+
// checks again each time either changes size.
|
|
15
|
+
/** The box's focus ring, from the semantic tokens: on the box's own classes. */
|
|
16
|
+
export const TABLE_SCROLL_FOCUS = 'outline-none focus-visible:ring-3 focus-visible:ring-ring/50';
|
|
17
|
+
// The columns named in the region's name; past them, an ellipsis.
|
|
18
|
+
const NAMED_COLUMNS = 3;
|
|
19
|
+
/**
|
|
20
|
+
* The region's accessible name: "Table, scrolls sideways", then the
|
|
21
|
+
* header row's first few columns ("…: Part, Width, Depth"). Empty
|
|
22
|
+
* headers are skipped; with none, the phrase alone.
|
|
23
|
+
*/
|
|
24
|
+
export function tableScrollName(columns) {
|
|
25
|
+
const named = columns.map((column) => column.replace(/\s+/g, ' ').trim()).filter(Boolean);
|
|
26
|
+
if (named.length === 0)
|
|
27
|
+
return 'Table, scrolls sideways';
|
|
28
|
+
const shown = named.slice(0, NAMED_COLUMNS).join(', ');
|
|
29
|
+
return `Table, scrolls sideways: ${shown}${named.length > NAMED_COLUMNS ? ', …' : ''}`;
|
|
30
|
+
}
|
|
31
|
+
// Set only when it differs, so a check that finds nothing new writes
|
|
32
|
+
// nothing (the editor reads its own DOM's mutations).
|
|
33
|
+
function set(box, name, value) {
|
|
34
|
+
if (box.getAttribute(name) === value)
|
|
35
|
+
return;
|
|
36
|
+
if (value === null)
|
|
37
|
+
box.removeAttribute(name);
|
|
38
|
+
else
|
|
39
|
+
box.setAttribute(name, value);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Watches a table's scroll box: a tab stop and a named region while the
|
|
43
|
+
* table is wider than the box, nothing while it fits. `table` is what
|
|
44
|
+
* scrolls in it (observed for its size; the box alone when absent).
|
|
45
|
+
*/
|
|
46
|
+
export function watchTableScroll(box, table, columns = []) {
|
|
47
|
+
let name = tableScrollName(columns);
|
|
48
|
+
let suspended = false;
|
|
49
|
+
const check = () => {
|
|
50
|
+
// scrollWidth counts what's hidden past the right edge; the two are
|
|
51
|
+
// whole pixels, so a table that just fits measures equal.
|
|
52
|
+
const overflows = box.scrollWidth > box.clientWidth;
|
|
53
|
+
set(box, 'tabindex', overflows && !suspended ? '0' : null);
|
|
54
|
+
set(box, 'role', overflows ? 'region' : null);
|
|
55
|
+
set(box, 'aria-label', overflows ? name : null);
|
|
56
|
+
};
|
|
57
|
+
// Server rendering and tests have no ResizeObserver: one check, then
|
|
58
|
+
// whatever the box was.
|
|
59
|
+
const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(check);
|
|
60
|
+
if (observer) {
|
|
61
|
+
observer.observe(box);
|
|
62
|
+
if (table)
|
|
63
|
+
observer.observe(table);
|
|
64
|
+
}
|
|
65
|
+
check();
|
|
66
|
+
return {
|
|
67
|
+
setColumns: (next) => {
|
|
68
|
+
name = tableScrollName(next);
|
|
69
|
+
check();
|
|
70
|
+
},
|
|
71
|
+
check,
|
|
72
|
+
suspend: () => {
|
|
73
|
+
suspended = true;
|
|
74
|
+
check();
|
|
75
|
+
},
|
|
76
|
+
resume: () => {
|
|
77
|
+
suspended = false;
|
|
78
|
+
check();
|
|
79
|
+
},
|
|
80
|
+
destroy: () => observer?.disconnect(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** A rendered table's header row, as texts: the header cells of its first row. */
|
|
84
|
+
export function headerTexts(table) {
|
|
85
|
+
const row = table?.querySelector('tr');
|
|
86
|
+
return row ? Array.from(row.querySelectorAll('th'), (cell) => cell.textContent ?? '') : [];
|
|
87
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ImageUpload } from '../../lib/image-file.js';
|
|
2
|
+
import type { HeadingLevel } from './prose-look.js';
|
|
2
3
|
export type RichTextEditorProps = {
|
|
3
4
|
/** The editable element's id, for a label's `htmlFor` or a test. */
|
|
4
5
|
id?: string;
|
|
@@ -52,6 +53,15 @@ export type RichTextEditorProps = {
|
|
|
52
53
|
imageAccept?: readonly string[];
|
|
53
54
|
/** The largest image `upload` takes, in bytes: a bigger one is refused before it's sent, with its size and the limit. */
|
|
54
55
|
imageMaxSize?: number;
|
|
56
|
+
/**
|
|
57
|
+
* The HTML level the text's `#` renders as, where the field sits in the
|
|
58
|
+
* page's outline: 2 straight under the page's <h1> (a form), 3 (the
|
|
59
|
+
* default) under a section's <h2>. `##` is one below it, `###` two
|
|
60
|
+
* below, never past <h6>, as in `<Markdown>`. Only the element changes:
|
|
61
|
+
* the look, and the Markdown saved (`#` stays `#`), don't. Read once,
|
|
62
|
+
* when it mounts.
|
|
63
|
+
*/
|
|
64
|
+
headingLevel?: HeadingLevel;
|
|
55
65
|
className?: string;
|
|
56
66
|
/** The field's label. A rich-text area must have one: this, or `aria-labelledby`. */
|
|
57
67
|
'aria-label'?: string;
|
|
@@ -13,6 +13,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
13
13
|
// Raw HTML in the text shows as its characters, as <Markdown> shows it,
|
|
14
14
|
// and nothing pasted or typed can add markup.
|
|
15
15
|
//
|
|
16
|
+
// Headings in the text sit under the page's outline (`headingLevel`, from
|
|
17
|
+
// <h3> by default), as <Markdown>'s do. A table wider than the field
|
|
18
|
+
// scrolls sideways, and while it does, its box is a tab stop the arrow
|
|
19
|
+
// keys scroll.
|
|
20
|
+
//
|
|
16
21
|
// Images in the text (``) show, at most the field's width.
|
|
17
22
|
// With `upload`, an image pasted, dropped or picked with the toolbar's
|
|
18
23
|
// Image button is uploaded through the app's own storage and goes in as
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Node as ProseNode } from '@milkdown/kit/prose/model';
|
|
2
|
-
import type { NodeView } from '@milkdown/kit/prose/view';
|
|
2
|
+
import type { EditorView, NodeView } from '@milkdown/kit/prose/view';
|
|
3
3
|
import { type CalloutKind } from '../../lib/markdown-callout.js';
|
|
4
4
|
/** A slot a node view has, for the editor to render its widget into. */
|
|
5
5
|
export type Widget = {
|
|
@@ -26,6 +26,9 @@ export declare function calloutView(node: ProseNode, getPos: () => number | unde
|
|
|
26
26
|
/**
|
|
27
27
|
* A table: in a box that scrolls sideways when it's wider than the text,
|
|
28
28
|
* with an "add a column" bar along its right and an "add a row" bar under
|
|
29
|
-
* it (widgets), Notion's way.
|
|
29
|
+
* it (widgets), Notion's way. While it scrolls, the box is a tab stop and
|
|
30
|
+
* a region named for its columns (prose-table-scroll.ts), which the arrow
|
|
31
|
+
* keys, Home and End scroll; a click in it still puts the caret in the
|
|
32
|
+
* cell clicked.
|
|
30
33
|
*/
|
|
31
|
-
export declare function tableView(getPos: () => number | undefined, registry: WidgetRegistry): NodeView;
|
|
34
|
+
export declare function tableView(node: ProseNode, view: EditorView, getPos: () => number | undefined, registry: WidgetRegistry): NodeView;
|