@vectoriox/iox-ui 5.13.1 → 5.15.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.
@@ -77,19 +77,56 @@ export function parseFontFamilies(stack: string): string[] {
77
77
  .filter(Boolean);
78
78
  }
79
79
 
80
+ /** Where Google serves the stylesheets, and the origin the font FILES come from (preconnect). */
81
+ export const GOOGLE_FONTS_CSS_ORIGIN = 'https://fonts.googleapis.com';
82
+ export const GOOGLE_FONTS_FILE_ORIGIN = 'https://fonts.gstatic.com';
83
+
84
+ /** The ONE stylesheet URL for a catalog id — both renderers link exactly this (one per family, T7). */
85
+ export function googleFontHref(id: string): string {
86
+ return `${GOOGLE_FONTS_CSS_ORIGIN}/css2?family=${id}&display=swap`;
87
+ }
88
+
89
+ const CATALOG_BY_LOWER = new Map(Object.entries(FONT_CATALOG).map(([family, id]) => [family.toLowerCase(), id]));
90
+
91
+ /** The catalog id of a family name (case-insensitive), or null — a system or uploaded face has none. */
92
+ export function catalogFontId(family: string | null | undefined): string | null {
93
+ return CATALOG_BY_LOWER.get(String(family ?? '').trim().toLowerCase()) ?? null;
94
+ }
95
+
96
+ const MONO_ROLE = 'font-mono';
97
+ const ALWAYS_LOADED_ROLES = FONT_FAMILY_TOKENS.filter(role => role !== MONO_ROLE);
98
+ const MONO_REFERENCE = /var\(\s*--iox-font-mono\b/;
99
+
100
+ export interface FontsForTokensOptions {
101
+ /**
102
+ * The CSS that will actually render (node rules, element styles). `font-mono` is loaded only when
103
+ * something READS it — here or in another token's value — because no element style does: an org
104
+ * would otherwise download a monospace face for a site with no code on it.
105
+ */
106
+ usedCss?: string;
107
+ }
108
+
80
109
  /**
81
- * The Google css2 ids to load for a token set: every catalog family named by a font role token,
82
- * one id per family (T7 — one `<link>` per family, so one bad id cannot sink the others).
110
+ * The Google css2 ids to load for a token set: every catalog family named by the display / body /
111
+ * label roles, plus mono when referenced (above). One id per family (T7 — one `<link>` per family, so
112
+ * one bad id cannot sink the others), in role order.
83
113
  */
84
- export function fontsForTokens(tokens: ReadonlyArray<DesignTokenLike> | null | undefined): string[] {
114
+ export function fontsForTokens(
115
+ tokens: ReadonlyArray<DesignTokenLike> | null | undefined,
116
+ opts: FontsForTokensOptions = {},
117
+ ): string[] {
118
+ const rows = (tokens ?? []).filter(t => !!t && typeof t.name === 'string');
119
+ const monoRead = MONO_REFERENCE.test(opts.usedCss ?? '')
120
+ || rows.some(t => t.name !== MONO_ROLE && MONO_REFERENCE.test(`${t.value ?? ''} ${t.values?.dark ?? ''}`));
121
+ const roles = monoRead ? FONT_FAMILY_TOKENS : ALWAYS_LOADED_ROLES;
85
122
  const seen = new Set<string>();
86
123
  const ids: string[] = [];
87
- for (const role of FONT_FAMILY_TOKENS) {
88
- const row = (tokens ?? []).find(t => t?.name === role);
124
+ for (const role of roles) {
125
+ const row = rows.find(t => t.name === role);
89
126
  for (const family of parseFontFamilies(row?.value ?? '')) {
90
- const id = FONT_CATALOG[family];
91
- if (id && !seen.has(family)) {
92
- seen.add(family);
127
+ const id = catalogFontId(family);
128
+ if (id && !seen.has(id)) {
129
+ seen.add(id);
93
130
  ids.push(id);
94
131
  }
95
132
  }
@@ -0,0 +1,82 @@
1
+ import { ELEMENT_STYLE_MARKER_CLASS, elementStyleClass } from './element-styles';
2
+ import { SPEC_SCHEMES } from './flavor-spec.generated';
3
+
4
+ /**
5
+ * The design-system classes a node renders, in BOTH renderers (builder canvas, engine component
6
+ * path, engine string path) — ONE helper, so the three can never disagree about which class a node
7
+ * carries:
8
+ *
9
+ * - the element-style MARKER `iox-es iox-es-<id>` (design-system §3b-bis) — component CSS carrying
10
+ * the system look is scoped to it, so a node saved before DS2 keeps its pre-DS2 look;
11
+ * - the section colour scheme `iox-scheme-<id>` (§3c) for a non-default `colorScheme` — the class
12
+ * `compileSchemeCss` re-declares tokens on, so everything inside re-resolves.
13
+ *
14
+ * Renderers read `elementStyle` and `colorScheme` for exactly this and nothing else.
15
+ */
16
+
17
+ /** The section colour schemes — the contract's `schemes` keys. */
18
+ export enum ColorScheme {
19
+ Default = 'default',
20
+ Subtle = 'subtle',
21
+ Inverse = 'inverse',
22
+ Accent = 'accent',
23
+ }
24
+
25
+ /** The content trait (stored in `props`, not a style prop) that picks a node's scheme. */
26
+ export const COLOR_SCHEME_TRAIT = 'colorScheme';
27
+
28
+ /** The node types that offer a scheme (spec §3c: Section/Container; LinkedContainer is a Container that links). */
29
+ export const COLOR_SCHEME_TYPES: ReadonlyArray<string> = ['Section', 'Container', 'LinkedContainer'];
30
+
31
+ /** Every scheme in contract order, with the contract's label (Default · Subtle · Inverse · Brand). */
32
+ export const COLOR_SCHEMES: ReadonlyArray<{ readonly id: ColorScheme; readonly label: string }> =
33
+ (Object.keys(SPEC_SCHEMES) as ColorScheme[]).map(id => ({ id, label: (SPEC_SCHEMES as Record<string, { label: string }>)[id].label }));
34
+
35
+ const SCHEME_IDS = new Set<string>(Object.keys(SPEC_SCHEMES));
36
+ const SCHEME_PREFIX = 'iox-scheme-';
37
+ const MARKER_PREFIX = `${ELEMENT_STYLE_MARKER_CLASS}-`;
38
+
39
+ /** `iox-scheme-<id>` for a known NON-default scheme, else null — an unknown id never reaches a class attribute. */
40
+ export function colorSchemeClass(id: unknown): string | null {
41
+ return typeof id === 'string' && id !== ColorScheme.Default && SCHEME_IDS.has(id) ? `${SCHEME_PREFIX}${id}` : null;
42
+ }
43
+
44
+ /** The node shapes this module reads: a serialized node (`props`) or a builder node (`traits[]`). */
45
+ export interface DesignClassSource {
46
+ elementStyle?: string | null;
47
+ props?: Readonly<Record<string, unknown>> | null;
48
+ traits?: ReadonlyArray<{ name?: string; default?: unknown }> | null;
49
+ }
50
+
51
+ /** A node's `colorScheme`, from `props` (serialized) or its trait (builder). */
52
+ export function colorSchemeOf(node: DesignClassSource | null | undefined): string | null {
53
+ if (!node) return null;
54
+ const fromProps = node.props?.[COLOR_SCHEME_TRAIT];
55
+ if (fromProps !== undefined) return typeof fromProps === 'string' ? fromProps : null;
56
+ const trait = node.traits?.find(t => t?.name === COLOR_SCHEME_TRAIT)?.default;
57
+ return typeof trait === 'string' ? trait : null;
58
+ }
59
+
60
+ /** True when the node wears a known element style — the marker is on it. */
61
+ export function hasElementStyleMarker(node: { elementStyle?: string | null } | null | undefined): boolean {
62
+ return elementStyleClass(node?.elementStyle) !== null;
63
+ }
64
+
65
+ /** The classes a node renders: `'iox-es iox-es-card iox-scheme-inverse'`, or `''` for none. */
66
+ export function nodeDesignClasses(node: DesignClassSource | null | undefined): string {
67
+ const marker = elementStyleClass(node?.elementStyle);
68
+ return [marker ? `${ELEMENT_STYLE_MARKER_CLASS} ${marker}` : '', colorSchemeClass(colorSchemeOf(node)) ?? '']
69
+ .filter(Boolean).join(' ');
70
+ }
71
+
72
+ const isDesignClass = (c: string) =>
73
+ c === ELEMENT_STYLE_MARKER_CLASS || c.startsWith(MARKER_PREFIX) || c.startsWith(SCHEME_PREFIX);
74
+
75
+ /** Make an element's design classes match the node — the live switch on the canvas and the
76
+ * component path's one-time stamp. Removes stale ones; touches no other class. */
77
+ export function syncNodeDesignClasses(el: { classList: DOMTokenList } | null | undefined, node: DesignClassSource | null | undefined): void {
78
+ if (!el?.classList) return;
79
+ for (const c of Array.from(el.classList).filter(isDesignClass)) el.classList.remove(c);
80
+ const classes = nodeDesignClasses(node);
81
+ if (classes) el.classList.add(...classes.split(' '));
82
+ }
@@ -14,9 +14,13 @@ export * from './css-names';
14
14
  export * from './token-css';
15
15
  export * from './flavor';
16
16
  export * from './element-styles';
17
+ export * from './node-classes';
18
+ export * from './empty-image';
17
19
  export * from './brand';
18
20
  export * from './flavor-tokens';
19
21
  export * from './flavor-seed';
20
22
  export * from './spring';
21
23
  export * from './fonts';
22
24
  export * from './token-names';
25
+ export * from './design-settings';
26
+ export * from './design-apply';
@@ -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 { specTokenNames } from './token-names';
3
4
 
4
5
  /**
5
6
  * Design-token → CSS compilation, shared by BOTH renderers (flavors F0, design-system DS1).
@@ -14,6 +15,11 @@ import { SPEC_A11Y, SPEC_SCHEMES } from './flavor-spec.generated';
14
15
  * 4. reduced transparency `@media (prefers-reduced-transparency: reduce)`
15
16
  * 5. dark — LAST `@media (prefers-color-scheme: dark)`
16
17
  *
18
+ * MODE (design-system D6) is a compile choice, never a row change: `auto` is the visitor-following
19
+ * order above; `light` compiles the base from light values and emits no dark block; `dark` compiles
20
+ * the base from DARK values (schemes and placeholders resolved from them too) and emits no dark block —
21
+ * so a Midnight site is dark for a light-OS visitor.
22
+ *
17
23
  * ⚠️ The appended-last contract: the dark block REDEFINES custom properties, so every consumer
18
24
  * appends it AFTER all of its base rules — the engine pushes it after the page's css blocks, the
19
25
  * builder registry keeps it as its last token rule (responsive A3 lesson).
@@ -32,6 +38,16 @@ export interface DesignTokenLike {
32
38
  export interface TokenCssOptions {
33
39
  /** Selector that carries the variables instead of `:root` — a preview wrapper, a specimen. */
34
40
  scope?: string;
41
+ /** The studio's colour mode (`settings.design.mode`). Absent = `auto`. */
42
+ mode?: DesignMode;
43
+ /**
44
+ * Declare every SPEC token the rows lack as `initial` on the scope (the guaranteed-invalid value),
45
+ * so a `var()` inside the scope cannot inherit a same-named property from an ANCESTOR. The builder
46
+ * needs it: the admin's `:root` chrome tokens share names with org tokens (`accent`, `surface-2`…),
47
+ * and a missing org row would silently paint the canvas in admin chrome. With it, the canvas
48
+ * computes exactly what the live site computes for that row — unset.
49
+ */
50
+ isolate?: boolean;
35
51
  }
36
52
 
37
53
  export interface TokenCssBlocks {
@@ -114,6 +130,16 @@ const DARK: ResolvedMode = 'dark';
114
130
 
115
131
  const modeValue = (t: DesignTokenLike, mode: ResolvedMode): string => (mode === DARK ? darkOf(t) ?? t.value : t.value);
116
132
 
133
+ /** The rows as ONE mode: each row's value in that mode, no dark override left. `auto` (or none) →
134
+ * the rows unchanged. */
135
+ function rowsForMode(rows: ReadonlyArray<DesignTokenLike>, mode: DesignMode | undefined): DesignTokenLike[] {
136
+ if (mode !== LIGHT && mode !== DARK) return [...rows];
137
+ return rows.map(t => {
138
+ const { values: _dropped, ...rest } = t;
139
+ return { ...rest, value: modeValue(t, mode) };
140
+ });
141
+ }
142
+
117
143
  /** Every token's ROOT value in one mode, with `var(--iox-…)` references resolved against the same
118
144
  * set (recursively; a reference cycle or an unknown token is left as written). */
119
145
  export function resolveTokenValues(
@@ -223,10 +249,14 @@ export function compileTokenCssBlocks(
223
249
  tokens: ReadonlyArray<DesignTokenLike> | null | undefined,
224
250
  opts: TokenCssOptions = {},
225
251
  ): TokenCssBlocks {
226
- const rows = usableTokens(tokens);
252
+ const rows = rowsForMode(usableTokens(tokens), opts.mode);
227
253
  const root = opts.scope ?? ROOT;
254
+ const present = new Set(rows.map(t => t.name));
255
+ const unset: Array<[string, string]> = opts.isolate
256
+ ? [...specTokenNames()].filter(name => !present.has(name)).map((name): [string, string] => [name, 'initial'])
257
+ : [];
228
258
  const base = [
229
- rule(root, rows.map((t): [string, string] => [t.name, t.value])),
259
+ rule(root, [...unset, ...rows.map((t): [string, string] => [t.name, t.value])]),
230
260
  compileSchemeCss(rows, opts.scope),
231
261
  ...ACCESSIBILITY_MEDIA.map(({ query, overrides }) =>
232
262
  media(query, [rule(root, presentOverrides(rows, overrides, false), ' ')])),
@@ -30,3 +30,24 @@ export function unknownTokenReferences(css: string, alsoAllowed: Iterable<string
30
30
  const extra = new Set(alsoAllowed);
31
31
  return tokenReferences(css).filter(name => !spec.has(name) && !extra.has(name));
32
32
  }
33
+
34
+ const LEGACY_SCOPE = /:not\(\s*\.iox-es\s*\)/;
35
+ const RULE = /([^{}]+)\{([^{}]*)\}/g;
36
+
37
+ /**
38
+ * Tokens read by a LEGACY rule — any rule whose selector is scoped `:not(.iox-es)` (the pre-DS2 look
39
+ * of a node that wears no element style, design-system §3b-bis). Such a rule must read NO token at
40
+ * all: a token it reads repaints a pre-DS2 page the moment the org's rows are seeded or a flavor is
41
+ * applied (D5). Returns `selector → names` for each offending rule; comments are ignored.
42
+ */
43
+ export function legacyRuleTokenReferences(css: string): Array<{ selector: string; tokens: string[] }> {
44
+ const text = String(css ?? '').replace(/\/\*[\s\S]*?\*\//g, '');
45
+ const out: Array<{ selector: string; tokens: string[] }> = [];
46
+ for (const m of text.matchAll(RULE)) {
47
+ const selector = m[1].trim().replace(/\s+/g, ' ');
48
+ if (!LEGACY_SCOPE.test(selector)) continue;
49
+ const tokens = tokenReferences(m[2]);
50
+ if (tokens.length) out.push({ selector, tokens });
51
+ }
52
+ return out;
53
+ }
@@ -0,0 +1,30 @@
1
+ /* Design-system component CSS that EVERY renderer needs — the ONE copy.
2
+ *
3
+ * Same reason as iox-forms.css: the SSR engine's string renderer emits plain markup, the engine's
4
+ * component path renders the shared components, and the builder canvas renders its own twins, so a
5
+ * component-scoped copy in each would be three copies that drift. Consumers add this file to their
6
+ * `angular.json` styles.
7
+ *
8
+ * Everything here is scoped to the element-style marker (design-system §3b-bis): a node saved
9
+ * before element styles never matches, so it renders exactly as before.
10
+ */
11
+
12
+ /* Empty images (DS3). A marked Image with no source carries `data-iox-empty` and a size-less
13
+ placeholder src (design-core `emptyImageSrc`), so it stays a real replaced element: its own
14
+ aspect-ratio and object-fit apply and this fill shows through — on the canvas behind the
15
+ "Add image" label, on the live site on its own. Out-specifies the node rule (0,2,1 vs 0,1,0)
16
+ only while the image is empty. */
17
+ img.iox-es-media[data-iox-empty] {
18
+ background-color: var(--iox-surface-2);
19
+ }
20
+
21
+ /* An empty marked Gallery takes the same fill, on the element that carries its node rule (size,
22
+ radius): the string renderer's one div, or the component's inner `.iox-gallery` under the
23
+ `<iox-gallery>` host that wears the marker. Its cinematic overlay has nothing to darken. */
24
+ .iox-gallery.iox-es-media[data-iox-empty],
25
+ iox-gallery.iox-es-media[data-iox-empty] > .iox-gallery {
26
+ background-color: var(--iox-surface-2);
27
+ }
28
+ .iox-es-media[data-iox-empty] .iox-gallery__overlay {
29
+ display: none;
30
+ }