@vectoriox/iox-ui 5.10.0 → 5.11.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 +8037 -8920
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +9584 -10575
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +2 -11315
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/index.d.ts +3 -9343
- 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/flavor-spec.generated.ts +9454 -10524
- package/src/design-core/flavor-tokens.ts +7 -3
- package/src/design-core/fonts.ts +14 -4
- package/src/design-core/spring.ts +21 -9
- package/src/design-core/token-css.ts +81 -31
|
@@ -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;
|
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 => {
|
|
@@ -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,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SPEC_A11Y, SPEC_SCHEMES } from './flavor-spec.generated';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Design-token → CSS compilation, shared by BOTH renderers (flavors F0, design-system DS1).
|
|
@@ -40,24 +40,11 @@ export interface TokenCssBlocks {
|
|
|
40
40
|
dark: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
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
|
-
};
|
|
43
|
+
/** What `prefers-reduced-motion: reduce` collapses — the contract's complete list (`a11y.reducedMotion`). */
|
|
44
|
+
export const REDUCED_MOTION_OVERRIDES: Readonly<Record<string, string>> = SPEC_A11Y.reducedMotion;
|
|
54
45
|
|
|
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
|
-
};
|
|
46
|
+
/** What `prefers-reduced-transparency: reduce` makes solid — the contract's `a11y.reducedTransparency`. */
|
|
47
|
+
export const REDUCED_TRANSPARENCY_OVERRIDES: Readonly<Record<string, string>> = SPEC_A11Y.reducedTransparency;
|
|
61
48
|
|
|
62
49
|
const ROOT = ':root';
|
|
63
50
|
const DEFAULT_SCHEME = 'default';
|
|
@@ -95,7 +82,7 @@ const schemeSelector = (id: string, scope?: string) =>
|
|
|
95
82
|
scope ? `${scope} .iox-scheme-${id}` : `.iox-scheme-${id}`;
|
|
96
83
|
|
|
97
84
|
const nonDefaultSchemes = (): ReadonlyArray<[string, Readonly<Record<string, string>>]> =>
|
|
98
|
-
Object.entries(
|
|
85
|
+
Object.entries(SPEC_SCHEMES)
|
|
99
86
|
.filter(([id]) => id !== DEFAULT_SCHEME)
|
|
100
87
|
.map(([id, scheme]) => [id, scheme.set as Readonly<Record<string, string>>]);
|
|
101
88
|
|
|
@@ -116,32 +103,95 @@ export function tokensToDarkVarMap(tokens: ReadonlyArray<DesignTokenLike> | null
|
|
|
116
103
|
return map;
|
|
117
104
|
}
|
|
118
105
|
|
|
106
|
+
const TOKEN_REF = /var\(\s*--iox-([\w-]+)\s*(?:,([^()]*))?\)/g;
|
|
107
|
+
const PLACEHOLDER = /\{\{\s*([\w-]+)\s*\}\}/g;
|
|
108
|
+
|
|
109
|
+
type Mode = 'light' | 'dark';
|
|
110
|
+
|
|
111
|
+
const modeValue = (t: DesignTokenLike, mode: Mode): string => (mode === 'dark' ? darkOf(t) ?? t.value : t.value);
|
|
112
|
+
|
|
113
|
+
/** Every token's ROOT value in one mode, with `var(--iox-…)` references resolved against the same
|
|
114
|
+
* set (recursively; a reference cycle or an unknown token is left as written). */
|
|
115
|
+
export function resolveTokenValues(
|
|
116
|
+
tokens: ReadonlyArray<DesignTokenLike> | null | undefined,
|
|
117
|
+
mode: Mode = 'light',
|
|
118
|
+
): Record<string, string> {
|
|
119
|
+
const raw = new Map(usableTokens(tokens).map(t => [t.name, modeValue(t, mode)]));
|
|
120
|
+
const out: Record<string, string> = {};
|
|
121
|
+
const resolve = (name: string, trail: ReadonlySet<string>): string | null => {
|
|
122
|
+
if (name in out) return out[name];
|
|
123
|
+
const value = raw.get(name);
|
|
124
|
+
if (value === undefined || trail.has(name)) return null;
|
|
125
|
+
const next = new Set(trail).add(name);
|
|
126
|
+
return value.replace(TOKEN_REF, (ref, inner: string, fallback?: string) =>
|
|
127
|
+
resolve(inner, next) ?? (fallback !== undefined ? fallback.trim() : ref));
|
|
128
|
+
};
|
|
129
|
+
for (const name of raw.keys()) {
|
|
130
|
+
const value = resolve(name, new Set());
|
|
131
|
+
if (value !== null) out[name] = value;
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A scheme value with its `{{token}}` placeholders replaced by resolved root values — or null
|
|
137
|
+
* when a placeholder names a token the set does not have (the declaration is then omitted). */
|
|
138
|
+
function substitute(template: string, resolved: Readonly<Record<string, string>>): string | null {
|
|
139
|
+
let missing = false;
|
|
140
|
+
const value = template.replace(PLACEHOLDER, (_, name: string) => {
|
|
141
|
+
if (!(name in resolved)) missing = true;
|
|
142
|
+
return resolved[name] ?? '';
|
|
143
|
+
});
|
|
144
|
+
return missing ? null : value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const schemeEntries = (set: Readonly<Record<string, string>>, resolved: Readonly<Record<string, string>>) =>
|
|
148
|
+
Object.entries(set)
|
|
149
|
+
.map(([name, template]): [string, string | null] => [name, substitute(template, resolved)])
|
|
150
|
+
.filter((e): e is [string, string] => e[1] !== null);
|
|
151
|
+
|
|
152
|
+
const hasPlaceholder = (value: string) => value.includes('{{');
|
|
153
|
+
|
|
119
154
|
/**
|
|
120
|
-
* Section colour schemes (design-system plan §3c).
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
155
|
+
* Section colour schemes (design-system plan §3c). Two rules, both about WHERE a custom property
|
|
156
|
+
* resolves:
|
|
157
|
+
*
|
|
158
|
+
* - ⚠️ Custom properties resolve where they are DECLARED: `--iox-card-bg: var(--iox-surface)`
|
|
159
|
+
* declared on `:root` keeps `:root`'s surface inside `.iox-scheme-inverse`. So each scheme block
|
|
160
|
+
* carries the spec's overrides PLUS every other token whose value references a token,
|
|
161
|
+
* re-declared verbatim — re-resolving it in the section.
|
|
162
|
+
* - ⚠️ A scheme that SWAPS two tokens (Brand: accent ⇄ accent-ink) cannot write them as `var()`:
|
|
163
|
+
* on one element `accent: var(--iox-accent-ink)` + `accent-ink: var(--iox-accent)` is a cycle,
|
|
164
|
+
* and CSS makes both invalid. The contract writes `{{accent}}` instead — the token's ROOT value,
|
|
165
|
+
* resolved here at compile time (and again from dark values for the dark twin).
|
|
124
166
|
*/
|
|
125
167
|
export function compileSchemeCss(tokens: ReadonlyArray<DesignTokenLike> | null | undefined, scope?: string): string {
|
|
126
168
|
const rows = usableTokens(tokens);
|
|
127
169
|
if (!rows.length) return '';
|
|
170
|
+
const resolved = resolveTokenValues(rows, 'light');
|
|
128
171
|
return nonDefaultSchemes().map(([id, set]) => rule(schemeSelector(id, scope), [
|
|
129
|
-
...
|
|
172
|
+
...schemeEntries(set, resolved),
|
|
130
173
|
...rows.filter(t => !(t.name in set) && t.value.includes(REFERENCE))
|
|
131
174
|
.map((t): [string, string] => [t.name, t.value]),
|
|
132
175
|
])).filter(Boolean).join('\n');
|
|
133
176
|
}
|
|
134
177
|
|
|
135
|
-
/** The dark twin of
|
|
136
|
-
*
|
|
178
|
+
/** The dark twin of both scheme rules: a re-declared token with a dark value would otherwise pin
|
|
179
|
+
* the LIGHT value inside the section, and a `{{token}}` swap was resolved from LIGHT values. */
|
|
137
180
|
function darkSchemeRules(rows: ReadonlyArray<DesignTokenLike>, scope: string | undefined): string[] {
|
|
181
|
+
const light = resolveTokenValues(rows, 'light');
|
|
182
|
+
const dark = resolveTokenValues(rows, 'dark');
|
|
138
183
|
const redeclared = rows.filter(t => {
|
|
139
|
-
const
|
|
140
|
-
return
|
|
184
|
+
const d = darkOf(t);
|
|
185
|
+
return d !== null && (t.value.includes(REFERENCE) || d.includes(REFERENCE));
|
|
186
|
+
});
|
|
187
|
+
return nonDefaultSchemes().map(([id, set]) => {
|
|
188
|
+
const swapped = Object.fromEntries(Object.entries(set).filter(([, v]) => hasPlaceholder(v)));
|
|
189
|
+
const lightSwap = new Map(schemeEntries(swapped, light));
|
|
190
|
+
return rule(schemeSelector(id, scope), [
|
|
191
|
+
...schemeEntries(swapped, dark).filter(([name, value]) => lightSwap.get(name) !== value),
|
|
192
|
+
...redeclared.filter(t => !(t.name in set)).map((t): [string, string] => [t.name, darkOf(t) as string]),
|
|
193
|
+
], ' ');
|
|
141
194
|
});
|
|
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
195
|
}
|
|
146
196
|
|
|
147
197
|
function presentOverrides(rows: ReadonlyArray<DesignTokenLike>, overrides: Readonly<Record<string, string>>,
|