@tristan2828/ui-foundation 4.22.0 → 4.24.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 +23 -0
- package/conventions/docs/design-language.md +14 -9
- package/dist/components/app/markdown.js +9 -8
- package/dist/components/app/milkdown-editor.js +4 -1
- package/dist/components/app/prose-look.d.ts +23 -11
- package/dist/components/app/prose-look.js +23 -11
- package/package.json +1 -1
- package/styles/index.css +1 -0
- package/styles/theme.css +18 -12
|
@@ -642,3 +642,26 @@ soft one; copy it.
|
|
|
642
642
|
- **Hard-deleted:** `DELETE` removes the row. Give the entity no `delete`
|
|
643
643
|
in `mcp_entities.py`: a chat that read planted instructions would
|
|
644
644
|
destroy data that only a backup returns.
|
|
645
|
+
|
|
646
|
+
## Fields only a script writes
|
|
647
|
+
|
|
648
|
+
Some fields the API takes on create and update are for a script, not for
|
|
649
|
+
the people or chats using the app: an importer that runs again and again
|
|
650
|
+
stores each record's id in the source it came from, so a re-run finds the
|
|
651
|
+
record even after it's renamed. The API has to accept that id, for the
|
|
652
|
+
importer. A chat that changed or cleared it would break the matching, and
|
|
653
|
+
the next run would create the record a second time.
|
|
654
|
+
|
|
655
|
+
- *MCP:* list the field's wire name in the entity's `McpEntity`
|
|
656
|
+
`script_only` (`script_only=("sourceId",)`). Chats still read it
|
|
657
|
+
(`fetch` shows it), but `create` and `update` refuse a call that names
|
|
658
|
+
it, by its wire name or its snake_case one, before the entity's own
|
|
659
|
+
function runs: `Nothing was saved.` and `- sourceId: set by a script,
|
|
660
|
+
not through MCP`, with no audit row. It's left out of the two tools'
|
|
661
|
+
descriptions, so a chat isn't invited to send it. The backend refuses
|
|
662
|
+
to start if a name isn't a field of the create or update schema, or if
|
|
663
|
+
create requires it (a chat could then never create one).
|
|
664
|
+
- *Screens:* the form leaves the field out, for the same reason.
|
|
665
|
+
- Don't use it for a field the server works out itself (`computed`) or
|
|
666
|
+
one that's never on the request: those aren't in the create or update
|
|
667
|
+
schema to begin with.
|
|
@@ -208,18 +208,23 @@ outline (that stays `type-page-title` and `type-section-title`):
|
|
|
208
208
|
|
|
209
209
|
| Class | For | Size / weight |
|
|
210
210
|
|---|---|---|
|
|
211
|
-
| `type-heading-1` | `#` in a note,
|
|
212
|
-
| `type-heading-2` | `##`,
|
|
213
|
-
| `type-heading-3` | `###`,
|
|
211
|
+
| `type-heading-1` | `#` in a note, on a band in `--heading-band` with a 2px `--rule` along its foot | 28px / 700 |
|
|
212
|
+
| `type-heading-2` | `##`, on a band in `--muted` | 20px / 600 |
|
|
213
|
+
| `type-heading-3` | `###`, a small uppercase label in `--muted-foreground` | 12px / 600 |
|
|
214
214
|
|
|
215
|
-
The
|
|
216
|
-
|
|
215
|
+
The bands run the text's full width, with 12px of padding either side.
|
|
216
|
+
In text with a band, the rest (paragraphs, lists, tables, callouts,
|
|
217
|
+
`###` and deeper) is inset by the same 12px, so it lines up with the
|
|
218
|
+
words in the band rather than its edge; text with no band stays flush. The level reads at a glance: two bands of
|
|
219
|
+
different strength, then a label.
|
|
217
220
|
|
|
218
|
-
The same text has
|
|
221
|
+
The same text has four colour tokens of its own: `--link` (`text-link`), a
|
|
219
222
|
link's colour, blue as well as underlined (5.59:1 on white, 8.90:1 on the
|
|
220
|
-
dark page); `--heading-
|
|
221
|
-
|
|
222
|
-
divider draws, stronger than `--border`'s hairline
|
|
223
|
+
dark page); `--heading-band` (`bg-heading-band`), the `#` band, one step
|
|
224
|
+
stronger than `--muted`; `--rule` (`bg-rule`), the 2px line a `---`
|
|
225
|
+
divider draws, stronger than `--border`'s hairline, and the foot of the
|
|
226
|
+
`#` band; and `--heading-bar`, which the headings' old left bars drew in,
|
|
227
|
+
kept for apps but unused by the package. A callout (`> [!NOTE]`) is a 10% wash
|
|
223
228
|
of its kind's tone behind ordinary text, with the icon in the tone's text
|
|
224
229
|
shade: note `info`, tip `success`, important category slot 2, warning
|
|
225
230
|
`warning`, caution `destructive`. The package draws all of it; an app
|
|
@@ -5,7 +5,7 @@ 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';
|
|
8
|
+
import { CALLOUT_BOX, CALLOUT_LOOK, headingClass, PROSE, PROSE_INSET } from './prose-look.js';
|
|
9
9
|
const REMARK_PLUGINS = [remarkGfm, remarkCallouts];
|
|
10
10
|
function isCalloutKind(value) {
|
|
11
11
|
return CALLOUT_KINDS.includes(value);
|
|
@@ -17,13 +17,14 @@ function omitNode(props) {
|
|
|
17
17
|
delete rest.node;
|
|
18
18
|
return rest;
|
|
19
19
|
}
|
|
20
|
+
// The text's colour first, so a level's own (`###`'s muted label) wins.
|
|
20
21
|
const COMPONENTS = {
|
|
21
|
-
h1: (props) => _jsx("h3", { ...omitNode(props), className: cn(
|
|
22
|
-
h2: (props) => _jsx("h4", { ...omitNode(props), className: cn(
|
|
23
|
-
h3: (props) => _jsx("h5", { ...omitNode(props), className: cn(
|
|
24
|
-
h4: (props) => _jsx("h6", { ...omitNode(props), className: cn(
|
|
25
|
-
h5: (props) => _jsx("h6", { ...omitNode(props), className: cn(
|
|
26
|
-
h6: (props) => _jsx("h6", { ...omitNode(props), className: cn(
|
|
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)) }),
|
|
27
28
|
a: (props) => {
|
|
28
29
|
const { href, children, ...rest } = omitNode(props);
|
|
29
30
|
if (!href)
|
|
@@ -74,5 +75,5 @@ const COMPONENTS = {
|
|
|
74
75
|
td: (props) => _jsx(TableCell, { ...omitNode(props), className: "border-r border-border whitespace-normal last:border-r-0" }),
|
|
75
76
|
};
|
|
76
77
|
export function Markdown({ children, className, ...rest }) {
|
|
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 }) }));
|
|
78
|
+
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 }) }));
|
|
78
79
|
}
|
|
@@ -714,7 +714,10 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
714
714
|
'has-[[aria-readonly=true]]:text-muted-foreground',
|
|
715
715
|
// The same type roles <Markdown> uses, so text reads the same
|
|
716
716
|
// being edited as being read.
|
|
717
|
-
'[&_.ProseMirror]:flex [&_.ProseMirror]:flex-col [&_.ProseMirror]:gap-3',
|
|
717
|
+
'[&_.ProseMirror]:flex [&_.ProseMirror]:flex-col [&_.ProseMirror]:gap-3',
|
|
718
|
+
// PROSE_INSET, on the editable element: with a heading's band
|
|
719
|
+
// in the text, the rest lines up with the words in it.
|
|
720
|
+
'[&_.ProseMirror:has(>:is(h3,h4))>:not(h3,h4)]:mx-3', !inline && '[&_.ProseMirror]:min-h-20',
|
|
718
721
|
// ProseMirror's own required style (prosemirror-view's CSS): spaces
|
|
719
722
|
// as typed, so one at the end of a line isn't kept as .
|
|
720
723
|
'[&_.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',
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { type LucideIcon } from 'lucide-react';
|
|
2
2
|
import type { CalloutKind } from '../../lib/markdown-callout.js';
|
|
3
3
|
/**
|
|
4
|
-
* Classes for written text's own elements.
|
|
5
|
-
* <Markdown> adds the text colour, and the editor's read-only text is
|
|
6
|
-
* muted as a whole.
|
|
4
|
+
* Classes for written text's own elements.
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* `#` and `##` sit on a full-width tinted band, stronger for `#`, which
|
|
7
|
+
* also has a 2px rule along its foot, so the top of the outline is marked
|
|
8
|
+
* twice. `###` is a small uppercase label. The band's text is always the
|
|
9
|
+
* foreground: the editor's read-only text is muted as a whole, and muted
|
|
10
|
+
* text on the `##` band measures 4.34:1.
|
|
11
|
+
*
|
|
12
|
+
* Where there's a band, the rest of the text (paragraphs, lists, tables,
|
|
13
|
+
* callouts, `###` and deeper) is inset by its padding, PROSE_INSET, so it
|
|
14
|
+
* lines up with the words inside the band rather than its edge.
|
|
12
15
|
*/
|
|
13
16
|
export declare const PROSE: {
|
|
14
|
-
/** `#`: the largest,
|
|
15
|
-
readonly heading1: "type-heading-1 mt-4 border-
|
|
16
|
-
readonly heading2: "type-heading-2 mt-
|
|
17
|
-
|
|
17
|
+
/** `#`: the largest, on the stronger band with a rule along its foot. */
|
|
18
|
+
readonly heading1: "type-heading-1 mt-4 rounded-t-sm border-b-2 border-rule bg-heading-band px-3 py-2 text-foreground first:mt-0";
|
|
19
|
+
readonly heading2: "type-heading-2 mt-5 rounded-sm bg-muted px-3 py-1.25 text-foreground first:mt-0";
|
|
20
|
+
/** `###`: a small uppercase label in the muted tone, no band. */
|
|
21
|
+
readonly heading3: "type-heading-3 mt-0.5 uppercase tracking-[0.08em] text-muted-foreground first:mt-0";
|
|
18
22
|
/** `####` and deeper: a label, as before. */
|
|
19
23
|
readonly heading4: "type-label mt-1 first:mt-0";
|
|
20
24
|
/** Coloured as well as underlined. */
|
|
@@ -22,6 +26,14 @@ export declare const PROSE: {
|
|
|
22
26
|
/** `---`: a 2px line in the rule tone, with room around it. */
|
|
23
27
|
readonly rule: "my-2 h-0.5 rounded-full border-0 bg-rule";
|
|
24
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* On the text's container: when it has a band (the `<h3>` or `<h4>` a `#`
|
|
31
|
+
* or `##` renders as), everything else is inset by the band's 12px
|
|
32
|
+
* padding, so the text lines up with the words in the band. Text with no
|
|
33
|
+
* band stays flush, where the section's own text starts. A margin, so a
|
|
34
|
+
* list's own indent and a table's or callout's box keep theirs.
|
|
35
|
+
*/
|
|
36
|
+
export declare const PROSE_INSET = "[&:has(>:is(h3,h4))>:not(h3,h4)]:mx-3";
|
|
25
37
|
/** A heading's classes by its Markdown level (1 for `#`). */
|
|
26
38
|
export declare function headingClass(level: number): string;
|
|
27
39
|
export type CalloutLook = {
|
|
@@ -7,20 +7,24 @@
|
|
|
7
7
|
// (what "Callout" inserts).
|
|
8
8
|
import { InfoIcon, LightbulbIcon, MessageSquareWarningIcon, OctagonAlertIcon, TriangleAlertIcon, } from 'lucide-react';
|
|
9
9
|
/**
|
|
10
|
-
* Classes for written text's own elements.
|
|
11
|
-
* <Markdown> adds the text colour, and the editor's read-only text is
|
|
12
|
-
* muted as a whole.
|
|
10
|
+
* Classes for written text's own elements.
|
|
13
11
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* `#` and `##` sit on a full-width tinted band, stronger for `#`, which
|
|
13
|
+
* also has a 2px rule along its foot, so the top of the outline is marked
|
|
14
|
+
* twice. `###` is a small uppercase label. The band's text is always the
|
|
15
|
+
* foreground: the editor's read-only text is muted as a whole, and muted
|
|
16
|
+
* text on the `##` band measures 4.34:1.
|
|
17
|
+
*
|
|
18
|
+
* Where there's a band, the rest of the text (paragraphs, lists, tables,
|
|
19
|
+
* callouts, `###` and deeper) is inset by its padding, PROSE_INSET, so it
|
|
20
|
+
* lines up with the words inside the band rather than its edge.
|
|
18
21
|
*/
|
|
19
22
|
export const PROSE = {
|
|
20
|
-
/** `#`: the largest,
|
|
21
|
-
heading1: 'type-heading-1 mt-4 border-
|
|
22
|
-
heading2: 'type-heading-2 mt-
|
|
23
|
-
|
|
23
|
+
/** `#`: the largest, on the stronger band with a rule along its foot. */
|
|
24
|
+
heading1: 'type-heading-1 mt-4 rounded-t-sm border-b-2 border-rule bg-heading-band px-3 py-2 text-foreground first:mt-0',
|
|
25
|
+
heading2: 'type-heading-2 mt-5 rounded-sm bg-muted px-3 py-1.25 text-foreground first:mt-0',
|
|
26
|
+
/** `###`: a small uppercase label in the muted tone, no band. */
|
|
27
|
+
heading3: 'type-heading-3 mt-0.5 uppercase tracking-[0.08em] text-muted-foreground first:mt-0',
|
|
24
28
|
/** `####` and deeper: a label, as before. */
|
|
25
29
|
heading4: 'type-label mt-1 first:mt-0',
|
|
26
30
|
/** Coloured as well as underlined. */
|
|
@@ -28,6 +32,14 @@ export const PROSE = {
|
|
|
28
32
|
/** `---`: a 2px line in the rule tone, with room around it. */
|
|
29
33
|
rule: 'my-2 h-0.5 rounded-full border-0 bg-rule',
|
|
30
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* On the text's container: when it has a band (the `<h3>` or `<h4>` a `#`
|
|
37
|
+
* or `##` renders as), everything else is inset by the band's 12px
|
|
38
|
+
* padding, so the text lines up with the words in the band. Text with no
|
|
39
|
+
* band stays flush, where the section's own text starts. A margin, so a
|
|
40
|
+
* list's own indent and a table's or callout's box keep theirs.
|
|
41
|
+
*/
|
|
42
|
+
export const PROSE_INSET = '[&:has(>:is(h3,h4))>:not(h3,h4)]:mx-3';
|
|
31
43
|
/** A heading's classes by its Markdown level (1 for `#`). */
|
|
32
44
|
export function headingClass(level) {
|
|
33
45
|
return level === 1 ? PROSE.heading1 : level === 2 ? PROSE.heading2 : level === 3 ? PROSE.heading3 : PROSE.heading4;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.24.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
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
--color-info-text: var(--info-text);
|
|
50
50
|
--color-link: var(--link);
|
|
51
51
|
--color-rule: var(--rule);
|
|
52
|
+
--color-heading-band: var(--heading-band);
|
|
52
53
|
--color-heading-bar: var(--heading-bar);
|
|
53
54
|
--color-category-1: var(--category-1);
|
|
54
55
|
--color-category-2: var(--category-2);
|
package/styles/theme.css
CHANGED
|
@@ -142,17 +142,18 @@
|
|
|
142
142
|
* Headings inside written text (a record's notes: <Markdown> and the
|
|
143
143
|
* rich-text editor), not the screen's own outline. Three clear steps
|
|
144
144
|
* over the 14px body, roughly Notion's proportions, so `#`, `##` and
|
|
145
|
-
* `###` never look alike. Where the roles are used,
|
|
146
|
-
*
|
|
145
|
+
* `###` never look alike. Where the roles are used, `#` and `##` also
|
|
146
|
+
* sit on a tinted band (--heading-band, --muted) and `###` is a small
|
|
147
|
+
* uppercase label (prose-look.ts).
|
|
147
148
|
*/
|
|
148
|
-
--type-heading-1-size: 1.
|
|
149
|
-
--type-heading-1-leading:
|
|
149
|
+
--type-heading-1-size: 1.75rem;
|
|
150
|
+
--type-heading-1-leading: 2.25rem;
|
|
150
151
|
--type-heading-1-weight: 700;
|
|
151
152
|
--type-heading-2-size: 1.25rem;
|
|
152
153
|
--type-heading-2-leading: 1.75rem;
|
|
153
154
|
--type-heading-2-weight: 600;
|
|
154
|
-
--type-heading-3-size:
|
|
155
|
-
--type-heading-3-leading: 1.
|
|
155
|
+
--type-heading-3-size: 0.75rem;
|
|
156
|
+
--type-heading-3-leading: 1.25rem;
|
|
156
157
|
--type-heading-3-weight: 600;
|
|
157
158
|
|
|
158
159
|
/*
|
|
@@ -208,8 +209,11 @@
|
|
|
208
209
|
/* A thematic break (`---`) in written text: a 2px line meant to be
|
|
209
210
|
seen, stronger than --border's hairline (on white: 2.59). */
|
|
210
211
|
--rule: var(--gray-400);
|
|
211
|
-
/* The
|
|
212
|
-
|
|
212
|
+
/* The band behind a `#` heading in written text: one step stronger than
|
|
213
|
+
--muted, which `##`'s band takes. Its foot is a 2px --rule. */
|
|
214
|
+
--heading-band: var(--gray-200);
|
|
215
|
+
/* Written text's headings drew a bar in this until 4.24.0; nothing in
|
|
216
|
+
the package uses it now. Kept for an app that does (on white: 5.59). */
|
|
213
217
|
--heading-bar: var(--blue-700);
|
|
214
218
|
/* Categorical slots — a glyph tint, never a fill behind text. */
|
|
215
219
|
--category-1: var(--category-1-700);
|
|
@@ -305,12 +309,12 @@
|
|
|
305
309
|
--type-label-leading: 1.625rem;
|
|
306
310
|
--type-caption-size: 0.9375rem;
|
|
307
311
|
--type-caption-leading: 1.25rem;
|
|
308
|
-
--type-heading-1-size:
|
|
309
|
-
--type-heading-1-leading: 2.
|
|
312
|
+
--type-heading-1-size: 2.1875rem;
|
|
313
|
+
--type-heading-1-leading: 2.8125rem;
|
|
310
314
|
--type-heading-2-size: 1.5625rem;
|
|
311
315
|
--type-heading-2-leading: 2.125rem;
|
|
312
|
-
--type-heading-3-size:
|
|
313
|
-
--type-heading-3-leading: 1.
|
|
316
|
+
--type-heading-3-size: 0.9375rem;
|
|
317
|
+
--type-heading-3-leading: 1.5625rem;
|
|
314
318
|
|
|
315
319
|
--table-cell-px: 0.75rem;
|
|
316
320
|
--table-cell-py: 0.75rem;
|
|
@@ -350,6 +354,8 @@
|
|
|
350
354
|
--info-text: var(--blue-400);
|
|
351
355
|
--link: var(--blue-400); /* on near-black: 8.90 */
|
|
352
356
|
--rule: var(--gray-600); /* on near-black: 2.54 */
|
|
357
|
+
/* Stronger is lighter on the dark page: halfway from --muted to gray-700. */
|
|
358
|
+
--heading-band: color-mix(in oklab, var(--gray-900), var(--gray-700));
|
|
353
359
|
--heading-bar: var(--blue-400); /* on near-black: 8.90 */
|
|
354
360
|
/* Categorical slots — lighter shades to read against the dark page. */
|
|
355
361
|
--category-1: var(--category-1-400);
|