@tristan2828/ui-foundation 4.23.0 → 4.25.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 +5 -1
- package/conventions/docs/add-an-entity.md +3 -2
- package/conventions/docs/design-language.md +14 -9
- package/dist/api/gateway/auth.js +2 -1
- 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/dist/error-reporting/reporter.js +2 -2
- package/package.json +1 -1
- package/styles/index.css +1 -0
- package/styles/theme.css +18 -12
package/conventions/AGENTS.md
CHANGED
|
@@ -6,7 +6,11 @@ React Router v7, TanStack Query. The shared layer — primitives, the
|
|
|
6
6
|
`DataTable`/`EntityView`/`EntityForm` composites, the app shell, auth,
|
|
7
7
|
the gateway error seam, design tokens, lint rules — is the npm package
|
|
8
8
|
`@tristan2828/ui-foundation`, updated by bumping its version. Everything in
|
|
9
|
-
`src/` is this app's own.
|
|
9
|
+
`src/` is this app's own. The backend's auth and security layer (sign-in,
|
|
10
|
+
sessions, limits, TOTP, error reporting, deploy checks) is the pip package
|
|
11
|
+
`ui-foundation-backend` (`foundation_backend`), pinned in
|
|
12
|
+
`backend/pyproject.toml` to the same version; never copy one of its
|
|
13
|
+
modules into `backend/app/`.
|
|
10
14
|
|
|
11
15
|
Contract-first: `openapi.yaml` is the source of truth and is owned by this
|
|
12
16
|
app. In development MSW serves the contract, so there may be no backend at
|
|
@@ -605,10 +605,11 @@ asyncio is a `MissingGreenlet` and a 500.
|
|
|
605
605
|
- **After a commit, read by a `user_id` saved before it, never by the
|
|
606
606
|
`User`.** Take `user_id = user.id` at the top of the handler and use that
|
|
607
607
|
from then on; or `await session.refresh(user)` first, as `_start_session`
|
|
608
|
-
in `routers/auth.py` does to return it.
|
|
608
|
+
in `foundation_backend/routers/auth.py` does to return it.
|
|
609
609
|
- **Test signed in for real.** The backend's `client` fixture
|
|
610
610
|
(`tests/conftest.py`) holds a `sessions` row and sends its cookie,
|
|
611
|
-
so the real `get_current_user` runs (`
|
|
611
|
+
so the real `get_current_user` runs (`session_cookie` from
|
|
612
|
+
`foundation_backend.testing.signed_in`). Never stand
|
|
612
613
|
a `User(...)` in for `get_current_user` in an override: a detached user
|
|
613
614
|
never expires, so the test passes and the route fails live.
|
|
614
615
|
|
|
@@ -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
|
package/dist/api/gateway/auth.js
CHANGED
|
@@ -28,7 +28,8 @@ export async function loginTotp(input) {
|
|
|
28
28
|
return { user: res.body };
|
|
29
29
|
}
|
|
30
30
|
// Auto-logs in on success — the backend sets the same session cookie
|
|
31
|
-
// /auth/login does (see backend
|
|
31
|
+
// /auth/login does (see _start_session in the backend package's
|
|
32
|
+
// foundation_backend/routers/auth.py), so
|
|
32
33
|
// the gateway shape is identical to login's: a User on 200, AppError
|
|
33
34
|
// otherwise (a duplicate email arrives as kind:'validation' with
|
|
34
35
|
// fieldErrors.email, same as any other field error).
|
|
@@ -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;
|
|
@@ -10,8 +10,8 @@ export function resolveErrorReporting(option) {
|
|
|
10
10
|
return option === true ? {} : option;
|
|
11
11
|
}
|
|
12
12
|
// A query string or fragment in a URL or path, up to a stack frame's
|
|
13
|
-
// trailing :line:col (the backend's scrub, in
|
|
14
|
-
// the same rule).
|
|
13
|
+
// trailing :line:col (the backend's scrub, in
|
|
14
|
+
// foundation_backend/error_reporting.py, is the same rule).
|
|
15
15
|
const URL_QUERY = /(\b[a-z][a-z0-9+.-]*:\/\/[^\s?#"'()]+|(?<![\w/])\/[^\s?#"'()]*)[?#][^\s"'()]*?(?=(?::\d+){1,2}(?=[\s"'()]|$)|[\s"'()]|$)/gi;
|
|
16
16
|
export function scrub(text, limit) {
|
|
17
17
|
const cut = text.replace(URL_QUERY, "$1");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.25.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);
|