@vectoriox/iox-ui 5.29.1 → 5.31.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/composite-core/index.d.ts +270 -0
- package/composite-core/package.json +3 -0
- package/design-core/index.d.ts +870 -41
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs +698 -0
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -0
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +1090 -82
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-forms-core.mjs +74 -1
- package/fesm2022/vectoriox-iox-ui-forms-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs +110 -30
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +210 -31
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/forms-core/index.d.ts +45 -2
- package/generation-core/index.d.ts +35 -23
- package/index.d.ts +42 -15
- package/package.json +5 -1
- package/src/composite-core/badge.ts +103 -0
- package/src/composite-core/card.ts +524 -0
- package/src/composite-core/composite-node.ts +143 -0
- package/src/composite-core/public-api.ts +11 -0
- package/src/composite-core/structure.ts +58 -0
- package/src/design-core/contrast-audit.ts +38 -5
- package/src/design-core/flavor-fixtures.generated.ts +144 -144
- package/src/design-core/flavor-spec.generated.ts +970 -26
- package/src/design-core/node-classes.ts +16 -4
- package/src/design-core/node-links.ts +29 -0
- package/src/forms-core/nesting.ts +92 -0
- package/src/forms-core/public-api.ts +1 -0
- package/src/generation-core/compose-site.ts +55 -1
- package/src/generation-core/node-baselines.generated.ts +14 -2
- package/src/generation-core/patterns.ts +29 -2
- package/src/generation-core/serialized-node.ts +2 -0
- package/src/generation-core/site-spec.generated.ts +13 -26
- package/styles/iox-design.css +62 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { CardPart, CardRole } from './card';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The structure every composite declares (composite-components plan §3a `RoleSpec`): where a role may
|
|
5
|
+
* live and whether it is LOCKED (a structural part that cannot be deleted or moved out — a card always
|
|
6
|
+
* has its content column and its title). Optional parts carry the Parts switch that re-adds them.
|
|
7
|
+
* The builder's delete paths and its drop/paste rule ask `structureRefusal` — ONE answer for all of them.
|
|
8
|
+
*/
|
|
9
|
+
export interface CompositeRoleSpec {
|
|
10
|
+
/** The role of the part's parent; null for a composite root. */
|
|
11
|
+
readonly parent: string | null;
|
|
12
|
+
readonly locked?: boolean;
|
|
13
|
+
readonly part?: CardPart;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const COMPOSITE_ROLE_SPECS: Readonly<Record<string, CompositeRoleSpec>> = {
|
|
17
|
+
[CardRole.Card]: { parent: null },
|
|
18
|
+
[CardRole.Media]: { parent: CardRole.Card, part: CardPart.Media },
|
|
19
|
+
[CardRole.Badge]: { parent: null, part: CardPart.Badge },
|
|
20
|
+
[CardRole.Content]: { parent: CardRole.Card, locked: true },
|
|
21
|
+
[CardRole.Eyebrow]: { parent: CardRole.Content, part: CardPart.Eyebrow },
|
|
22
|
+
[CardRole.Title]: { parent: CardRole.Content, locked: true },
|
|
23
|
+
[CardRole.Text]: { parent: CardRole.Content, part: CardPart.Text },
|
|
24
|
+
[CardRole.Actions]: { parent: CardRole.Content, part: CardPart.Actions },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export enum StructureEdit {
|
|
28
|
+
Delete = 'delete',
|
|
29
|
+
/** A drop, paste or wrap that would put the node somewhere new. */
|
|
30
|
+
Move = 'move',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export enum StructureRefusal {
|
|
34
|
+
/** A locked part cannot be deleted (the card's Content and Title). */
|
|
35
|
+
LockedPart = 'locked-part',
|
|
36
|
+
/** A locked part can only live directly inside its own parent role. */
|
|
37
|
+
LockedPlacement = 'locked-placement',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface RoleLike { role?: string | null }
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Why the structure guard refuses `edit` on `node`, or null when it is allowed. `ancestors` (Move only) =
|
|
44
|
+
* the new parent chain, root first, the direct parent last.
|
|
45
|
+
*/
|
|
46
|
+
export function structureRefusal(node: RoleLike | null | undefined, edit: StructureEdit, ancestors: ReadonlyArray<RoleLike> = []): StructureRefusal | null {
|
|
47
|
+
const spec = node?.role ? COMPOSITE_ROLE_SPECS[node.role] : undefined;
|
|
48
|
+
if (!spec?.locked) return null;
|
|
49
|
+
if (edit === StructureEdit.Delete) return StructureRefusal.LockedPart;
|
|
50
|
+
const parent = ancestors[ancestors.length - 1];
|
|
51
|
+
return parent?.role === spec.parent ? null : StructureRefusal.LockedPlacement;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** True when `node` is a locked part (shown with a lock and no delete in the layer tree). Deleting a whole
|
|
55
|
+
* composite is always allowed — only its locked parts are refused. */
|
|
56
|
+
export function isLockedPart(node: RoleLike | null | undefined): boolean {
|
|
57
|
+
return !!(node?.role && COMPOSITE_ROLE_SPECS[node.role]?.locked);
|
|
58
|
+
}
|
|
@@ -17,7 +17,9 @@ import { DesignTokenLike, ResolvedMode, resolveSchemeTokenValues } from './token
|
|
|
17
17
|
* a gradient ground (Glass's mesh, Midnight's glow) checked at its worst stop;
|
|
18
18
|
* - base and hover; a button's icon read through the contract's `inkScopes` (it takes the button's ink),
|
|
19
19
|
* its focus ring; the form parts the field baseline paints (`contrastAudit.formParts`); the form's
|
|
20
|
-
* Success / Error notices, in every scheme and container
|
|
20
|
+
* Success / Error notices, in every scheme and container;
|
|
21
|
+
* - 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`).
|
|
21
23
|
*
|
|
22
24
|
* WCAG 2: 4.5:1 for text, 3:1 for glyphs, UI boundaries and focus indicators (1.4.11), judged on 8-bit
|
|
23
25
|
* colours. The same audit gates the contract (`colour.mjs auditContrast`, every flavor × tint × a seed
|
|
@@ -45,16 +47,20 @@ interface FormPart {
|
|
|
45
47
|
readonly thumb?: string;
|
|
46
48
|
readonly min: number;
|
|
47
49
|
}
|
|
48
|
-
interface InkScope { readonly styles: ReadonlyArray<string>; readonly set: Readonly<Record<string, string>>; }
|
|
50
|
+
interface InkScope { readonly styles: ReadonlyArray<string>; readonly set: Readonly<Record<string, string>>; readonly children?: boolean; }
|
|
51
|
+
interface Scrim { readonly style: string; readonly token: string; readonly scheme: string; readonly image: string; }
|
|
49
52
|
|
|
50
53
|
const FORM_PARTS: ReadonlyArray<FormPart> = SPEC_CONTRAST_AUDIT.formParts;
|
|
51
54
|
const INK_SCOPES: ReadonlyArray<InkScope> = SPEC_INK_SCOPES;
|
|
55
|
+
const SCRIMS: ReadonlyArray<Scrim> = SPEC_CONTRAST_AUDIT.scrims;
|
|
56
|
+
const SCRIM_STYLES: ReadonlySet<string> = new Set(SCRIMS.map(s => s.style));
|
|
57
|
+
/** What a scrim block's children read, beside its own ink (each pinned to it by the block's ink scope). */
|
|
58
|
+
const SCRIM_HELD_TOKENS: ReadonlyArray<string> = ['heading-color', 'eyebrow-color', 'text-muted'];
|
|
52
59
|
const SCHEME_IDS: ReadonlyArray<string> = Object.keys(SPEC_SCHEMES);
|
|
53
60
|
const CONTAINER_GROUPS: ReadonlySet<string> = new Set(['Layout', 'Overlays']);
|
|
54
61
|
const NOT_CONTENT: ReadonlySet<string> = new Set(['rule', 'media']);
|
|
55
62
|
const SECTION_STYLE = 'section';
|
|
56
63
|
const ICON_STYLE = 'icon';
|
|
57
|
-
const BUTTON_GROUP = 'Buttons';
|
|
58
64
|
const GLYPH_GROUP = 'Media';
|
|
59
65
|
const FORMS_GROUP = 'Forms';
|
|
60
66
|
const DEFAULT_SCHEME = 'default';
|
|
@@ -95,7 +101,7 @@ export function auditDesignContrast(rows: ReadonlyArray<DesignTokenLike>, mode:
|
|
|
95
101
|
...styles.filter(e => CONTAINER_GROUPS.has(e.group) && ownStyle(e)['backgroundColor'] && e.id !== SECTION_STYLE),
|
|
96
102
|
];
|
|
97
103
|
const content = styles.filter(e => !CONTAINER_GROUPS.has(e.group) && (ownStyle(e)['color'] || ownStyle(e)['backgroundColor'])
|
|
98
|
-
&& e.group !== FORMS_GROUP && !NOT_CONTENT.has(e.id));
|
|
104
|
+
&& e.group !== FORMS_GROUP && !NOT_CONTENT.has(e.id) && !SCRIM_STYLES.has(e.id));
|
|
99
105
|
const notices = styles.filter(e => e.group === FORMS_GROUP && ownStyle(e)['color']);
|
|
100
106
|
const section = ownStyle(byId.get(SECTION_STYLE) as ElementStyle);
|
|
101
107
|
const iconColour = ownStyle(byId.get(ICON_STYLE) as ElementStyle)['color'];
|
|
@@ -136,7 +142,8 @@ export function auditDesignContrast(rows: ReadonlyArray<DesignTokenLike>, mode:
|
|
|
136
142
|
const under = fill.length && opaque(fill) ? overAll(fill, bgs) : bgs;
|
|
137
143
|
const ink = s['color'] ? parseCssColour(resolveIn(values, s['color']), boxInk) : boxInk;
|
|
138
144
|
check(e.id, state === 'base' ? (glyph ? 'glyph' : 'text') : `${state} text`, ink, under, glyph ? WCAG_AA_UI : WCAG_AA_TEXT);
|
|
139
|
-
|
|
145
|
+
// What a filled element HOLDS (inkScopes): a button's or a badge's icon reads its ink.
|
|
146
|
+
if (state === 'base' && INK_SCOPES.some(sc => sc.styles.includes(e.id))) {
|
|
140
147
|
check(e.id, 'icon', parseCssColour(resolveIn(inkScoped(values, e.id), iconColour), ink), under, WCAG_AA_UI);
|
|
141
148
|
const focus = (e.focus as StyleMap | undefined)?.['boxShadow'];
|
|
142
149
|
if (focus) check(e.id, 'focus ring', ringColour(resolveIn(values, focus), ink), bgs, WCAG_AA_UI);
|
|
@@ -171,9 +178,35 @@ export function auditDesignContrast(rows: ReadonlyArray<DesignTokenLike>, mode:
|
|
|
171
178
|
}
|
|
172
179
|
}
|
|
173
180
|
}
|
|
181
|
+
for (const scrim of SCRIMS) findings.push(...scrimFindings(rows, mode, scrim, byId.get(scrim.style)));
|
|
174
182
|
return findings;
|
|
175
183
|
}
|
|
176
184
|
|
|
185
|
+
/** Text over an image: the block's ink — and every token its ink scope pins to it — over the scrim
|
|
186
|
+
* composited on the WORST-CASE image, in the scheme the block wears. Never over the section: the image
|
|
187
|
+
* covers it. */
|
|
188
|
+
function scrimFindings(rows: ReadonlyArray<DesignTokenLike>, mode: ResolvedMode, scrim: Scrim, style: ElementStyle | undefined): ContrastFinding[] {
|
|
189
|
+
const values = resolveSchemeTokenValues(rows, mode, scrim.scheme);
|
|
190
|
+
const layer = parseCssColour(values[scrim.token]);
|
|
191
|
+
const image = parseCssColour(scrim.image);
|
|
192
|
+
if (!style || !layer || !image) return [];
|
|
193
|
+
const bg = quantizeRgb(compositeOver(layer, image));
|
|
194
|
+
const ink = parseCssColour(resolveIn(values, ownStyle(style)['color']));
|
|
195
|
+
const held = inkScoped(values, scrim.style);
|
|
196
|
+
const parts: Array<[string, Rgba | null]> = [['text', ink], ...SCRIM_HELD_TOKENS.map((t): [string, Rgba | null] => [t, parseCssColour(held[t], ink)])];
|
|
197
|
+
const out: ContrastFinding[] = [];
|
|
198
|
+
for (const [part, fg] of parts) {
|
|
199
|
+
if (!fg) continue;
|
|
200
|
+
const f = quantizeRgb(fg.a < 1 ? compositeOver(fg, bg) : fg);
|
|
201
|
+
const ratio = wcagContrastRatio(f, bg);
|
|
202
|
+
out.push({
|
|
203
|
+
scheme: scrim.scheme, container: scrim.style, element: scrim.style, part: `${part} over a ${scrim.image} image`,
|
|
204
|
+
fg: toHexColor(f), bg: toHexColor(bg), ratio: Math.round(ratio * 100) / 100, min: WCAG_AA_TEXT, pass: ratio >= WCAG_AA_TEXT - 1e-9,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
177
210
|
/** The pairs the Design ▸ Colour tab lists for the CURRENT brand (Board 6 + DS9): what a filled button
|
|
178
211
|
* shows, and the link on every section scheme — each the worst case on the section itself. */
|
|
179
212
|
export enum ColourCheckId {
|