@vectoriox/iox-ui 5.10.0 → 5.12.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/design-core/index.d.ts +8156 -8953
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +9795 -10645
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +153 -11334
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/index.d.ts +69 -9346
- package/package.json +1 -1
- package/src/design-core/brand.ts +36 -3
- package/src/design-core/design-token.model.ts +14 -14
- package/src/design-core/element-styles.ts +271 -0
- package/src/design-core/flavor-spec.generated.ts +9454 -10524
- package/src/design-core/flavor-tokens.ts +7 -3
- package/src/design-core/flavor.ts +11 -88
- package/src/design-core/fonts.ts +14 -4
- package/src/design-core/public-api.ts +1 -0
- package/src/design-core/spring.ts +21 -9
- package/src/design-core/token-css.ts +100 -31
- package/styles/iox-forms.css +56 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { deriveAccentTokens } from './brand';
|
|
2
2
|
import { cssString } from './fonts';
|
|
3
|
-
import {
|
|
3
|
+
import { SPEC_AXES } from './flavor-spec.generated';
|
|
4
4
|
import {
|
|
5
5
|
DesignChoices, DesignTokenRow, FlavorPackV2, FlavorSpecToken, flavorSourceStamp, USER_TOKEN_SOURCE,
|
|
6
6
|
} from './design-token.model';
|
|
@@ -70,7 +70,7 @@ export function buildFlavorTokens(pack: FlavorPackV2, choices: DesignChoices): D
|
|
|
70
70
|
const pair = pack.pairs.find(p => p.id === choices.pairId) ?? pack.pairs[0];
|
|
71
71
|
for (const [name, value] of Object.entries(pair?.tokens ?? {})) set(name, value);
|
|
72
72
|
|
|
73
|
-
const axes =
|
|
73
|
+
const axes = SPEC_AXES;
|
|
74
74
|
for (const [name, value] of [
|
|
75
75
|
...axisValues(axes.shapes, choices.shape),
|
|
76
76
|
...axisValues(axes.densities, choices.density),
|
|
@@ -86,7 +86,11 @@ export function buildFlavorTokens(pack: FlavorPackV2, choices: DesignChoices): D
|
|
|
86
86
|
|
|
87
87
|
if (choices.seed && choices.seed.toLowerCase() !== pack.seed.toLowerCase()) {
|
|
88
88
|
const bg = byName.get('bg');
|
|
89
|
-
|
|
89
|
+
const inverseBg = byName.get('inverse-bg');
|
|
90
|
+
for (const accent of deriveAccentTokens(choices.seed, {
|
|
91
|
+
bg: bg?.value ?? '', bgDark: bg?.values?.dark,
|
|
92
|
+
inverseBg: inverseBg?.value, inverseBgDark: inverseBg?.values?.dark,
|
|
93
|
+
})) {
|
|
90
94
|
const row = byName.get(accent.name);
|
|
91
95
|
if (!row) continue;
|
|
92
96
|
row.value = accent.value;
|
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import { styleKeyToKebab } from './css-names';
|
|
2
1
|
import { DesignTokenGroup, DesignTokenTier, DesignTokenType } from './design-token.model';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
* Flavors (flavors-and-theming-plan.md, F1):
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* check that every referenced token exists.
|
|
11
|
-
*
|
|
12
|
-
* The fallback is the bootstrap: until F2 seeds the org's `designtokens` rows, no `--iox-*`
|
|
13
|
-
* var is defined anywhere, and `var(--iox-radius-control, 4px)` must still paint identically
|
|
14
|
-
* in the builder canvas and the SSR page. Once tokens ARE seeded (or re-themed), the var wins
|
|
15
|
-
* over the fallback everywhere — which is exactly the §3 propagation story.
|
|
4
|
+
* Flavors v1 (flavors-and-theming-plan.md, F1–F2): a pack is its TOKENS plus the accent hue
|
|
5
|
+
* opinion the brand derivation reads. The per-type `defaults` map and its resolvers are gone
|
|
6
|
+
* (design-system plan §3e, DS2): a new element is born wearing an ELEMENT STYLE
|
|
7
|
+
* (`element-styles.ts`), which references tokens and is flavor-independent. Swiss survives only
|
|
8
|
+
* until DS4 migrates orgs to Crisp.
|
|
16
9
|
*/
|
|
17
10
|
|
|
18
11
|
export interface FlavorTokenDef {
|
|
@@ -32,82 +25,24 @@ export interface FlavorPack {
|
|
|
32
25
|
label: string;
|
|
33
26
|
blurb: string;
|
|
34
27
|
tokens: FlavorTokenDef[];
|
|
35
|
-
/** type → styleTrait → TOKEN NAME (never a CSS value — see the module header). */
|
|
36
|
-
defaults: Record<string, Record<string, string>>;
|
|
37
28
|
/** F2: the accent's hue rotation (degrees in OKLab) from the brand seed — a pack opinion. */
|
|
38
29
|
accentHueDeg?: number;
|
|
39
30
|
}
|
|
40
31
|
|
|
41
|
-
/**
|
|
42
|
-
export function
|
|
43
|
-
type: string,
|
|
44
|
-
pack: FlavorPack | null | undefined,
|
|
45
|
-
): Record<string, string> {
|
|
46
|
-
const map = pack?.defaults?.[type];
|
|
47
|
-
if (!map) return {};
|
|
48
|
-
const byName = new Map((pack!.tokens ?? []).map(t => [t.name, t.value]));
|
|
49
|
-
const out: Record<string, string> = {};
|
|
50
|
-
for (const [trait, tokenName] of Object.entries(map)) {
|
|
51
|
-
const fallback = byName.get(tokenName);
|
|
52
|
-
if (fallback === undefined) continue; // validator fails the build; runtime stays safe
|
|
53
|
-
out[trait] = `var(--iox-${tokenName}, ${fallback})`;
|
|
54
|
-
}
|
|
55
|
-
return out;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* The ENGINE's half of the cascade: kebab `prop: var(…)` declarations for every pack default
|
|
60
|
-
* the node's persisted styleProps does NOT set. Appended beside renderDefaultDeclarations —
|
|
61
|
-
* same guard-on-undefined discipline, so a node saved post-F1 (var already serialised via the
|
|
62
|
-
* builder's trait defaults) is untouched, and a legacy node picks the pack default up at
|
|
63
|
-
* render time. Node styleProps always wins.
|
|
64
|
-
*/
|
|
65
|
-
export function flavorDefaultDeclarations(
|
|
66
|
-
type: string,
|
|
67
|
-
styleProps: Record<string, any> | null | undefined,
|
|
68
|
-
pack: FlavorPack | null | undefined,
|
|
69
|
-
): string[] {
|
|
70
|
-
const sp = styleProps ?? {};
|
|
71
|
-
const out: string[] = [];
|
|
72
|
-
for (const [trait, value] of Object.entries(resolveFlavorDefaults(type, pack))) {
|
|
73
|
-
if (sp[trait] === undefined) out.push(`${styleKeyToKebab(trait)}: ${value}`);
|
|
74
|
-
}
|
|
75
|
-
return out;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* The CI gate (flavors plan §10 C4): a pack may only name types and traits that exist in the
|
|
80
|
-
* live component registry, and tokens it defines itself. Returns human-readable errors; a
|
|
81
|
-
* consumer spec feeds it the REAL registry and asserts [].
|
|
82
|
-
*/
|
|
83
|
-
export function validateFlavorPack(
|
|
84
|
-
pack: FlavorPack,
|
|
85
|
-
knownTraitsByType: Record<string, ReadonlySet<string> | string[]>,
|
|
86
|
-
): string[] {
|
|
32
|
+
/** A pack's own sanity: every token row names itself and carries a value. Returns errors. */
|
|
33
|
+
export function validateFlavorPack(pack: FlavorPack): string[] {
|
|
87
34
|
const errors: string[] = [];
|
|
88
|
-
const tokenNames = new Set((pack.tokens ?? []).map(t => t.name));
|
|
89
35
|
for (const t of pack.tokens ?? []) {
|
|
90
36
|
if (!t.name) errors.push(`${pack.id}: token with empty name`);
|
|
91
37
|
if (!t.value) errors.push(`${pack.id}: token "${t.name}" has no value`);
|
|
92
38
|
}
|
|
93
|
-
for (const [type, traits] of Object.entries(pack.defaults ?? {})) {
|
|
94
|
-
const known = knownTraitsByType[type];
|
|
95
|
-
if (!known) { errors.push(`${pack.id}: unknown component type "${type}"`); continue; }
|
|
96
|
-
const knownSet = known instanceof Set ? known : new Set(known);
|
|
97
|
-
for (const [trait, tokenName] of Object.entries(traits)) {
|
|
98
|
-
if (!knownSet.has(trait)) errors.push(`${pack.id}: "${type}" has no style trait "${trait}"`);
|
|
99
|
-
if (!tokenNames.has(tokenName)) errors.push(`${pack.id}: "${type}.${trait}" references undefined token "${tokenName}"`);
|
|
100
|
-
if (/[:(]|^#|\d/.test(tokenName)) errors.push(`${pack.id}: "${type}.${trait}" must name a token, not a value ("${tokenName}")`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
39
|
return errors;
|
|
104
40
|
}
|
|
105
41
|
|
|
106
42
|
/**
|
|
107
|
-
* Swiss — the
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* legacy pages before anyone chose anything.
|
|
43
|
+
* Swiss — the F1 pack: four tokens (control/surface radius, control/stack spacing) and the hue
|
|
44
|
+
* opinion F2's brand step reads. Kept until DS4 migrates `flavorId: swiss` orgs to Crisp; its four
|
|
45
|
+
* rows are the spec's `legacyAliases`.
|
|
111
46
|
*/
|
|
112
47
|
export const SWISS_FLAVOR: FlavorPack = {
|
|
113
48
|
id: 'swiss',
|
|
@@ -120,18 +55,6 @@ export const SWISS_FLAVOR: FlavorPack = {
|
|
|
120
55
|
{ name: 'space-control', value: '10px 18px', type: 'spacing', group: 'space' },
|
|
121
56
|
{ name: 'space-stack', value: '12px', type: 'spacing', group: 'space' },
|
|
122
57
|
],
|
|
123
|
-
|
|
124
|
-
Button: { borderRadius: 'radius-control', padding: 'space-control' },
|
|
125
|
-
ButtonBlock: { borderRadius: 'radius-control', padding: 'space-control' },
|
|
126
|
-
Card: { borderRadius: 'radius-surface' },
|
|
127
|
-
},
|
|
128
|
-
// F2: Swiss turns the seed toward a cool counterpoint. NOTE deliberately: the brand tokens
|
|
129
|
-
// deriveBrandTokens() emits are SEEDED per org — they do not join `defaults` here, because a
|
|
130
|
-
// colour default repaints legacy nodes whose saved styleProps lack the key (the engine fills
|
|
131
|
-
// missing keys from config defaults). Coloured element defaults are a Swiss v2 upgrade
|
|
132
|
-
// decision (§10 C2), taken with the picker in front of the author — never a side effect.
|
|
58
|
+
// F2: Swiss turns the seed toward a cool counterpoint.
|
|
133
59
|
accentHueDeg: 150,
|
|
134
60
|
};
|
|
135
|
-
|
|
136
|
-
/** What both renderers use until F2 lets an org choose. */
|
|
137
|
-
export const DEFAULT_FLAVOR: FlavorPack = SWISS_FLAVOR;
|
package/src/design-core/fonts.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SPEC_FONTS } from './flavor-spec.generated';
|
|
2
2
|
import { DesignTokenLike } from './token-css';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -7,7 +7,7 @@ import { DesignTokenLike } from './token-css';
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/** Family name → Google Fonts css2 `family=` id. Replaces the engine's private GOOGLE_FONTS map. */
|
|
10
|
-
export const FONT_CATALOG: Readonly<Record<string, string>> =
|
|
10
|
+
export const FONT_CATALOG: Readonly<Record<string, string>> = SPEC_FONTS;
|
|
11
11
|
|
|
12
12
|
export type FontFormat = 'woff2' | 'woff' | 'truetype' | 'opentype';
|
|
13
13
|
export type FontStyle = 'normal' | 'italic';
|
|
@@ -30,9 +30,19 @@ const MIN_WEIGHT = 100;
|
|
|
30
30
|
const MAX_WEIGHT = 900;
|
|
31
31
|
const FONT_FORMATS: ReadonlyArray<FontFormat> = ['woff2', 'woff', 'truetype', 'opentype'];
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* A CSS string literal, single-quoted: `\` and `'` escaped, line breaks and NUL dropped, and `<` /
|
|
35
|
+
* `>` hex-escaped (`\3c `) — the face CSS is inlined into a `<style>` by the SSR engine, where an
|
|
36
|
+
* uploaded family named `</style><script>` would otherwise close the element.
|
|
37
|
+
*/
|
|
34
38
|
export function cssString(value: string): string {
|
|
35
|
-
|
|
39
|
+
const body = String(value)
|
|
40
|
+
.replace(/[\r\n\f\0]/g, '')
|
|
41
|
+
.replace(/\\/g, '\\\\')
|
|
42
|
+
.replace(/'/g, "\\'")
|
|
43
|
+
.replace(/</g, '\\3c ')
|
|
44
|
+
.replace(/>/g, '\\3e ');
|
|
45
|
+
return `'${body}'`;
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
const clampWeight = (w: unknown): number => {
|
|
@@ -13,6 +13,7 @@ export * from './design-token.model';
|
|
|
13
13
|
export * from './css-names';
|
|
14
14
|
export * from './token-css';
|
|
15
15
|
export * from './flavor';
|
|
16
|
+
export * from './element-styles';
|
|
16
17
|
export * from './brand';
|
|
17
18
|
export * from './flavor-tokens';
|
|
18
19
|
export * from './flavor-seed';
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* generator (`build-spec.mjs` `spring()`) uses to write `springs.lively`, so a spring computed at
|
|
4
4
|
* runtime is byte-identical to the one in the contract.
|
|
5
5
|
*
|
|
6
|
-
* `stiffness` is k (ω₀ = √k), `damping` the ratio
|
|
6
|
+
* `stiffness` is k (ω₀ = √k), `damping` the ratio ζ: < 1 under-damped, = 1 critically damped,
|
|
7
|
+
* > 1 over-damped. Both must be positive and finite — a zero or negative spring never settles.
|
|
7
8
|
* Duration = the last instant the curve is more than 0.1% away from rest, rounded up to 10ms.
|
|
8
9
|
*/
|
|
9
10
|
const SETTLE_TOLERANCE = 0.001;
|
|
@@ -13,20 +14,31 @@ const SAMPLES = 40;
|
|
|
13
14
|
const DECIMALS = 4;
|
|
14
15
|
const ROUND_TO_MS = 10;
|
|
15
16
|
|
|
17
|
+
/** Displacement from rest → target (0 → 1) of a unit-mass spring released at rest. */
|
|
18
|
+
function springPosition(w0: number, damping: number): (t: number) => number {
|
|
19
|
+
if (damping < 1) {
|
|
20
|
+
const wd = w0 * Math.sqrt(1 - damping * damping);
|
|
21
|
+
return t => 1 - Math.exp(-damping * w0 * t) * (Math.cos(wd * t) + ((damping * w0) / wd) * Math.sin(wd * t));
|
|
22
|
+
}
|
|
23
|
+
if (damping === 1) return t => 1 - Math.exp(-w0 * t) * (1 + w0 * t);
|
|
24
|
+
// Over-damped: two real, negative roots. Written with exponentials of the roots, not cosh/sinh,
|
|
25
|
+
// which overflow for a stiff, heavily damped spring well inside the settle horizon.
|
|
26
|
+
const root = w0 * Math.sqrt(damping * damping - 1);
|
|
27
|
+
const r1 = -damping * w0 + root;
|
|
28
|
+
const r2 = -damping * w0 - root;
|
|
29
|
+
return t => 1 - (r2 * Math.exp(r1 * t) - r1 * Math.exp(r2 * t)) / (r2 - r1);
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
export interface LinearSpring {
|
|
17
33
|
easing: string;
|
|
18
34
|
duration: string;
|
|
19
35
|
}
|
|
20
36
|
|
|
21
37
|
export function springToLinear(stiffness: number, damping: number): LinearSpring {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return 1 - Math.exp(-damping * w0 * t) * (Math.cos(wd * t) + ((damping * w0) / wd) * Math.sin(wd * t));
|
|
27
|
-
}
|
|
28
|
-
return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
|
|
29
|
-
};
|
|
38
|
+
if (!(Number.isFinite(stiffness) && stiffness > 0) || !(Number.isFinite(damping) && damping > 0)) {
|
|
39
|
+
throw new RangeError(`springToLinear: stiffness and damping must be > 0 (got ${stiffness}, ${damping})`);
|
|
40
|
+
}
|
|
41
|
+
const x = springPosition(Math.sqrt(stiffness), damping);
|
|
30
42
|
let settle = 0;
|
|
31
43
|
for (let t = 0; t < SETTLE_HORIZON_S; t += SETTLE_STEP_S) if (Math.abs(1 - x(t)) > SETTLE_TOLERANCE) settle = t;
|
|
32
44
|
const durationMs = Math.ceil((settle * 1000) / ROUND_TO_MS) * ROUND_TO_MS;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DesignMode } from './design-token.model';
|
|
2
|
+
import { SPEC_A11Y, SPEC_SCHEMES } from './flavor-spec.generated';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Design-token → CSS compilation, shared by BOTH renderers (flavors F0, design-system DS1).
|
|
@@ -40,24 +41,11 @@ export interface TokenCssBlocks {
|
|
|
40
41
|
dark: string;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
/** What `prefers-reduced-motion: reduce` collapses —
|
|
44
|
-
export const REDUCED_MOTION_OVERRIDES: Readonly<Record<string, string>> =
|
|
45
|
-
'dur-fast': '0ms',
|
|
46
|
-
'dur-base': '0ms',
|
|
47
|
-
'dur-slow': '0ms',
|
|
48
|
-
'hover-lift': '0px',
|
|
49
|
-
'button-hover-x': '0px',
|
|
50
|
-
'button-hover-y': '0px',
|
|
51
|
-
'card-hover-x': '0px',
|
|
52
|
-
'card-lift': '0px',
|
|
53
|
-
};
|
|
44
|
+
/** What `prefers-reduced-motion: reduce` collapses — the contract's complete list (`a11y.reducedMotion`). */
|
|
45
|
+
export const REDUCED_MOTION_OVERRIDES: Readonly<Record<string, string>> = SPEC_A11Y.reducedMotion;
|
|
54
46
|
|
|
55
|
-
/** What `prefers-reduced-transparency: reduce` makes solid —
|
|
56
|
-
export const REDUCED_TRANSPARENCY_OVERRIDES: Readonly<Record<string, string>> =
|
|
57
|
-
'surface-glass': 'var(--iox-surface)',
|
|
58
|
-
'surface-glass-strong': 'var(--iox-surface)',
|
|
59
|
-
'blur-surface': 'none',
|
|
60
|
-
};
|
|
47
|
+
/** What `prefers-reduced-transparency: reduce` makes solid — the contract's `a11y.reducedTransparency`. */
|
|
48
|
+
export const REDUCED_TRANSPARENCY_OVERRIDES: Readonly<Record<string, string>> = SPEC_A11Y.reducedTransparency;
|
|
61
49
|
|
|
62
50
|
const ROOT = ':root';
|
|
63
51
|
const DEFAULT_SCHEME = 'default';
|
|
@@ -95,7 +83,7 @@ const schemeSelector = (id: string, scope?: string) =>
|
|
|
95
83
|
scope ? `${scope} .iox-scheme-${id}` : `.iox-scheme-${id}`;
|
|
96
84
|
|
|
97
85
|
const nonDefaultSchemes = (): ReadonlyArray<[string, Readonly<Record<string, string>>]> =>
|
|
98
|
-
Object.entries(
|
|
86
|
+
Object.entries(SPEC_SCHEMES)
|
|
99
87
|
.filter(([id]) => id !== DEFAULT_SCHEME)
|
|
100
88
|
.map(([id, scheme]) => [id, scheme.set as Readonly<Record<string, string>>]);
|
|
101
89
|
|
|
@@ -116,32 +104,113 @@ export function tokensToDarkVarMap(tokens: ReadonlyArray<DesignTokenLike> | null
|
|
|
116
104
|
return map;
|
|
117
105
|
}
|
|
118
106
|
|
|
107
|
+
const TOKEN_REF = /var\(\s*--iox-([\w-]+)\s*(?:,([^()]*))?\)/g;
|
|
108
|
+
const PLACEHOLDER = /\{\{\s*([\w-]+)\s*\}\}/g;
|
|
109
|
+
|
|
110
|
+
/** A compiled colour mode — the contract's modes minus `auto`, which is a compile CHOICE, not values. */
|
|
111
|
+
export type ResolvedMode = Exclude<DesignMode, 'auto'>;
|
|
112
|
+
const LIGHT: ResolvedMode = 'light';
|
|
113
|
+
const DARK: ResolvedMode = 'dark';
|
|
114
|
+
|
|
115
|
+
const modeValue = (t: DesignTokenLike, mode: ResolvedMode): string => (mode === DARK ? darkOf(t) ?? t.value : t.value);
|
|
116
|
+
|
|
117
|
+
/** Every token's ROOT value in one mode, with `var(--iox-…)` references resolved against the same
|
|
118
|
+
* set (recursively; a reference cycle or an unknown token is left as written). */
|
|
119
|
+
export function resolveTokenValues(
|
|
120
|
+
tokens: ReadonlyArray<DesignTokenLike> | null | undefined,
|
|
121
|
+
mode: ResolvedMode = LIGHT,
|
|
122
|
+
): Record<string, string> {
|
|
123
|
+
const raw = new Map(usableTokens(tokens).map(t => [t.name, modeValue(t, mode)]));
|
|
124
|
+
const out: Record<string, string> = {};
|
|
125
|
+
const resolve = (name: string, trail: ReadonlySet<string>): string | null => {
|
|
126
|
+
if (name in out) return out[name];
|
|
127
|
+
const value = raw.get(name);
|
|
128
|
+
if (value === undefined || trail.has(name)) return null;
|
|
129
|
+
const next = new Set(trail).add(name);
|
|
130
|
+
return value.replace(TOKEN_REF, (ref, inner: string, fallback?: string) =>
|
|
131
|
+
resolve(inner, next) ?? (fallback !== undefined ? fallback.trim() : ref));
|
|
132
|
+
};
|
|
133
|
+
for (const name of raw.keys()) {
|
|
134
|
+
const value = resolve(name, new Set());
|
|
135
|
+
if (value !== null) out[name] = value;
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
119
140
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
|
|
141
|
+
* `value` with every `var(--iox-…)` replaced by that token's resolved root value in `mode` — what
|
|
142
|
+
* the Style panel's Detach writes, so the node keeps exactly what it showed as a literal. An unknown
|
|
143
|
+
* token falls back to its declared fallback, else stays as written.
|
|
144
|
+
*/
|
|
145
|
+
export function resolveTokenReferences(
|
|
146
|
+
value: string,
|
|
147
|
+
tokens: ReadonlyArray<DesignTokenLike> | null | undefined,
|
|
148
|
+
mode: ResolvedMode = LIGHT,
|
|
149
|
+
): string {
|
|
150
|
+
const resolved = resolveTokenValues(tokens, mode);
|
|
151
|
+
return value.replace(TOKEN_REF, (ref, name: string, fallback?: string) =>
|
|
152
|
+
resolved[name] ?? (fallback !== undefined ? fallback.trim() : ref));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** A scheme value with its `{{token}}` placeholders replaced by resolved root values — or null
|
|
156
|
+
* when a placeholder names a token the set does not have (the declaration is then omitted). */
|
|
157
|
+
function substitute(template: string, resolved: Readonly<Record<string, string>>): string | null {
|
|
158
|
+
let missing = false;
|
|
159
|
+
const value = template.replace(PLACEHOLDER, (_, name: string) => {
|
|
160
|
+
if (!(name in resolved)) missing = true;
|
|
161
|
+
return resolved[name] ?? '';
|
|
162
|
+
});
|
|
163
|
+
return missing ? null : value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const schemeEntries = (set: Readonly<Record<string, string>>, resolved: Readonly<Record<string, string>>) =>
|
|
167
|
+
Object.entries(set)
|
|
168
|
+
.map(([name, template]): [string, string | null] => [name, substitute(template, resolved)])
|
|
169
|
+
.filter((e): e is [string, string] => e[1] !== null);
|
|
170
|
+
|
|
171
|
+
const hasPlaceholder = (value: string) => value.includes('{{');
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Section colour schemes (design-system plan §3c). Two rules, both about WHERE a custom property
|
|
175
|
+
* resolves:
|
|
176
|
+
*
|
|
177
|
+
* - ⚠️ Custom properties resolve where they are DECLARED: `--iox-card-bg: var(--iox-surface)`
|
|
178
|
+
* declared on `:root` keeps `:root`'s surface inside `.iox-scheme-inverse`. So each scheme block
|
|
179
|
+
* carries the spec's overrides PLUS every other token whose value references a token,
|
|
180
|
+
* re-declared verbatim — re-resolving it in the section.
|
|
181
|
+
* - ⚠️ A scheme that SWAPS two tokens (Brand: accent ⇄ accent-ink) cannot write them as `var()`:
|
|
182
|
+
* on one element `accent: var(--iox-accent-ink)` + `accent-ink: var(--iox-accent)` is a cycle,
|
|
183
|
+
* and CSS makes both invalid. The contract writes `{{accent}}` instead — the token's ROOT value,
|
|
184
|
+
* resolved here at compile time (and again from dark values for the dark twin).
|
|
124
185
|
*/
|
|
125
186
|
export function compileSchemeCss(tokens: ReadonlyArray<DesignTokenLike> | null | undefined, scope?: string): string {
|
|
126
187
|
const rows = usableTokens(tokens);
|
|
127
188
|
if (!rows.length) return '';
|
|
189
|
+
const resolved = resolveTokenValues(rows, LIGHT);
|
|
128
190
|
return nonDefaultSchemes().map(([id, set]) => rule(schemeSelector(id, scope), [
|
|
129
|
-
...
|
|
191
|
+
...schemeEntries(set, resolved),
|
|
130
192
|
...rows.filter(t => !(t.name in set) && t.value.includes(REFERENCE))
|
|
131
193
|
.map((t): [string, string] => [t.name, t.value]),
|
|
132
194
|
])).filter(Boolean).join('\n');
|
|
133
195
|
}
|
|
134
196
|
|
|
135
|
-
/** The dark twin of
|
|
136
|
-
*
|
|
197
|
+
/** The dark twin of both scheme rules: a re-declared token with a dark value would otherwise pin
|
|
198
|
+
* the LIGHT value inside the section, and a `{{token}}` swap was resolved from LIGHT values. */
|
|
137
199
|
function darkSchemeRules(rows: ReadonlyArray<DesignTokenLike>, scope: string | undefined): string[] {
|
|
200
|
+
const light = resolveTokenValues(rows, LIGHT);
|
|
201
|
+
const dark = resolveTokenValues(rows, DARK);
|
|
138
202
|
const redeclared = rows.filter(t => {
|
|
139
|
-
const
|
|
140
|
-
return
|
|
203
|
+
const d = darkOf(t);
|
|
204
|
+
return d !== null && (t.value.includes(REFERENCE) || d.includes(REFERENCE));
|
|
205
|
+
});
|
|
206
|
+
return nonDefaultSchemes().map(([id, set]) => {
|
|
207
|
+
const swapped = Object.fromEntries(Object.entries(set).filter(([, v]) => hasPlaceholder(v)));
|
|
208
|
+
const lightSwap = new Map(schemeEntries(swapped, light));
|
|
209
|
+
return rule(schemeSelector(id, scope), [
|
|
210
|
+
...schemeEntries(swapped, dark).filter(([name, value]) => lightSwap.get(name) !== value),
|
|
211
|
+
...redeclared.filter(t => !(t.name in set)).map((t): [string, string] => [t.name, darkOf(t) as string]),
|
|
212
|
+
], ' ');
|
|
141
213
|
});
|
|
142
|
-
return nonDefaultSchemes().map(([id, set]) => rule(schemeSelector(id, scope), redeclared
|
|
143
|
-
.filter(t => !(t.name in set))
|
|
144
|
-
.map((t): [string, string] => [t.name, darkOf(t) as string]), ' '));
|
|
145
214
|
}
|
|
146
215
|
|
|
147
216
|
function presentOverrides(rows: ReadonlyArray<DesignTokenLike>, overrides: Readonly<Record<string, string>>,
|
package/styles/iox-forms.css
CHANGED
|
@@ -49,6 +49,57 @@
|
|
|
49
49
|
.iox-field__error { margin: 4px 0 0; }
|
|
50
50
|
.iox-submit { font: inherit; border: none; cursor: pointer; }
|
|
51
51
|
|
|
52
|
+
/* ── Visual baseline — EXACTLY the spec's `fieldClassBaseline` (design-system plan DS2) ──────────
|
|
53
|
+
* Every value is a token, so a re-theme restyles every form with no page write. Each var() carries
|
|
54
|
+
* today's value as its fallback (trap T6) — `inherit` where the structural rule above inherits, the
|
|
55
|
+
* engine reset's `0` for padding, and `revert` (the UA default, which is what rendered before) for
|
|
56
|
+
* everything nothing declared — so an org whose tokens are not seeded renders as it did.
|
|
57
|
+
* Two spec values are bare keywords and cannot fall back: `border-style: solid` (the UA inset
|
|
58
|
+
* border becomes solid) and the focus ring's `outline: none`, whose box-shadow therefore falls back
|
|
59
|
+
* to a visible system-colour ring rather than to nothing. */
|
|
60
|
+
.iox-field__control {
|
|
61
|
+
background: var(--iox-field-bg, revert);
|
|
62
|
+
color: var(--iox-field-ink, inherit);
|
|
63
|
+
border-width: var(--iox-field-border-width, revert);
|
|
64
|
+
border-style: solid;
|
|
65
|
+
border-color: var(--iox-field-border, revert);
|
|
66
|
+
border-radius: var(--iox-field-radius, revert);
|
|
67
|
+
padding: var(--iox-field-pad, 0);
|
|
68
|
+
font-family: var(--iox-font-body, inherit);
|
|
69
|
+
font-size: var(--iox-size-body, inherit);
|
|
70
|
+
transition: border-color var(--iox-dur-base, 0s) var(--iox-ease-standard, ease), box-shadow var(--iox-dur-base, 0s) var(--iox-ease-standard, ease);
|
|
71
|
+
}
|
|
72
|
+
.iox-field__control:focus-visible {
|
|
73
|
+
border-color: var(--iox-accent, revert);
|
|
74
|
+
box-shadow: var(--iox-field-focus, 0 0 0 2px Highlight);
|
|
75
|
+
outline: none;
|
|
76
|
+
}
|
|
77
|
+
.iox-field__label {
|
|
78
|
+
font-family: var(--iox-font-body, inherit);
|
|
79
|
+
font-size: var(--iox-field-label-size, inherit);
|
|
80
|
+
font-weight: var(--iox-field-label-weight, inherit);
|
|
81
|
+
color: var(--iox-text, inherit);
|
|
82
|
+
}
|
|
83
|
+
.iox-field__help {
|
|
84
|
+
font-size: var(--iox-size-small, inherit);
|
|
85
|
+
color: var(--iox-text-muted, inherit);
|
|
86
|
+
}
|
|
87
|
+
.iox-field__error {
|
|
88
|
+
font-size: var(--iox-size-small, inherit);
|
|
89
|
+
color: var(--iox-danger, inherit);
|
|
90
|
+
}
|
|
91
|
+
.iox-invalid .iox-field__control {
|
|
92
|
+
border-color: var(--iox-danger, revert);
|
|
93
|
+
}
|
|
94
|
+
/* Native-appearance controls (checkbox, radio, range — and the switch, a checkbox) draw themselves:
|
|
95
|
+
the box baseline must not frame them. `:where()` keeps this at the baseline's specificity, so the
|
|
96
|
+
switch rules below still paint the track. */
|
|
97
|
+
.iox-field__control:where([type='checkbox'], [type='radio'], [type='range']) {
|
|
98
|
+
border: none;
|
|
99
|
+
padding: 0;
|
|
100
|
+
background: transparent;
|
|
101
|
+
}
|
|
102
|
+
|
|
52
103
|
/* Visually hidden, still announced — a label may be hidden, never deleted. */
|
|
53
104
|
.iox-field__label--hidden {
|
|
54
105
|
position: absolute; width: 1px; height: 1px; margin: -1px;
|
|
@@ -65,7 +116,7 @@
|
|
|
65
116
|
background: var(--iox-line, #d8d4d1);
|
|
66
117
|
position: relative;
|
|
67
118
|
cursor: pointer;
|
|
68
|
-
transition: background 150ms ease-out;
|
|
119
|
+
transition: background var(--iox-dur-fast, 150ms) var(--iox-ease-standard, ease-out);
|
|
69
120
|
flex-shrink: 0;
|
|
70
121
|
}
|
|
71
122
|
.iox-field__control--switch::before {
|
|
@@ -76,11 +127,11 @@
|
|
|
76
127
|
width: 16px;
|
|
77
128
|
height: 16px;
|
|
78
129
|
border-radius: 999px;
|
|
79
|
-
background: #ffffff;
|
|
80
|
-
transition: transform 150ms ease-out;
|
|
130
|
+
background: var(--iox-accent-ink, #ffffff);
|
|
131
|
+
transition: transform var(--iox-dur-fast, 150ms) var(--iox-ease-standard, ease-out);
|
|
81
132
|
}
|
|
82
133
|
.iox-field__control--switch:checked {
|
|
83
|
-
background: var(--iox-brand-500, #1f1d1b);
|
|
134
|
+
background: var(--iox-accent, var(--iox-brand-500, #1f1d1b));
|
|
84
135
|
}
|
|
85
136
|
.iox-field__control--switch:checked::before {
|
|
86
137
|
transform: translateX(16px);
|
|
@@ -92,5 +143,5 @@
|
|
|
92
143
|
/* ── range (control='range') ── */
|
|
93
144
|
.iox-field__control--range {
|
|
94
145
|
width: 100%;
|
|
95
|
-
accent-color: var(--iox-brand-500, #1f1d1b);
|
|
146
|
+
accent-color: var(--iox-accent, var(--iox-brand-500, #1f1d1b));
|
|
96
147
|
}
|