@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.
@@ -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, with a 5px bar in `--heading-bar` | 26px / 700 |
212
- | `type-heading-2` | `##`, with a 3px bar in `--heading-bar` | 20px / 600 |
213
- | `type-heading-3` | `###`, with a 2px bar in `--rule` | 17px / 600 |
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 bar runs down the heading's left edge and thins as the level drops,
216
- so the level reads at a glance as well as from the size.
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 three colour tokens of its own: `--link` (`text-link`), a
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-bar` (`border-heading-bar`), the same blue for the
221
- bar beside `#` and `##`; and `--rule` (`bg-rule`), the 2px line a `---`
222
- divider draws, stronger than `--border`'s hairline. A callout (`> [!NOTE]`) is a 10% wash
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(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
+ 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', !inline && '[&_.ProseMirror]:min-h-20',
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 &nbsp;.
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. No colours on the heading text:
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
- * Each of the first three heading levels has a bar down its left edge that
9
- * thins as the level drops: 5px in the heading-bar tone for `#`, 3px for
10
- * `##`, 2px in the rule tone for `###`. A level reads from the bar
11
- * as well as from the size.
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, with the widest bar, so it reads as the top of the outline. */
15
- readonly heading1: "type-heading-1 mt-4 border-l-[5px] border-heading-bar pl-3 first:mt-0";
16
- readonly heading2: "type-heading-2 mt-3 border-l-[3px] border-heading-bar pl-3 first:mt-0";
17
- readonly heading3: "type-heading-3 mt-1 border-l-2 border-rule pl-3 first:mt-0";
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. No colours on the heading text:
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
- * Each of the first three heading levels has a bar down its left edge that
15
- * thins as the level drops: 5px in the heading-bar tone for `#`, 3px for
16
- * `##`, 2px in the rule tone for `###`. A level reads from the bar
17
- * as well as from the size.
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, with the widest bar, so it reads as the top of the outline. */
21
- heading1: 'type-heading-1 mt-4 border-l-[5px] border-heading-bar pl-3 first:mt-0',
22
- heading2: 'type-heading-2 mt-3 border-l-[3px] border-heading-bar pl-3 first:mt-0',
23
- heading3: 'type-heading-3 mt-1 border-l-2 border-rule pl-3 first:mt-0',
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.22.0",
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, each also gets a
146
- * bar down its left edge that thins by level (--heading-bar).
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.625rem;
149
- --type-heading-1-leading: 2rem;
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: 1.0625rem;
155
- --type-heading-3-leading: 1.5rem;
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 bar down the left of a `#` or `##` heading in written text, so
212
- the level shows at a glance (on white: 5.59). `###` takes --rule. */
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: 2rem;
309
- --type-heading-1-leading: 2.5rem;
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: 1.3125rem;
313
- --type-heading-3-leading: 1.875rem;
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);