@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.
Files changed (41) hide show
  1. package/conventions/docs/add-an-entity.md +9 -4
  2. package/conventions/docs/cell-patterns.md +13 -4
  3. package/conventions/docs/design-language.md +22 -0
  4. package/dist/components/app/data-table.js +21 -13
  5. package/dist/components/app/markdown.js +38 -20
  6. package/dist/components/app/milkdown-editor.js +189 -31
  7. package/dist/components/app/prose-look.d.ts +32 -0
  8. package/dist/components/app/prose-look.js +48 -0
  9. package/dist/components/app/rich-text-format-items.d.ts +13 -1
  10. package/dist/components/app/rich-text-format-items.js +43 -10
  11. package/dist/components/app/rich-text-formats.d.ts +15 -2
  12. package/dist/components/app/rich-text-formats.js +99 -10
  13. package/dist/components/app/rich-text-node-views.d.ts +31 -0
  14. package/dist/components/app/rich-text-node-views.js +79 -0
  15. package/dist/components/app/rich-text-shortcuts.d.ts +12 -0
  16. package/dist/components/app/rich-text-shortcuts.js +52 -0
  17. package/dist/components/app/rich-text-slash-menu.js +4 -1
  18. package/dist/components/app/rich-text-slash.js +26 -10
  19. package/dist/components/app/rich-text-table-menu.d.ts +20 -0
  20. package/dist/components/app/rich-text-table-menu.js +53 -0
  21. package/dist/components/app/rich-text-table.d.ts +19 -0
  22. package/dist/components/app/rich-text-table.js +162 -0
  23. package/dist/components/app/rich-text-toolbar.d.ts +11 -2
  24. package/dist/components/app/rich-text-toolbar.js +14 -23
  25. package/dist/components/app/rich-text-widgets.d.ts +19 -0
  26. package/dist/components/app/rich-text-widgets.js +34 -0
  27. package/dist/components/app/secondary-filters.d.ts +18 -0
  28. package/dist/components/app/secondary-filters.js +32 -0
  29. package/dist/components/ui/badge.d.ts +1 -1
  30. package/dist/components/ui/combobox.js +7 -1
  31. package/dist/components/ui/dropdown-menu.d.ts +29 -0
  32. package/dist/components/ui/dropdown-menu.js +51 -0
  33. package/dist/hooks/use-table-url-state.d.ts +1 -1
  34. package/dist/hooks/use-table-url-state.js +6 -5
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +1 -0
  37. package/dist/lib/markdown-callout.d.ts +38 -0
  38. package/dist/lib/markdown-callout.js +103 -0
  39. package/package.json +1 -1
  40. package/styles/index.css +17 -0
  41. package/styles/theme.css +30 -0
@@ -0,0 +1,38 @@
1
+ export declare const CALLOUT_KINDS: readonly ["note", "tip", "important", "warning", "caution"];
2
+ export type CalloutKind = (typeof CALLOUT_KINDS)[number];
3
+ type MdNode = {
4
+ type: string;
5
+ children?: MdNode[];
6
+ value?: string;
7
+ data?: Record<string, unknown>;
8
+ [key: string]: unknown;
9
+ };
10
+ /**
11
+ * For react-markdown: a callout quote becomes a `<div data-callout="note">`
12
+ * (its marker gone), which <Markdown>'s `div` component draws as one.
13
+ */
14
+ export declare function remarkCallouts(): (tree: unknown) => void;
15
+ /**
16
+ * For the rich-text editor: a callout quote becomes a `callout` node
17
+ * (`kind`, `marker`), which its schema reads and `calloutHandler` writes.
18
+ */
19
+ export declare function remarkCalloutNodes(): (tree: unknown) => void;
20
+ /** The marker to write for a callout: as it was written, unless its kind changed. */
21
+ export declare function calloutMarker(kind: CalloutKind, written: string | null | undefined): string;
22
+ type ToMarkdownState = {
23
+ enter: (name: string) => () => void;
24
+ createTracker: (info: unknown) => {
25
+ move: (value: string) => void;
26
+ shift: (value: number) => void;
27
+ current: () => unknown;
28
+ };
29
+ containerFlow: (node: MdNode, info: unknown) => string;
30
+ indentLines: (value: string, map: (line: string, index: number, blank: boolean) => string) => string;
31
+ };
32
+ /**
33
+ * Writes a `callout` node the way remark writes a quote, with the marker
34
+ * as its first line. A first block that isn't a paragraph gets a blank
35
+ * line after the marker, so it can't run into it.
36
+ */
37
+ export declare function calloutHandler(node: MdNode, _parent: unknown, state: ToMarkdownState, info: unknown): string;
38
+ export {};
@@ -0,0 +1,103 @@
1
+ // Callouts (Notion's coloured boxes) in Markdown: GitHub's alert syntax, a
2
+ // quote whose first line is its kind in brackets.
3
+ //
4
+ // > [!NOTE]
5
+ // > Measure twice, cut once.
6
+ //
7
+ // It's plain Markdown, so a note stays readable anywhere (GitHub and
8
+ // Obsidian render it as a callout too, anything else as a quote), and
9
+ // remark parses it as a blockquote whose first text is the marker. These
10
+ // plugins read that marker: `remarkCallouts` for <Markdown> (an element
11
+ // react-markdown hands to its `div` component) and `remarkCalloutNodes`
12
+ // for the rich-text editor (a `callout` node it has a schema for, written
13
+ // back with `calloutHandler`). The merge (markdown-merge.ts) needs
14
+ // neither: to it a callout is the quote it's written as.
15
+ //
16
+ // Five kinds, GitHub's: note, tip, important, warning, caution. The marker
17
+ // is kept as it was written (`[!note]` stays lower case) unless the kind
18
+ // changes, so an untouched callout never reads as edited.
19
+ export const CALLOUT_KINDS = ['note', 'tip', 'important', 'warning', 'caution'];
20
+ // The marker alone on the quote's first line: `[!NOTE]`, any case, spaces
21
+ // after it allowed.
22
+ const MARKER = /^\[!(note|tip|important|warning|caution)\][ \t]*(?:\n|$)/i;
23
+ /**
24
+ * A quote that's a callout: its kind and marker as written, with the
25
+ * marker taken out of its first paragraph (and that paragraph too, when
26
+ * the marker was all of it). Null for any other node, untouched.
27
+ */
28
+ function takeCallout(node) {
29
+ if (node.type !== 'blockquote')
30
+ return null;
31
+ const paragraph = node.children?.[0];
32
+ const text = paragraph?.type === 'paragraph' ? paragraph.children?.[0] : undefined;
33
+ if (!paragraph || text?.type !== 'text' || typeof text.value !== 'string')
34
+ return null;
35
+ const found = MARKER.exec(text.value);
36
+ if (!found)
37
+ return null;
38
+ const rest = text.value.slice(found[0].length);
39
+ if (rest)
40
+ text.value = rest;
41
+ else
42
+ paragraph.children.shift();
43
+ if (paragraph.children.length === 0)
44
+ node.children.shift();
45
+ return { kind: found[1].toLowerCase(), marker: found[0].trim() };
46
+ }
47
+ function visit(node, each) {
48
+ for (const child of node.children ?? [])
49
+ visit(child, each);
50
+ each(node);
51
+ }
52
+ /**
53
+ * For react-markdown: a callout quote becomes a `<div data-callout="note">`
54
+ * (its marker gone), which <Markdown>'s `div` component draws as one.
55
+ */
56
+ export function remarkCallouts() {
57
+ return (tree) => {
58
+ visit(tree, (node) => {
59
+ const callout = takeCallout(node);
60
+ if (callout)
61
+ node.data = { ...node.data, hName: 'div', hProperties: { dataCallout: callout.kind } };
62
+ });
63
+ };
64
+ }
65
+ /**
66
+ * For the rich-text editor: a callout quote becomes a `callout` node
67
+ * (`kind`, `marker`), which its schema reads and `calloutHandler` writes.
68
+ */
69
+ export function remarkCalloutNodes() {
70
+ return (tree) => {
71
+ visit(tree, (node) => {
72
+ const callout = takeCallout(node);
73
+ if (!callout)
74
+ return;
75
+ node.type = 'callout';
76
+ node.kind = callout.kind;
77
+ node.marker = callout.marker;
78
+ });
79
+ };
80
+ }
81
+ /** The marker to write for a callout: as it was written, unless its kind changed. */
82
+ export function calloutMarker(kind, written) {
83
+ return written && written.slice(2, -1).toLowerCase() === kind ? written : `[!${kind.toUpperCase()}]`;
84
+ }
85
+ /**
86
+ * Writes a `callout` node the way remark writes a quote, with the marker
87
+ * as its first line. A first block that isn't a paragraph gets a blank
88
+ * line after the marker, so it can't run into it.
89
+ */
90
+ export function calloutHandler(node, _parent, state, info) {
91
+ const exit = state.enter('blockquote');
92
+ const tracker = state.createTracker(info);
93
+ tracker.move('> ');
94
+ tracker.shift(2);
95
+ const marker = calloutMarker(node.kind, node.marker);
96
+ // Milkdown's serializer starts a container's flow on a new line; the
97
+ // marker's line is that line.
98
+ const body = state.containerFlow(node, tracker.current()).replace(/^\n+/, '');
99
+ const joined = !body ? marker : node.children?.[0]?.type === 'paragraph' ? `${marker}\n${body}` : `${marker}\n\n${body}`;
100
+ const value = state.indentLines(joined, (line, _index, blank) => '>' + (blank ? '' : ' ') + line);
101
+ exit();
102
+ return value;
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tristan2828/ui-foundation",
3
- "version": "3.20.1",
3
+ "version": "3.22.0",
4
4
  "description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/styles/index.css CHANGED
@@ -47,6 +47,8 @@
47
47
  --color-info: var(--info);
48
48
  --color-info-foreground: var(--info-foreground);
49
49
  --color-info-text: var(--info-text);
50
+ --color-link: var(--link);
51
+ --color-rule: var(--rule);
50
52
  --color-category-1: var(--category-1);
51
53
  --color-category-2: var(--category-2);
52
54
  --color-category-3: var(--category-3);
@@ -105,6 +107,21 @@
105
107
  line-height: var(--type-caption-leading);
106
108
  font-weight: var(--type-caption-weight);
107
109
  }
110
+ @utility type-heading-1 {
111
+ font-size: var(--type-heading-1-size);
112
+ line-height: var(--type-heading-1-leading);
113
+ font-weight: var(--type-heading-1-weight);
114
+ }
115
+ @utility type-heading-2 {
116
+ font-size: var(--type-heading-2-size);
117
+ line-height: var(--type-heading-2-leading);
118
+ font-weight: var(--type-heading-2-weight);
119
+ }
120
+ @utility type-heading-3 {
121
+ font-size: var(--type-heading-3-size);
122
+ line-height: var(--type-heading-3-leading);
123
+ font-weight: var(--type-heading-3-weight);
124
+ }
108
125
 
109
126
  @layer base {
110
127
  * {
package/styles/theme.css CHANGED
@@ -123,6 +123,22 @@
123
123
  --type-caption-size: 0.75rem; /* counts, metadata, helper text */
124
124
  --type-caption-leading: 1rem;
125
125
  --type-caption-weight: 400;
126
+ /*
127
+ * Headings inside written text (a record's notes: <Markdown> and the
128
+ * rich-text editor), not the screen's own outline. Three clear steps
129
+ * over the 14px body, roughly Notion's proportions, so `#`, `##` and
130
+ * `###` never look alike. Heading 1 also gets a rule under it (where
131
+ * the roles are used), so it reads as the top of the outline.
132
+ */
133
+ --type-heading-1-size: 1.625rem;
134
+ --type-heading-1-leading: 2rem;
135
+ --type-heading-1-weight: 700;
136
+ --type-heading-2-size: 1.25rem;
137
+ --type-heading-2-leading: 1.75rem;
138
+ --type-heading-2-weight: 600;
139
+ --type-heading-3-size: 1.0625rem;
140
+ --type-heading-3-leading: 1.5rem;
141
+ --type-heading-3-weight: 600;
126
142
 
127
143
  /*
128
144
  * Table density (design-language.md "Density"). The table primitive's
@@ -167,6 +183,12 @@
167
183
  --success-text: var(--green-800);
168
184
  --warning-text: var(--amber-800);
169
185
  --info-text: var(--blue-800);
186
+ /* A link in written text: coloured as well as underlined, so it reads
187
+ as a link at a glance (on white: 5.59). */
188
+ --link: var(--blue-700);
189
+ /* A thematic break (`---`) in written text: a 2px line meant to be
190
+ seen, stronger than --border's hairline (on white: 2.59). */
191
+ --rule: var(--gray-400);
170
192
  /* Categorical slots — a glyph tint, never a fill behind text. */
171
193
  --category-1: var(--category-1-700);
172
194
  --category-2: var(--category-2-700);
@@ -176,6 +198,11 @@
176
198
  --category-6: var(--category-6-700);
177
199
  --category-7: var(--category-7-700);
178
200
  --category-8: var(--category-8-700);
201
+ /* The zebra stripe (table.tsx: bg-muted/40 on even rows) mixed onto the
202
+ page, opaque: what a pinned table cell paints on an even row, so it
203
+ matches the row without letting scrolled columns show through. Mixed
204
+ in sRGB, as the browser composites the translucent stripe. */
205
+ --table-stripe: color-mix(in srgb, var(--muted) 40%, var(--background));
179
206
  --border: var(--gray-200);
180
207
  --input: var(--gray-200);
181
208
  --ring: var(--gray-400);
@@ -236,6 +263,8 @@
236
263
  --success-text: var(--green-400);
237
264
  --warning-text: var(--amber-400);
238
265
  --info-text: var(--blue-400);
266
+ --link: var(--blue-400); /* on near-black: 8.90 */
267
+ --rule: var(--gray-600); /* on near-black: 2.54 */
239
268
  /* Categorical slots — lighter shades to read against the dark page. */
240
269
  --category-1: var(--category-1-400);
241
270
  --category-2: var(--category-2-400);
@@ -245,6 +274,7 @@
245
274
  --category-6: var(--category-6-400);
246
275
  --category-7: var(--category-7-400);
247
276
  --category-8: var(--category-8-400);
277
+ --table-stripe: color-mix(in srgb, var(--muted) 40%, var(--background));
248
278
  --border: var(--white-a10);
249
279
  --input: var(--white-a15);
250
280
  --ring: var(--gray-500);