@apollion-dsi/tokens 5.6.0 → 5.8.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/README.md +52 -38
- package/lib/build.cjs +10 -9
- package/lib/build.esm.js +10 -9
- package/lib/builders/colors.d.ts +27 -0
- package/lib/cli.mjs +13 -12
- package/lib/config-loader.cjs +1 -1
- package/lib/config-loader.esm.js +2 -2
- package/lib/config-schema.cjs +1 -1
- package/lib/runtime/v1.cjs +1 -1
- package/lib/runtime/v1.esm.js +1 -1
- package/package.json +1 -1
- package/src/build.ts +28 -1
- package/src/builders/breakpoints.ts +25 -0
- package/src/builders/colors.ts +34 -3
- package/src/builders/ladder.ts +116 -0
- package/src/builders/layout.ts +24 -0
- package/src/config-schema-runtime.ts +1 -0
- package/src/config-schema.ts +5 -0
- package/src/ir.ts +90 -7
- package/src/renderers/css.ts +48 -0
- package/src/renderers/tailwind.ts +14 -3
- package/src/theme-factory.ts +5 -1
- package/src/token-meta.ts +9 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout builder — **Strangler Fig duplicate** of
|
|
3
|
+
* `@apollion-dsi/core/themes/layout` (ADR-025 invariant 5).
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the container-size scale (the `cq` channel thresholds — px, slot
|
|
6
|
+
* widths, NOT viewport breakpoints) and the page-layout constants
|
|
7
|
+
* (`pageMaxWidth`). Axis-invariant.
|
|
8
|
+
*
|
|
9
|
+
* @see ./colors.ts (Strangler Fig contract notes)
|
|
10
|
+
*/
|
|
11
|
+
import type { ContainerSizesInput, LayoutInput } from '@apollion-dsi/core/themes/layout';
|
|
12
|
+
import { defaultInputContainerSizes, defaultInputLayout } from '@apollion-dsi/core/themes/layout';
|
|
13
|
+
|
|
14
|
+
/** Resolved `[key, '<value>px']` pairs of the container-size scale, ascending. */
|
|
15
|
+
export function containerSizeEntries(
|
|
16
|
+
input: ContainerSizesInput = defaultInputContainerSizes,
|
|
17
|
+
): ReadonlyArray<readonly [string, string]> {
|
|
18
|
+
return Object.entries(input.values).map(([key, value]) => [key, `${value}px`] as const);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Resolved `[key, '<value>px']` pairs of the page-layout constants. */
|
|
22
|
+
export function layoutEntries(input: LayoutInput = defaultInputLayout): ReadonlyArray<readonly [string, string]> {
|
|
23
|
+
return [['page-max-width', `${input.pageMaxWidth}px`] as const];
|
|
24
|
+
}
|
package/src/config-schema.ts
CHANGED
|
@@ -45,6 +45,11 @@ export interface ApollionOutputFlags {
|
|
|
45
45
|
* channel-triplet companion vars to `dist/css/` so opaque colour tokens
|
|
46
46
|
* support Tailwind's alpha modifier. See `renderers/tailwind.ts`. */
|
|
47
47
|
tailwind?: boolean;
|
|
48
|
+
/** `dist/css/<brand>.<surface>.<dimension>.modes.css` — both modes in ONE file,
|
|
49
|
+
* scoped by `html[data-apollion-mode]` (+ `prefers-color-scheme` fallback) the
|
|
50
|
+
* way the core mode engine scopes them (ADR-024). Requires `modes` to include
|
|
51
|
+
* both `light` and `dark`. See `renderers/css.ts` (`renderCssModes`). */
|
|
52
|
+
cssModes?: boolean;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
export interface ApollionConfig {
|
package/src/ir.ts
CHANGED
|
@@ -38,6 +38,8 @@ import type { SpacingAlias } from '@apollion-dsi/core/themes/spacing';
|
|
|
38
38
|
import { defaultInputSpacing } from '@apollion-dsi/core/themes/spacing';
|
|
39
39
|
import { converter, formatHex, parse } from 'culori';
|
|
40
40
|
|
|
41
|
+
import { breakpointEntries } from './builders/breakpoints';
|
|
42
|
+
import { containerSizeEntries, layoutEntries } from './builders/layout';
|
|
41
43
|
import { createSpacing } from './builders/spacing';
|
|
42
44
|
import type { Variant } from './config-schema';
|
|
43
45
|
import type { ColorsThemeInterface } from './theme-factory';
|
|
@@ -265,6 +267,11 @@ function buildColorPrimitives(colors: ColorsThemeInterface): IRGroup {
|
|
|
265
267
|
}
|
|
266
268
|
out.grayscale = recordColorGroup(colors.grayscale);
|
|
267
269
|
out.neutral = recordColorGroup(colors.neutral);
|
|
270
|
+
// ADR-023 — surface ladder roles (mode-aware grounds / inks / edges / washes).
|
|
271
|
+
out.surface = recordColorGroup(colors.surface as unknown as Record<string, string>);
|
|
272
|
+
out.ink = recordColorGroup(colors.ink as unknown as Record<string, string>);
|
|
273
|
+
out.edge = recordColorGroup(colors.edge as unknown as Record<string, string>);
|
|
274
|
+
out.wash = recordColorGroup(colors.wash as unknown as Record<string, string>);
|
|
268
275
|
out.baseDark = colorToken(colors.baseDark ?? '#000000');
|
|
269
276
|
out.baseLight = colorToken(colors.baseLight ?? '#ffffff');
|
|
270
277
|
out.deepDark = colorToken(colors.deepDark ?? '#000000');
|
|
@@ -317,6 +324,11 @@ const BG_REF: Readonly<Record<string, string>> = {
|
|
|
317
324
|
danger: 'color.danger.base',
|
|
318
325
|
warning: 'color.warning.base',
|
|
319
326
|
info: 'color.information.base',
|
|
327
|
+
canvas: 'color.surface.canvas',
|
|
328
|
+
sunken: 'color.surface.sunken',
|
|
329
|
+
paper: 'color.surface.paper',
|
|
330
|
+
raised: 'color.surface.raised',
|
|
331
|
+
overlay: 'color.surface.overlay',
|
|
320
332
|
};
|
|
321
333
|
|
|
322
334
|
const TEXT_REF: Readonly<Record<string, string>> = {
|
|
@@ -327,6 +339,17 @@ const TEXT_REF: Readonly<Record<string, string>> = {
|
|
|
327
339
|
onDanger: 'color.baseLight',
|
|
328
340
|
onWarning: 'color.baseDark',
|
|
329
341
|
onInfo: 'color.baseLight',
|
|
342
|
+
primary: 'color.ink.primary',
|
|
343
|
+
secondary: 'color.ink.secondary',
|
|
344
|
+
muted: 'color.ink.muted',
|
|
345
|
+
disabled: 'color.ink.disabled',
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/** Foundation `edge.*` → `color.edge.*` primitive (ADR-023 ink-alpha lines). */
|
|
349
|
+
const EDGE_REF: Readonly<Record<string, string>> = {
|
|
350
|
+
hairline: 'color.edge.hairline',
|
|
351
|
+
border: 'color.edge.border',
|
|
352
|
+
strong: 'color.edge.strong',
|
|
330
353
|
};
|
|
331
354
|
|
|
332
355
|
/** Navigate the structural palette by IR path (`color.x.y`). */
|
|
@@ -640,10 +663,52 @@ export function assertSlicesMetaComplete(slices: readonly IRSlice[]): void {
|
|
|
640
663
|
}
|
|
641
664
|
|
|
642
665
|
/**
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
666
|
+
* Viewport breakpoint scale (APOLLION-BREAKPOINT-TOKENS) — axis-invariant,
|
|
667
|
+
* px by contract, half-open `[value, next)` bands over `min-width` sets.
|
|
668
|
+
* Values mirror `@apollion-dsi/core/themes/breakpoints` via the Strangler Fig
|
|
669
|
+
* builder; the parity suite pins the mirror.
|
|
670
|
+
*
|
|
671
|
+
* CSS emission (`--apollion-breakpoint-*`) is **informational only**: `var()`
|
|
672
|
+
* is invalid inside a `@media` prelude, so those custom properties serve JS
|
|
673
|
+
* `getComputedStyle` readers, never media queries. Query-capable surfaces are
|
|
674
|
+
* the TS record and the Tailwind `screens` scale (literal px).
|
|
675
|
+
*/
|
|
676
|
+
function buildBreakpoints(): IRGroup {
|
|
677
|
+
const out: Record<string, IRToken> = {};
|
|
678
|
+
for (const [key, px] of breakpointEntries()) out[key] = { type: 'dimension', value: toDimensionValue(px) };
|
|
679
|
+
return out;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Container-size scale + page-layout constants (ADR-025 invariant 5) —
|
|
684
|
+
* axis-invariant mirrors of `theme.containerSizes` / `theme.layout`.
|
|
685
|
+
* `container-size.*` are the `cq` channel thresholds (slot widths, a
|
|
686
|
+
* deliberately separate scale from `breakpoint.*`).
|
|
687
|
+
*/
|
|
688
|
+
function buildContainerSizes(): IRGroup {
|
|
689
|
+
const out: Record<string, IRToken> = {};
|
|
690
|
+
for (const [key, px] of containerSizeEntries()) out[key] = { type: 'dimension', value: toDimensionValue(px) };
|
|
691
|
+
return out;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function buildLayoutConstants(): IRGroup {
|
|
695
|
+
const out: Record<string, IRToken> = {};
|
|
696
|
+
for (const [key, px] of layoutEntries()) out[key] = { type: 'dimension', value: toDimensionValue(px) };
|
|
697
|
+
return out;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** `$extensions` payload documenting the breakpoint interval contract. */
|
|
701
|
+
const BREAKPOINT_EXTENSION = {
|
|
702
|
+
unit: 'px',
|
|
703
|
+
interval: 'half-open [value, next) — min-width sets, ascending',
|
|
704
|
+
cssVarsUsage: 'informational only — var() is invalid inside a @media prelude',
|
|
705
|
+
} as const;
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Axis-invariant base slice: border (radius/width/style) + the viewport
|
|
709
|
+
* `breakpoint.*` scale. `border.width` references the `border-width.*`
|
|
710
|
+
* primitives, which travel WITH this slice (`border-width` namespace) so the
|
|
711
|
+
* set is reference-closed. Identical for every brand/mode/surface/dimension.
|
|
647
712
|
*
|
|
648
713
|
* **ADR-011:** font left the base set — its `size` scale is now density-aware,
|
|
649
714
|
* so it lives in per-dimension `font.<dimension>` sets ({@link buildFontIR}),
|
|
@@ -654,13 +719,22 @@ export function buildBaseIR(): IRSlice {
|
|
|
654
719
|
const primitives: IRGroup = { borderWidth };
|
|
655
720
|
const border = buildBorder(primitives);
|
|
656
721
|
|
|
657
|
-
const extensions: Record<string, unknown> = {};
|
|
722
|
+
const extensions: Record<string, unknown> = { 'com.apollion.breakpoint': BREAKPOINT_EXTENSION };
|
|
658
723
|
if (Object.keys(border.extensions).length > 0) {
|
|
659
724
|
extensions['com.apollion.border'] = { radius: border.extensions };
|
|
660
725
|
}
|
|
661
726
|
|
|
662
727
|
const seen = new Set<string>();
|
|
663
|
-
const groups = decorateSlice(
|
|
728
|
+
const groups = decorateSlice(
|
|
729
|
+
{
|
|
730
|
+
'border-width': borderWidth,
|
|
731
|
+
border: border.group,
|
|
732
|
+
breakpoint: buildBreakpoints(),
|
|
733
|
+
'container-size': buildContainerSizes(),
|
|
734
|
+
layout: buildLayoutConstants(),
|
|
735
|
+
},
|
|
736
|
+
seen,
|
|
737
|
+
);
|
|
664
738
|
return { groups, extensions };
|
|
665
739
|
}
|
|
666
740
|
|
|
@@ -691,6 +765,7 @@ export function buildColorIR(foundation: FoundationLayer, colors: ColorsThemeInt
|
|
|
691
765
|
color,
|
|
692
766
|
bg: colorRefGroup(foundation.bg as unknown as Record<string, string>, colors, BG_REF),
|
|
693
767
|
text: colorRefGroup(foundation.text as unknown as Record<string, string>, colors, TEXT_REF),
|
|
768
|
+
edge: colorRefGroup(foundation.edge as unknown as Record<string, string>, colors, EDGE_REF),
|
|
694
769
|
tx: buildReadableTextGroup(colors),
|
|
695
770
|
shadow: buildShadow(colors),
|
|
696
771
|
},
|
|
@@ -737,7 +812,11 @@ export function buildIR(foundation: FoundationLayer, colors: ColorsThemeInterfac
|
|
|
737
812
|
const border = buildBorder(primitives);
|
|
738
813
|
const font = buildFont(variant.dimension);
|
|
739
814
|
|
|
740
|
-
const extensions: Record<string, unknown> = {
|
|
815
|
+
const extensions: Record<string, unknown> = {
|
|
816
|
+
'com.apollion.variant': meta,
|
|
817
|
+
'com.apollion.font': font.extensions,
|
|
818
|
+
'com.apollion.breakpoint': BREAKPOINT_EXTENSION,
|
|
819
|
+
};
|
|
741
820
|
if (Object.keys(border.extensions).length > 0) {
|
|
742
821
|
extensions['com.apollion.border'] = { radius: border.extensions };
|
|
743
822
|
}
|
|
@@ -749,9 +828,13 @@ export function buildIR(foundation: FoundationLayer, colors: ColorsThemeInterfac
|
|
|
749
828
|
groups: {
|
|
750
829
|
bg: colorRefGroup(foundation.bg as unknown as Record<string, string>, colors, BG_REF),
|
|
751
830
|
text: colorRefGroup(foundation.text as unknown as Record<string, string>, colors, TEXT_REF),
|
|
831
|
+
edge: colorRefGroup(foundation.edge as unknown as Record<string, string>, colors, EDGE_REF),
|
|
752
832
|
tx: buildReadableTextGroup(colors),
|
|
753
833
|
spacing: dimensionRefGroup(foundation.spacing as unknown as Record<string, string>, primitives, SPACE_REF),
|
|
754
834
|
border: border.group,
|
|
835
|
+
breakpoint: buildBreakpoints(),
|
|
836
|
+
'container-size': buildContainerSizes(),
|
|
837
|
+
layout: buildLayoutConstants(),
|
|
755
838
|
font: font.group,
|
|
756
839
|
shadow: buildShadow(colors),
|
|
757
840
|
},
|
package/src/renderers/css.ts
CHANGED
|
@@ -90,3 +90,51 @@ export function renderCss(ir: IRDocument, options: RenderCssOptions = {}): strin
|
|
|
90
90
|
|
|
91
91
|
return lines.join('\n');
|
|
92
92
|
}
|
|
93
|
+
|
|
94
|
+
/** Options of {@link renderCssModes}. */
|
|
95
|
+
export interface RenderCssModesOptions extends RenderCssOptions {
|
|
96
|
+
/** `<html>` attribute carrying the resolved mode (same default as the core mode engine). */
|
|
97
|
+
attribute?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Mode-scoped CSS (APOLLION-DARK-MODE-ENGINE, ADR-024 — `output.cssModes`):
|
|
102
|
+
* ONE file per (brand × surface × dimension) carrying BOTH modes, scoped the
|
|
103
|
+
* way the core mode engine scopes them — `:root`/`[attr="light"]` for light,
|
|
104
|
+
* `[attr="dark"]` for dark, plus a `prefers-color-scheme` fallback for pages
|
|
105
|
+
* that never set the attribute. Consumers without React link this file and
|
|
106
|
+
* flip the mode with the attribute (`getModeScript` / `syncDocumentMode`)
|
|
107
|
+
* — the "CSS-only switch" of the docs, made real.
|
|
108
|
+
*/
|
|
109
|
+
export function renderCssModes(light: IRDocument, dark: IRDocument, options: RenderCssModesOptions = {}): string {
|
|
110
|
+
const { attribute = 'data-apollion-mode', rgbCompanions } = options;
|
|
111
|
+
const { brand, surface, dimension } = light.meta;
|
|
112
|
+
const lines: string[] = [HEADER_LINE, ''];
|
|
113
|
+
|
|
114
|
+
const block = (selector: string, ir: IRDocument, scheme: 'light' | 'dark'): void => {
|
|
115
|
+
lines.push(`${selector} {`, ` color-scheme: ${scheme};`);
|
|
116
|
+
emitVars(ir.groups, [], lines);
|
|
117
|
+
if (rgbCompanions) emitRgbVars(ir.groups, [], lines);
|
|
118
|
+
lines.push('}', '');
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
lines.push(
|
|
122
|
+
`/* Variant: brand=${brand} surface=${surface} dimension=${dimension} modes=light+dark (attribute ${attribute}) */`,
|
|
123
|
+
'',
|
|
124
|
+
);
|
|
125
|
+
block(`:root, html[${attribute}="light"]`, light, 'light');
|
|
126
|
+
block(`html[${attribute}="dark"]`, dark, 'dark');
|
|
127
|
+
lines.push('@media (prefers-color-scheme: dark) {');
|
|
128
|
+
lines.push(` html:not([${attribute}]) {`, ' color-scheme: dark;');
|
|
129
|
+
const inner: string[] = [];
|
|
130
|
+
emitVars(dark.groups, [], inner);
|
|
131
|
+
if (rgbCompanions) emitRgbVars(dark.groups, [], inner);
|
|
132
|
+
for (const line of inner) lines.push(` ${line}`);
|
|
133
|
+
lines.push(' }', '}', '');
|
|
134
|
+
|
|
135
|
+
lines.push('@supports (background: paint(squircle)) or (color: oklch(0% 0 0)) {');
|
|
136
|
+
emitProperties(light.groups, [], lines);
|
|
137
|
+
lines.push('}', '');
|
|
138
|
+
|
|
139
|
+
return lines.join('\n');
|
|
140
|
+
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
import { converter, parse } from 'culori';
|
|
31
31
|
|
|
32
32
|
import type { IRColorValue, IRDocument, IRGroup, IRToken } from '../ir';
|
|
33
|
-
import { isToken, kebab } from '../ir';
|
|
33
|
+
import { isToken, kebab, rawValue } from '../ir';
|
|
34
34
|
|
|
35
35
|
const toRgb = converter('rgb');
|
|
36
36
|
|
|
@@ -91,13 +91,13 @@ interface ColorEntry {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* Colour entries for the preset's `colors` scale: the `bg`/`text`/`tx`
|
|
94
|
+
* Colour entries for the preset's `colors` scale: the `bg`/`text`/`edge`/`tx`
|
|
95
95
|
* foundation groups, keyed `<group>-<path>` (the CSS var name minus the
|
|
96
96
|
* `--apollion-` prefix — one mental model across surfaces).
|
|
97
97
|
*/
|
|
98
98
|
function colorEntries(ir: IRDocument): ColorEntry[] {
|
|
99
99
|
const out: ColorEntry[] = [];
|
|
100
|
-
for (const groupName of ['bg', 'text', 'tx'] as const) {
|
|
100
|
+
for (const groupName of ['bg', 'text', 'edge', 'tx'] as const) {
|
|
101
101
|
for (const [key, token] of flatTokens(groupAt(ir.groups, groupName), [groupName])) {
|
|
102
102
|
const color = colorPayload(token);
|
|
103
103
|
if (!color) continue;
|
|
@@ -145,6 +145,17 @@ export function renderTailwindPreset(ir: IRDocument): string {
|
|
|
145
145
|
}
|
|
146
146
|
out.push(' },');
|
|
147
147
|
|
|
148
|
+
// `screens` takes LITERAL px, never `var()` — custom properties are invalid
|
|
149
|
+
// inside a `@media` prelude. `xs` (0) is the unprefixed base band, omitted by
|
|
150
|
+
// Tailwind convention. APOLLION-BREAKPOINT-TOKENS.
|
|
151
|
+
pushScale(
|
|
152
|
+
out,
|
|
153
|
+
'screens',
|
|
154
|
+
[...flatTokens(groupAt(ir.groups, 'breakpoint'))]
|
|
155
|
+
.filter(([key]) => key !== 'xs')
|
|
156
|
+
.map(([key, token]) => [key, rawValue(token.value)] as const),
|
|
157
|
+
);
|
|
158
|
+
|
|
148
159
|
pushScale(out, 'spacing', varEntries(groupAt(ir.groups, 'spacing'), ['spacing']));
|
|
149
160
|
pushScale(out, 'borderRadius', varEntries(groupAt(ir.groups, 'border', 'radius'), ['border', 'radius']));
|
|
150
161
|
pushScale(out, 'borderWidth', varEntries(groupAt(ir.groups, 'border', 'width'), ['border', 'width']));
|
package/src/theme-factory.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { defaultInputSpacing } from '@apollion-dsi/core/themes/spacing';
|
|
|
32
32
|
import { invertForSurface } from '@apollion-dsi/core/themes/surface';
|
|
33
33
|
|
|
34
34
|
import { mountGreyscaleColors, mountNeutralColors, mountSingleColor } from './builders/colors';
|
|
35
|
+
import { resolveSurfaceLadder } from './builders/ladder';
|
|
35
36
|
import { createSpacing } from './builders/spacing';
|
|
36
37
|
import type { Variant } from './config-schema';
|
|
37
38
|
|
|
@@ -45,14 +46,17 @@ export interface VariantThemes {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
function composeColors(input: Variant['colors']) {
|
|
49
|
+
const neutral = mountNeutralColors(input);
|
|
48
50
|
return {
|
|
49
51
|
baseDark: input.baseDark,
|
|
50
52
|
baseLight: input.baseLight,
|
|
51
53
|
deepDark: input.deepDark,
|
|
52
54
|
deepLight: input.deepLight,
|
|
53
55
|
transparent: input.transparent ?? 'transparent',
|
|
54
|
-
neutral
|
|
56
|
+
neutral,
|
|
55
57
|
grayscale: mountGreyscaleColors(input),
|
|
58
|
+
// ADR-023 — surface ladder (mirror of core's createColors composition).
|
|
59
|
+
...resolveSurfaceLadder({ ...input, neutral }, input.ladder),
|
|
56
60
|
main: mountSingleColor(input.main!, input),
|
|
57
61
|
opposite: mountSingleColor(input.opposite!, input),
|
|
58
62
|
complementary: mountSingleColor(input.complementary!, input),
|
package/src/token-meta.ts
CHANGED
|
@@ -48,6 +48,15 @@ export const TOKEN_META: Readonly<Record<string, TokenMetaEntry>> = {
|
|
|
48
48
|
deprecated: 'Use bg.information once the foundation role is renamed (tracked for S6).',
|
|
49
49
|
},
|
|
50
50
|
|
|
51
|
+
// ── Surface ladder (ADR-023 — mode-aware grounds, inks, edges) ───────────
|
|
52
|
+
'bg.canvas': { description: 'Page ground — the theme canvas (never pure black in dark).' },
|
|
53
|
+
'bg.paper': { description: 'Component ground (cards, inputs, tables) — lighter than the canvas in both modes.' },
|
|
54
|
+
'bg.overlay': { description: 'Top of the elevation ladder — modal / dialog panels.' },
|
|
55
|
+
'text.primary': { description: 'Primary neutral ink — AA + APCA Lc ≥ 75 over bg.paper and bg.canvas.' },
|
|
56
|
+
'text.muted': { description: 'Muted neutral ink — AA + APCA Lc ≥ 45 over bg.paper.' },
|
|
57
|
+
'edge.border': { description: 'Default control outline — primary ink at 20 % alpha, legible on any ground.' },
|
|
58
|
+
'edge.hairline': { description: 'Dividers and card outlines — primary ink at 10 % alpha.' },
|
|
59
|
+
|
|
51
60
|
// ── Foundation text-on roles ─────────────────────────────────────────────
|
|
52
61
|
'text.on-primary': { description: 'Text/icon colour with guaranteed contrast over bg.primary.' },
|
|
53
62
|
'text.on-danger': { description: 'Text/icon colour with guaranteed contrast over bg.danger.' },
|