@vectoriox/iox-ui 5.33.0 → 5.34.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.33.0",
3
+ "version": "5.34.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -66,8 +66,9 @@ const PILL_BASE: Readonly<Record<string, string>> = {
66
66
  };
67
67
  const PILL_ICON: Readonly<Record<string, string>> = { fontSize: '1em' };
68
68
  const PILL_WORDS: Readonly<Record<string, string>> = { width: 'auto', minHeight: 'auto', padding: '0px 0px 0px 0px', margin: '0px 0px 0px 0px' };
69
- /** The pill's words inherit the pill's type and ink (the contract's inherit-everything text style). */
70
- const PILL_WORDS_STYLE = 'button-label';
69
+ /** The pill's words wear the pill's OWN label style (Card v2.1): its type from the badge tokens, its leading,
70
+ * tracking, case and ink inherited from the pill — never the button's label style. */
71
+ export const PILL_LABEL_STYLE = 'badge-label';
71
72
 
72
73
  export interface PillOptions {
73
74
  text?: string;
@@ -81,7 +82,7 @@ function buildPill(create: CompositeNodeFactory, elementStyle: string, text: str
81
82
  writeStyles(pill, PILL_BASE);
82
83
  const glyph = create({ key: `${key}-icon`, type: 'Icon', props: { iconClass: icon }, elementStyle: 'icon' });
83
84
  writeStyles(glyph, PILL_ICON);
84
- const words = create({ key: `${key}-text`, type: 'Text', props: { content: text, tag: 'span' }, elementStyle: PILL_WORDS_STYLE });
85
+ const words = create({ key: `${key}-text`, type: 'Text', props: { content: text, tag: 'span' }, elementStyle: PILL_LABEL_STYLE });
85
86
  writeStyles(words, PILL_WORDS);
86
87
  pill.children = [glyph, words];
87
88
  return pill;
@@ -1,4 +1,4 @@
1
- import { elementStyleById, LinkType, switchElementStyle } from '@vectoriox/iox-ui/design-core';
1
+ import { COLOR_SCHEME_TRAIT, elementStyleById, LinkType, setColorScheme, switchElementStyle } from '@vectoriox/iox-ui/design-core';
2
2
 
3
3
  import { BadgeTone, buildBadge } from './badge';
4
4
  import {
@@ -119,7 +119,6 @@ enum Scheme {
119
119
  Inverse = 'inverse',
120
120
  }
121
121
 
122
- const COLOR_SCHEME = 'colorScheme';
123
122
  const LINK_TYPE = 'linkType';
124
123
  const SAVE_ICON = 'ph ph-bookmark-simple';
125
124
 
@@ -336,8 +335,9 @@ function placeBadge(card: CompositeNode, badge: CompositeNode, onMedia: boolean)
336
335
  writeStyles(badge, { position: overMedia ? 'absolute' : 'static' });
337
336
  }
338
337
 
338
+ /** Through design-core's ONE scheme writer — the same setting Section colours edits everywhere. */
339
339
  function writeScheme(node: CompositeNode, scheme: Scheme): void {
340
- writeProp(node, COLOR_SCHEME, scheme);
340
+ setColorScheme(node, scheme);
341
341
  }
342
342
 
343
343
  /** Write one role's layout values from a spec (base + layers). */
@@ -424,7 +424,7 @@ function matches(card: CompositeNode, spec: CardLayoutSpec): boolean {
424
424
  const content = findByRole(card, CardRole.Content);
425
425
  if (content) {
426
426
  if (content.elementStyle !== spec.contentStyle || !holds(content, spec.content)) return false;
427
- if (!same(readProp(content, COLOR_SCHEME) ?? Scheme.Default, spec.contentScheme)) return false;
427
+ if (!same(readProp(content, COLOR_SCHEME_TRAIT) ?? Scheme.Default, spec.contentScheme)) return false;
428
428
  }
429
429
  const title = findByRole(card, CardRole.Title);
430
430
  if (title && !holds(title, spec.title)) return false;
@@ -3,6 +3,7 @@ import { WCAG_AA_UI } from './brand-neutrals';
3
3
  import { compositeOver, paintColours, parseCssColour, quantizeRgb, Rgba, ringColour } from './colour-eval';
4
4
  import { ElementStyle, ELEMENT_STYLES } from './element-styles';
5
5
  import { SPEC_CONTRAST_AUDIT, SPEC_INK_SCOPES, SPEC_SCHEMES } from './flavor-spec.generated';
6
+ import { SCHEME_INK } from './node-classes';
6
7
  import { DesignTokenLike, ResolvedMode, resolveSchemeTokenValues } from './token-css';
7
8
 
8
9
  /**
@@ -19,7 +20,10 @@ import { DesignTokenLike, ResolvedMode, resolveSchemeTokenValues } from './token
19
20
  * its focus ring; the form parts the field baseline paints (`contrastAudit.formParts`); the form's
20
21
  * Success / Error notices, in every scheme and container;
21
22
  * - a Badge's or Chip's icon through the same `inkScopes`, and text over an image (Card v2's "Image behind
22
- * text"): the block's ink over its scrim composited on the WORST-CASE image (`contrastAudit.scrims`).
23
+ * text"): the block's ink over its scrim composited on the WORST-CASE image (`contrastAudit.scrims`);
24
+ * - a container that WEARS a scheme inside a section of any scheme (a Featured card on a plain page): the
25
+ * text it holds inherits the scheme's ink (`SCHEME_INK`, iox-ui#15) on the inner scheme's fill, composited
26
+ * over the OUTER section — the pairing the token pairs cannot see.
23
27
  *
24
28
  * WCAG 2: 4.5:1 for text, 3:1 for glyphs, UI boundaries and focus indicators (1.4.11), judged on 8-bit
25
29
  * colours. The same audit gates the contract (`colour.mjs auditContrast`, every flavor × tint × a seed
@@ -65,6 +69,8 @@ const GLYPH_GROUP = 'Media';
65
69
  const FORMS_GROUP = 'Forms';
66
70
  const DEFAULT_SCHEME = 'default';
67
71
  const SECTION = 'section';
72
+ /** The element a scheme'd container's running text is reported as (iox-ui#15). */
73
+ export const INHERITED_TEXT = 'inherited text';
68
74
  const FIELD = 'field';
69
75
  const WHITE: Rgba = { r: 1, g: 1, b: 1, a: 1 };
70
76
  const SINGLE_REF = /^var\(--iox-([\w-]+)\)$/;
@@ -187,10 +193,53 @@ export function auditDesignContrast(rows: ReadonlyArray<DesignTokenLike>, mode:
187
193
  }
188
194
  }
189
195
  }
196
+ findings.push(...schemedContainerFindings(rows, mode, section, containers.filter((c): c is ElementStyle => !!c)));
190
197
  for (const scrim of SCRIMS) findings.push(...scrimFindings(rows, mode, scrim, byId.get(scrim.style)));
191
198
  return findings;
192
199
  }
193
200
 
201
+ /** What a section in `values` paints under its content: its own fill over the page, or the page. */
202
+ function sectionGround(values: Values): Rgba[] {
203
+ const pageInk = parseCssColour(values['text']);
204
+ const page = paintColours(values['page-bg'], pageInk);
205
+ const sectionPaint = paintColours(values['section-bg'], pageInk);
206
+ return opaque(sectionPaint) ? sectionPaint.map(c => (c.a < 1 ? compositeOver(c, page[0] ?? WHITE) : c)) : page;
207
+ }
208
+
209
+ /**
210
+ * A container that wears a NON-default scheme, inside a section of every scheme (vectoriox/iox-ui#15 — the
211
+ * Featured card is a Card in Brand on a Default page). Its tokens are its own scheme's laid over the outer
212
+ * section's (what it does not set, it inherits); a translucent fill composites over the OUTER section; the running text it holds (`body`, `lead`, a label: `color: inherit`)
213
+ * reads the scheme's ink, which is what the scheme-ink rule and `withSchemeInk` make CSS compute.
214
+ */
215
+ function schemedContainerFindings(rows: ReadonlyArray<DesignTokenLike>, mode: ResolvedMode, section: StyleMap,
216
+ containers: ReadonlyArray<ElementStyle>): ContrastFinding[] {
217
+ const out: ContrastFinding[] = [];
218
+ for (const outer of SCHEME_IDS) {
219
+ const ground = sectionGround(resolveSchemeTokenValues(rows, mode, outer));
220
+ for (const inner of SCHEME_IDS.filter(id => id !== DEFAULT_SCHEME)) {
221
+ const values = resolveSchemeTokenValues(rows, mode, [outer, inner]);
222
+ const schemeInk = parseCssColour(resolveIn(values, SCHEME_INK), parseCssColour(resolveIn(values, section['color'])));
223
+ for (const box of containers) {
224
+ const style = ownStyle(box);
225
+ const bgs = overAll(paintColours(resolveIn(values, style['backgroundColor']), schemeInk), ground);
226
+ const ink = style['color'] && style['color'] !== 'inherit' ? parseCssColour(resolveIn(values, style['color']), schemeInk) : schemeInk;
227
+ if (!ink) continue;
228
+ for (const g of bgs) {
229
+ const bg = quantizeRgb(g);
230
+ const f = quantizeRgb(ink.a < 1 ? compositeOver(ink, bg) : ink);
231
+ const ratio = wcagContrastRatio(f, bg);
232
+ out.push({
233
+ scheme: inner, container: `${box.id} in a ${outer} section`, element: INHERITED_TEXT, part: 'text',
234
+ fg: toHexColor(f), bg: toHexColor(bg), ratio: Math.round(ratio * 100) / 100, min: WCAG_AA_TEXT, pass: ratio >= WCAG_AA_TEXT - 1e-9,
235
+ });
236
+ }
237
+ }
238
+ }
239
+ }
240
+ return out;
241
+ }
242
+
194
243
  /** Text over an image: the block's ink — and every token its ink scope pins to it — over the scrim
195
244
  * composited on the WORST-CASE image, in the scheme the block wears. Never over the section: the image
196
245
  * covers it. */
@@ -1348,6 +1348,7 @@ export const SPEC_SCHEMES = {
1348
1348
  "set": {
1349
1349
  "section-bg": "{{accent}}",
1350
1350
  "bg": "{{accent}}",
1351
+ "nav-bg": "{{accent}}",
1351
1352
  "surface": "{{accent-hover}}",
1352
1353
  "surface-2": "{{accent-hover}}",
1353
1354
  "surface-glass": "{{accent-hover}}",
@@ -1371,6 +1372,8 @@ export const SPEC_SCHEMES = {
1371
1372
  }
1372
1373
  } as const;
1373
1374
 
1375
+ export const SPEC_SCHEME_INK = "var(--iox-text)" as const;
1376
+
1374
1377
  export const SPEC_A11Y = {
1375
1378
  "reducedMotion": {
1376
1379
  "dur-fast": "0ms",
@@ -2012,6 +2015,23 @@ export const SPEC_ELEMENT_STYLES = [
2012
2015
  "fontWeight": "var(--iox-weight-label)"
2013
2016
  }
2014
2017
  },
2018
+ {
2019
+ "id": "badge-label",
2020
+ "label": "Badge label",
2021
+ "group": "Content blocks",
2022
+ "appliesTo": [
2023
+ "Text"
2024
+ ],
2025
+ "base": {
2026
+ "fontFamily": "var(--iox-font-label)",
2027
+ "fontSize": "var(--iox-badge-size)",
2028
+ "fontWeight": "var(--iox-weight-label)",
2029
+ "lineHeight": "inherit",
2030
+ "letterSpacing": "inherit",
2031
+ "textTransform": "inherit",
2032
+ "color": "inherit"
2033
+ }
2034
+ },
2015
2035
  {
2016
2036
  "id": "media-scrim",
2017
2037
  "label": "Text over image",
@@ -1,5 +1,5 @@
1
1
  import { ELEMENT_STYLE_MARKER_CLASS, elementStyleClass } from './element-styles';
2
- import { SPEC_SCHEMES } from './flavor-spec.generated';
2
+ import { SPEC_SCHEME_INK, SPEC_SCHEMES } from './flavor-spec.generated';
3
3
 
4
4
  /**
5
5
  * The design-system classes a node renders, in BOTH renderers (builder canvas, engine component
@@ -104,6 +104,54 @@ export function colorSchemeOf(node: DesignClassSource | null | undefined): strin
104
104
  return typeof trait === 'string' ? trait : null;
105
105
  }
106
106
 
107
+ /**
108
+ * The ink a section colour scheme gives everything inside it (vectoriox/iox-ui#15). A scheme re-declares
109
+ * tokens only, so without this a Container wearing one kept the colour it INHERITED from the section around
110
+ * it: a Featured (Brand) card's body text stayed the page's dark ink on the Brand fill, 2.54:1. Two halves,
111
+ * one rule: `compileSchemeCss` gives every `iox-scheme-*` element `color: var(--iox-text)` (the node rule,
112
+ * emitted after it at the same specificity, still wins), and {@link withSchemeInk} reads a scheme'd node's
113
+ * own `inherit` — what the card / box / panel element styles write for "no colour of my own" — as this ink.
114
+ * The value is the contract's `schemeInk`.
115
+ */
116
+ export const SCHEME_INK: string = SPEC_SCHEME_INK;
117
+
118
+ /** The values that mean "no colour of my own" — `color` is inherited, so these equal no declaration. */
119
+ const INHERITED_INK: ReadonlySet<string> = new Set(['inherit', 'unset']);
120
+
121
+ /**
122
+ * A node's style map as its renderer compiles it under a section colour scheme: a NON-default scheme turns
123
+ * an inherited `color` into {@link SCHEME_INK}; anything else — no scheme, an author's own colour, a token —
124
+ * is returned untouched (the same object). Every render path calls it on every rule it compiles for the
125
+ * node (base, states, breakpoint layers): the builder's StyleRegistryService and both engine paths.
126
+ */
127
+ export function withSchemeInk<T extends Record<string, unknown>>(styles: T, scheme: unknown): T {
128
+ if (!styles || !colorSchemeClass(scheme)) return styles;
129
+ const color = styles['color'];
130
+ if (typeof color !== 'string' || !INHERITED_INK.has(color.trim().toLowerCase())) return styles;
131
+ return { ...styles, color: SCHEME_INK };
132
+ }
133
+
134
+ /**
135
+ * Write a node's section colour scheme — THE one writer, so every control that offers Section colours (the
136
+ * Style panel's Element style block, Card ▸ Card) edits the same setting: the `colorScheme` content trait on
137
+ * a builder node, `props.colorScheme` on a saved one. False when nothing changed: an unknown id, the value it
138
+ * already holds, or a builder node with no such trait (a type that offers no scheme).
139
+ */
140
+ export function setColorScheme(
141
+ node: { props?: Record<string, unknown> | null; traits?: Array<{ name?: string; default?: unknown }> | null } | null | undefined,
142
+ id: string,
143
+ ): boolean {
144
+ if (!node || !SCHEME_IDS.has(id) || (colorSchemeOf(node) ?? ColorScheme.Default) === id) return false;
145
+ if (Array.isArray(node.traits)) {
146
+ const trait = node.traits.find(t => t?.name === COLOR_SCHEME_TRAIT);
147
+ if (!trait) return false;
148
+ trait.default = id;
149
+ return true;
150
+ }
151
+ node.props = { ...(node.props ?? {}), [COLOR_SCHEME_TRAIT]: id };
152
+ return true;
153
+ }
154
+
107
155
  /** True when the node wears a known element style — the marker is on it. */
108
156
  export function hasElementStyleMarker(node: { elementStyle?: string | null } | null | undefined): boolean {
109
157
  return elementStyleClass(node?.elementStyle) !== null;
@@ -1,5 +1,6 @@
1
1
  import { DesignMode } from './design-token.model';
2
2
  import { SPEC_A11Y, SPEC_SCHEMES } from './flavor-spec.generated';
3
+ import { SCHEME_INK } from './node-classes';
3
4
  import { specTokenNames } from './token-names';
4
5
 
5
6
  /**
@@ -10,7 +11,8 @@ import { specTokenNames } from './token-names';
10
11
  *
11
12
  * Block order (design-system plan §3e, T8):
12
13
  * 1. base `:root { --iox-*: … }`
13
- * 2. schemes `.iox-scheme-<id> { overrides; every var()-referencing token re-declared }`
14
+ * 2. schemes `.iox-scheme-<id> { overrides; every var()-referencing token re-declared }`, then the
15
+ * ONE scheme-ink rule `.iox-scheme-subtle, … { color: var(--iox-text) }` (iox-ui#15)
14
16
  * 3. reduced motion `@media (prefers-reduced-motion: reduce)`
15
17
  * 4. reduced transparency `@media (prefers-reduced-transparency: reduce)`
16
18
  * 5. dark — LAST `@media (prefers-color-scheme: dark)`
@@ -167,19 +169,29 @@ export function resolveTokenValues(
167
169
  * Every token's value INSIDE a section wearing `schemeId`, in one mode — what CSS computes there: the
168
170
  * scheme's overrides (`{{token}}` / `{{~token}}` placeholders substituted from the root), every other
169
171
  * 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.
172
+ * makes that the section's own resolution). `default` (or an unknown id) is the root. A CHAIN
173
+ * (`['inverse', 'accent']`, outermost first) is an element wearing the last scheme inside elements wearing
174
+ * the ones before it: a token the inner scheme does not set is the outer one's (custom properties inherit).
171
175
  */
172
176
  export function resolveSchemeTokenValues(
173
177
  tokens: ReadonlyArray<DesignTokenLike> | null | undefined,
174
178
  mode: ResolvedMode,
175
- schemeId: string,
179
+ schemeId: string | ReadonlyArray<string>,
176
180
  ): Record<string, string> {
177
181
  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);
182
+ const sets = (typeof schemeId === 'string' ? [schemeId] : schemeId)
183
+ .map(id => (id === DEFAULT_SCHEME ? undefined : (SPEC_SCHEMES as Readonly<Record<string, { set: Readonly<Record<string, string>> }>>)[id]?.set));
184
+ if (!sets.some(Boolean)) return resolveTokenValues(rows, mode);
180
185
  const root = resolveTokenValues(rows, mode);
181
186
  const other = resolveTokenValues(rows, mode === DARK ? LIGHT : DARK);
182
- const overrides = new Map(schemeEntries(set, root, other));
187
+ // Nested (`[outer, inner]` — a Featured card in an Inverse section): each scheme block re-declares the
188
+ // rows that reference a token, so at every level those are restored before the block's own set.
189
+ const overrides = new Map<string, string>();
190
+ for (const set of sets) {
191
+ if (!set) continue;
192
+ for (const t of rows) if (modeValue(t, mode).includes(REFERENCE)) overrides.delete(t.name);
193
+ for (const [name, value] of schemeEntries(set, root, other)) overrides.set(name, value);
194
+ }
183
195
  return resolveTokenValues(rows.map(t => ({ name: t.name, value: overrides.get(t.name) ?? modeValue(t, mode) })), LIGHT);
184
196
  }
185
197
 
@@ -243,11 +255,27 @@ export function compileSchemeCss(
243
255
  if (!rows.length) return '';
244
256
  const resolved = resolveTokenValues(rows, LIGHT);
245
257
  const other = opposite ?? resolveTokenValues(rows, DARK);
246
- return nonDefaultSchemes().map(([id, set]) => rule(schemeSelector(id, scope), [
247
- ...schemeEntries(set, resolved, other),
248
- ...rows.filter(t => !(t.name in set) && t.value.includes(REFERENCE))
249
- .map((t): [string, string] => [t.name, t.value]),
250
- ])).filter(Boolean).join('\n');
258
+ return [
259
+ ...nonDefaultSchemes().map(([id, set]) => rule(schemeSelector(id, scope), [
260
+ ...schemeEntries(set, resolved, other),
261
+ ...rows.filter(t => !(t.name in set) && t.value.includes(REFERENCE))
262
+ .map((t): [string, string] => [t.name, t.value]),
263
+ ])),
264
+ schemeInkRule(scope),
265
+ ].filter(Boolean).join('\n');
266
+ }
267
+
268
+ /**
269
+ * Every element wearing a scheme takes the scheme's ink (vectoriox/iox-ui#15): the tokens re-resolve inside
270
+ * it, but `color` is inherited as a COMPUTED value, so without this a scheme'd Container's running text kept
271
+ * the outer section's ink. One rule, specificity (0,1,0) in every renderer — a builder scope is wrapped in
272
+ * `:where()` — emitted with the token CSS, BEFORE every node rule: a node's own colour (same specificity,
273
+ * later) still wins. A node's `color: inherit` is read as this ink by `withSchemeInk`.
274
+ */
275
+ function schemeInkRule(scope?: string): string {
276
+ const where = scope ? `:where(${scope}) ` : '';
277
+ const selectors = nonDefaultSchemes().map(([id]) => `${where}.iox-scheme-${id}`);
278
+ return selectors.length ? `${selectors.join(', ')} {\n color: ${SCHEME_INK};\n}` : '';
251
279
  }
252
280
 
253
281
  /** The dark twin of both scheme rules: a re-declared token with a dark value would otherwise pin