@vectoriox/iox-ui 5.25.0 → 5.26.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 +1153 -191
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +1680 -241
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/package.json +1 -1
- package/src/design-core/brand-neutrals.ts +236 -0
- package/src/design-core/brand.ts +24 -14
- package/src/design-core/colour-eval.ts +117 -0
- package/src/design-core/contrast-audit.ts +214 -0
- package/src/design-core/design-apply.ts +34 -14
- package/src/design-core/design-settings.ts +12 -3
- package/src/design-core/design-token.model.ts +26 -0
- package/src/design-core/flavor-fixtures.generated.ts +150 -0
- package/src/design-core/flavor-spec.generated.ts +1061 -189
- package/src/design-core/flavor-tokens.ts +36 -8
- package/src/design-core/public-api.ts +3 -0
- package/src/design-core/token-css.ts +48 -15
- package/styles/iox-design.css +16 -0
- package/styles/iox-forms.css +15 -9
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { deriveAccentTokens } from './brand';
|
|
2
|
+
import { flavorColours, FlavorColours, preTintAccentGrounds } from './brand-neutrals';
|
|
2
3
|
import { fontStackWithout, formatFontFamily } from './fonts';
|
|
3
4
|
import { SPEC_AXES } from './flavor-spec.generated';
|
|
4
5
|
import {
|
|
@@ -11,7 +12,8 @@ import {
|
|
|
11
12
|
*
|
|
12
13
|
* Layering, each step over the last (the boards' `tokenMap`, with the product's colour math):
|
|
13
14
|
* pack tokens → chosen font pair → shape · density · motion axes → pill-button rule →
|
|
14
|
-
* the author's own font roles →
|
|
15
|
+
* the author's own font roles → every COLOUR token from the brand seed and the background tint
|
|
16
|
+
* (`flavorColours`, DS9: tinted neutrals, the accent quartet, the held contrast pairs).
|
|
15
17
|
*
|
|
16
18
|
* `mode` does not change rows — every row keeps its light value and its dark override; mode is
|
|
17
19
|
* how the stylesheet is compiled.
|
|
@@ -91,13 +93,40 @@ export function buildFlavorTokens(pack: FlavorPackV2, choices: DesignChoices): D
|
|
|
91
93
|
if (typeof family === 'string' && family.trim() !== '') set(role, userFontStack(family, byName.get(role)?.value), USER_TOKEN_SOURCE);
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
setColours(byName, flavorColours(pack, { seed: choices.seed, tint: choices.tint ?? pack.defaults.tint }));
|
|
97
|
+
|
|
98
|
+
return rows;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Colour rows take their light value and the dark one only where it differs (the contract's own rule). */
|
|
102
|
+
function setColours(byName: Map<string, DesignTokenRow>, colours: FlavorColours): void {
|
|
103
|
+
for (const [name, [light, dark]] of Object.entries(colours)) {
|
|
104
|
+
const row = byName.get(name);
|
|
105
|
+
if (!row) continue;
|
|
106
|
+
row.value = light;
|
|
107
|
+
if (dark !== undefined && dark !== light) row.values = { dark };
|
|
108
|
+
else delete row.values;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* What the contract wrote for `choices` BEFORE DS9 — the baseline a studio that applied its look before
|
|
114
|
+
* the tint existed is judged against (`designBaseline`, `designTokenRepairs`): the look's own palette, the
|
|
115
|
+
* rows DS9 changed at their old values (`pack.legacy`), and a seed ≠ the pack's own re-deriving the accent
|
|
116
|
+
* set against `bg` alone, as `deriveAccentTokens` was called then. No held pairs, no tint.
|
|
117
|
+
*/
|
|
118
|
+
export function legacyFlavorTokens(pack: FlavorPackV2, choices: DesignChoices): DesignTokenRow[] {
|
|
119
|
+
const rows = buildFlavorTokens(pack, { ...choices, seed: pack.seed, tint: 'neutral' });
|
|
120
|
+
const byName = new Map(rows.map(r => [r.name, r]));
|
|
121
|
+
for (const [name, old] of Object.entries(pack.legacy ?? {})) {
|
|
122
|
+
const row = byName.get(name);
|
|
123
|
+
if (!row) continue;
|
|
124
|
+
row.value = old.value;
|
|
125
|
+
if (old.dark !== undefined) row.values = { dark: old.dark };
|
|
126
|
+
else delete row.values;
|
|
127
|
+
}
|
|
94
128
|
if (choices.seed && choices.seed.toLowerCase() !== pack.seed.toLowerCase()) {
|
|
95
|
-
const
|
|
96
|
-
const inverseBg = byName.get('inverse-bg');
|
|
97
|
-
for (const accent of deriveAccentTokens(choices.seed, {
|
|
98
|
-
bg: bg?.value ?? '', bgDark: bg?.values?.dark,
|
|
99
|
-
inverseBg: inverseBg?.value, inverseBgDark: inverseBg?.values?.dark,
|
|
100
|
-
})) {
|
|
129
|
+
for (const accent of deriveAccentTokens(choices.seed, preTintAccentGrounds(pack.palette))) {
|
|
101
130
|
const row = byName.get(accent.name);
|
|
102
131
|
if (!row) continue;
|
|
103
132
|
row.value = accent.value;
|
|
@@ -105,6 +134,5 @@ export function buildFlavorTokens(pack: FlavorPackV2, choices: DesignChoices): D
|
|
|
105
134
|
else delete row.values;
|
|
106
135
|
}
|
|
107
136
|
}
|
|
108
|
-
|
|
109
137
|
return rows;
|
|
110
138
|
}
|
|
@@ -18,6 +18,9 @@ export * from './adopt';
|
|
|
18
18
|
export * from './node-classes';
|
|
19
19
|
export * from './empty-image';
|
|
20
20
|
export * from './brand';
|
|
21
|
+
export * from './colour-eval';
|
|
22
|
+
export * from './brand-neutrals';
|
|
23
|
+
export * from './contrast-audit';
|
|
21
24
|
export * from './flavor-tokens';
|
|
22
25
|
export * from './flavor-seed';
|
|
23
26
|
export * from './spring';
|
|
@@ -121,7 +121,7 @@ export function tokensToDarkVarMap(tokens: ReadonlyArray<DesignTokenLike> | null
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
const TOKEN_REF = /var\(\s*--iox-([\w-]+)\s*(?:,([^()]*))?\)/g;
|
|
124
|
-
const PLACEHOLDER = /\{\{\s*([\w-]+)\s*\}\}/g;
|
|
124
|
+
const PLACEHOLDER = /\{\{\s*(~?)([\w-]+)\s*\}\}/g;
|
|
125
125
|
|
|
126
126
|
/** A compiled colour mode — the contract's modes minus `auto`, which is a compile CHOICE, not values. */
|
|
127
127
|
export type ResolvedMode = Exclude<DesignMode, 'auto'>;
|
|
@@ -163,6 +163,26 @@ export function resolveTokenValues(
|
|
|
163
163
|
return out;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Every token's value INSIDE a section wearing `schemeId`, in one mode — what CSS computes there: the
|
|
168
|
+
* scheme's overrides (`{{token}}` / `{{~token}}` placeholders substituted from the root), every other
|
|
169
|
+
* row as declared, and `var()` references resolved within the section (the §3c re-declaration rule
|
|
170
|
+
* makes that the section's own resolution). `default` (or an unknown id) is the root.
|
|
171
|
+
*/
|
|
172
|
+
export function resolveSchemeTokenValues(
|
|
173
|
+
tokens: ReadonlyArray<DesignTokenLike> | null | undefined,
|
|
174
|
+
mode: ResolvedMode,
|
|
175
|
+
schemeId: string,
|
|
176
|
+
): Record<string, string> {
|
|
177
|
+
const rows = usableTokens(tokens);
|
|
178
|
+
const set = schemeId === DEFAULT_SCHEME ? undefined : (SPEC_SCHEMES as Readonly<Record<string, { set: Readonly<Record<string, string>> }>>)[schemeId]?.set;
|
|
179
|
+
if (!set) return resolveTokenValues(rows, mode);
|
|
180
|
+
const root = resolveTokenValues(rows, mode);
|
|
181
|
+
const other = resolveTokenValues(rows, mode === DARK ? LIGHT : DARK);
|
|
182
|
+
const overrides = new Map(schemeEntries(set, root, other));
|
|
183
|
+
return resolveTokenValues(rows.map(t => ({ name: t.name, value: overrides.get(t.name) ?? modeValue(t, mode) })), LIGHT);
|
|
184
|
+
}
|
|
185
|
+
|
|
166
186
|
/**
|
|
167
187
|
* `value` with every `var(--iox-…)` replaced by that token's resolved root value in `mode` — what
|
|
168
188
|
* the Style panel's Detach writes, so the node keeps exactly what it showed as a literal. An unknown
|
|
@@ -178,20 +198,25 @@ export function resolveTokenReferences(
|
|
|
178
198
|
resolved[name] ?? (fallback !== undefined ? fallback.trim() : ref));
|
|
179
199
|
}
|
|
180
200
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
201
|
+
type ResolvedValues = Readonly<Record<string, string>>;
|
|
202
|
+
|
|
203
|
+
/** A scheme value with its placeholders replaced — `{{token}}` by the root's resolved value in this
|
|
204
|
+
* mode, `{{~token}}` by its value in the OTHER mode (an inverse section is the other mode's ground,
|
|
205
|
+
* DS9) — or null when a placeholder names a token the set does not have (the declaration is then
|
|
206
|
+
* omitted). */
|
|
207
|
+
function substitute(template: string, resolved: ResolvedValues, opposite: ResolvedValues): string | null {
|
|
184
208
|
let missing = false;
|
|
185
|
-
const value = template.replace(PLACEHOLDER, (_, name: string) => {
|
|
186
|
-
|
|
187
|
-
|
|
209
|
+
const value = template.replace(PLACEHOLDER, (_, tilde: string, name: string) => {
|
|
210
|
+
const from = tilde ? opposite : resolved;
|
|
211
|
+
if (!(name in from)) missing = true;
|
|
212
|
+
return from[name] ?? '';
|
|
188
213
|
});
|
|
189
214
|
return missing ? null : value;
|
|
190
215
|
}
|
|
191
216
|
|
|
192
|
-
const schemeEntries = (set: Readonly<Record<string, string>>, resolved:
|
|
217
|
+
const schemeEntries = (set: Readonly<Record<string, string>>, resolved: ResolvedValues, opposite: ResolvedValues) =>
|
|
193
218
|
Object.entries(set)
|
|
194
|
-
.map(([name, template]): [string, string | null] => [name, substitute(template, resolved)])
|
|
219
|
+
.map(([name, template]): [string, string | null] => [name, substitute(template, resolved, opposite)])
|
|
195
220
|
.filter((e): e is [string, string] => e[1] !== null);
|
|
196
221
|
|
|
197
222
|
const hasPlaceholder = (value: string) => value.includes('{{');
|
|
@@ -209,12 +234,17 @@ const hasPlaceholder = (value: string) => value.includes('{{');
|
|
|
209
234
|
* and CSS makes both invalid. The contract writes `{{accent}}` instead — the token's ROOT value,
|
|
210
235
|
* resolved here at compile time (and again from dark values for the dark twin).
|
|
211
236
|
*/
|
|
212
|
-
export function compileSchemeCss(
|
|
237
|
+
export function compileSchemeCss(
|
|
238
|
+
tokens: ReadonlyArray<DesignTokenLike> | null | undefined,
|
|
239
|
+
scope?: string,
|
|
240
|
+
opposite?: ResolvedValues,
|
|
241
|
+
): string {
|
|
213
242
|
const rows = usableTokens(tokens);
|
|
214
243
|
if (!rows.length) return '';
|
|
215
244
|
const resolved = resolveTokenValues(rows, LIGHT);
|
|
245
|
+
const other = opposite ?? resolveTokenValues(rows, DARK);
|
|
216
246
|
return nonDefaultSchemes().map(([id, set]) => rule(schemeSelector(id, scope), [
|
|
217
|
-
...schemeEntries(set, resolved),
|
|
247
|
+
...schemeEntries(set, resolved, other),
|
|
218
248
|
...rows.filter(t => !(t.name in set) && t.value.includes(REFERENCE))
|
|
219
249
|
.map((t): [string, string] => [t.name, t.value]),
|
|
220
250
|
])).filter(Boolean).join('\n');
|
|
@@ -231,9 +261,9 @@ function darkSchemeRules(rows: ReadonlyArray<DesignTokenLike>, scope: string | u
|
|
|
231
261
|
});
|
|
232
262
|
return nonDefaultSchemes().map(([id, set]) => {
|
|
233
263
|
const swapped = Object.fromEntries(Object.entries(set).filter(([, v]) => hasPlaceholder(v)));
|
|
234
|
-
const lightSwap = new Map(schemeEntries(swapped, light));
|
|
264
|
+
const lightSwap = new Map(schemeEntries(swapped, light, dark));
|
|
235
265
|
return rule(schemeSelector(id, scope), [
|
|
236
|
-
...schemeEntries(swapped, dark).filter(([name, value]) => lightSwap.get(name) !== value),
|
|
266
|
+
...schemeEntries(swapped, dark, light).filter(([name, value]) => lightSwap.get(name) !== value),
|
|
237
267
|
...redeclared.filter(t => !(t.name in set)).map((t): [string, string] => [t.name, darkOf(t) as string]),
|
|
238
268
|
], ' ');
|
|
239
269
|
});
|
|
@@ -249,15 +279,18 @@ export function compileTokenCssBlocks(
|
|
|
249
279
|
tokens: ReadonlyArray<DesignTokenLike> | null | undefined,
|
|
250
280
|
opts: TokenCssOptions = {},
|
|
251
281
|
): TokenCssBlocks {
|
|
252
|
-
const
|
|
282
|
+
const all = usableTokens(tokens);
|
|
283
|
+
const rows = rowsForMode(all, opts.mode);
|
|
253
284
|
const root = opts.scope ?? ROOT;
|
|
285
|
+
// A forced mode flattens the rows to ONE mode; a `{{~token}}` still reads the other mode's value.
|
|
286
|
+
const opposite = resolveTokenValues(all, opts.mode === DARK ? LIGHT : DARK);
|
|
254
287
|
const present = new Set(rows.map(t => t.name));
|
|
255
288
|
const unset: Array<[string, string]> = opts.isolate
|
|
256
289
|
? [...specTokenNames()].filter(name => !present.has(name)).map((name): [string, string] => [name, 'initial'])
|
|
257
290
|
: [];
|
|
258
291
|
const base = [
|
|
259
292
|
rule(root, [...unset, ...rows.map((t): [string, string] => [t.name, t.value])]),
|
|
260
|
-
compileSchemeCss(rows, opts.scope),
|
|
293
|
+
compileSchemeCss(rows, opts.scope, opposite),
|
|
261
294
|
...ACCESSIBILITY_MEDIA.map(({ query, overrides }) =>
|
|
262
295
|
media(query, [rule(root, presentOverrides(rows, overrides, false), ' ')])),
|
|
263
296
|
].filter(Boolean).join('\n');
|
package/styles/iox-design.css
CHANGED
|
@@ -28,3 +28,19 @@ iox-gallery.iox-es-media[data-iox-empty] > .iox-gallery {
|
|
|
28
28
|
.iox-es-media[data-iox-empty] .iox-gallery__overlay {
|
|
29
29
|
display: none;
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
/* What a filled button HOLDS reads its ink (DS9, the contract's `inkScopes`, pinned by
|
|
33
|
+
ink-scopes.spec.ts). An Icon dropped into a Primary button wore the `icon` style — `icon-color`
|
|
34
|
+
= accent-text, painted on the accent fill: 1:1 in a Brand section. Inside a button every token a
|
|
35
|
+
child's colour reads becomes `currentColor` — the button's own ink — so the label and the icon read
|
|
36
|
+
alike; an author's literal on the child still wins. */
|
|
37
|
+
.iox-es-button-primary,
|
|
38
|
+
.iox-es-button-secondary,
|
|
39
|
+
.iox-es-button-ghost {
|
|
40
|
+
--iox-icon-color: currentColor;
|
|
41
|
+
--iox-heading-color: currentColor;
|
|
42
|
+
--iox-eyebrow-color: currentColor;
|
|
43
|
+
--iox-link-color: currentColor;
|
|
44
|
+
--iox-link-hover-color: currentColor;
|
|
45
|
+
--iox-text-muted: currentColor;
|
|
46
|
+
}
|
package/styles/iox-forms.css
CHANGED
|
@@ -101,8 +101,8 @@
|
|
|
101
101
|
* A Field wearing the `field` element style renders `iox-es iox-es-field`; everything below is
|
|
102
102
|
* scoped to it, so a Field saved before DS2 renders exactly as above whatever tokens the org holds,
|
|
103
103
|
* and seeding an org's tokens never restyles a live legacy form.
|
|
104
|
-
*
|
|
105
|
-
* iox-ui's
|
|
104
|
+
* Every rule below IS the spec's `fieldClassBaseline`, selector and values verbatim (pinned by
|
|
105
|
+
* iox-ui's token-names spec). Values are bare spec tokens: a marked Field only exists where the
|
|
106
106
|
* builder seeded them. */
|
|
107
107
|
.iox-es-field .iox-field__control {
|
|
108
108
|
background: var(--iox-field-bg);
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
transition: border-color var(--iox-dur-base) var(--iox-ease-standard), box-shadow var(--iox-dur-base) var(--iox-ease-standard);
|
|
118
118
|
}
|
|
119
119
|
.iox-es-field .iox-field__control:focus-visible {
|
|
120
|
-
border-color: var(--iox-accent);
|
|
120
|
+
border-color: var(--iox-accent-text);
|
|
121
121
|
box-shadow: var(--iox-field-focus);
|
|
122
122
|
outline: none;
|
|
123
123
|
}
|
|
@@ -138,6 +138,14 @@
|
|
|
138
138
|
.iox-es-field.iox-invalid .iox-field__control {
|
|
139
139
|
border-color: var(--iox-danger);
|
|
140
140
|
}
|
|
141
|
+
/* DS9 (contrast audit): the placeholder is themed (the UA's #757575 read 1:1 in a Brand section); the
|
|
142
|
+
native controls, the focus edge and the switch read `accent-text` — the accent GATED as text on every
|
|
143
|
+
ground — never the `accent` fill (Pop's orange on its yellow panel: 2.13:1); the off track is
|
|
144
|
+
`text-muted` under a `bg` thumb (the decorative `line` it wore read 1.06:1). WCAG 1.4.11: 3:1. */
|
|
145
|
+
.iox-es-field .iox-field__control::placeholder {
|
|
146
|
+
color: var(--iox-text-muted);
|
|
147
|
+
opacity: 1;
|
|
148
|
+
}
|
|
141
149
|
/* Native-appearance controls (checkbox, radio, range — and the switch, a checkbox) draw themselves:
|
|
142
150
|
the box baseline must not frame them. `:where()` keeps this at the baseline's specificity, so the
|
|
143
151
|
switch rules below still paint the track. */
|
|
@@ -145,18 +153,16 @@
|
|
|
145
153
|
border: none;
|
|
146
154
|
padding: 0;
|
|
147
155
|
background: transparent;
|
|
156
|
+
accent-color: var(--iox-accent-text);
|
|
148
157
|
}
|
|
149
158
|
.iox-es-field .iox-field__control--switch {
|
|
150
|
-
background: var(--iox-
|
|
159
|
+
background: var(--iox-text-muted);
|
|
151
160
|
transition: background var(--iox-dur-fast) var(--iox-ease-standard);
|
|
152
161
|
}
|
|
153
162
|
.iox-es-field .iox-field__control--switch::before {
|
|
154
|
-
background: var(--iox-
|
|
163
|
+
background: var(--iox-bg);
|
|
155
164
|
transition: transform var(--iox-dur-fast) var(--iox-ease-standard);
|
|
156
165
|
}
|
|
157
166
|
.iox-es-field .iox-field__control--switch:checked {
|
|
158
|
-
background: var(--iox-accent);
|
|
159
|
-
}
|
|
160
|
-
.iox-es-field .iox-field__control--range {
|
|
161
|
-
accent-color: var(--iox-accent);
|
|
167
|
+
background: var(--iox-accent-text);
|
|
162
168
|
}
|