@vectoriox/iox-ui 5.30.0 → 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.
Files changed (30) hide show
  1. package/composite-core/index.d.ts +270 -0
  2. package/composite-core/package.json +3 -0
  3. package/design-core/index.d.ts +870 -41
  4. package/fesm2022/vectoriox-iox-ui-composite-core.mjs +698 -0
  5. package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -0
  6. package/fesm2022/vectoriox-iox-ui-design-core.mjs +1090 -82
  7. package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
  8. package/fesm2022/vectoriox-iox-ui-generation-core.mjs +110 -30
  9. package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
  10. package/fesm2022/vectoriox-iox-ui.mjs +134 -31
  11. package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
  12. package/generation-core/index.d.ts +35 -23
  13. package/index.d.ts +41 -15
  14. package/package.json +5 -1
  15. package/src/composite-core/badge.ts +103 -0
  16. package/src/composite-core/card.ts +524 -0
  17. package/src/composite-core/composite-node.ts +143 -0
  18. package/src/composite-core/public-api.ts +11 -0
  19. package/src/composite-core/structure.ts +58 -0
  20. package/src/design-core/contrast-audit.ts +38 -5
  21. package/src/design-core/flavor-fixtures.generated.ts +144 -144
  22. package/src/design-core/flavor-spec.generated.ts +970 -26
  23. package/src/design-core/node-classes.ts +16 -4
  24. package/src/design-core/node-links.ts +29 -0
  25. package/src/generation-core/compose-site.ts +55 -1
  26. package/src/generation-core/node-baselines.generated.ts +14 -2
  27. package/src/generation-core/patterns.ts +29 -2
  28. package/src/generation-core/serialized-node.ts +2 -0
  29. package/src/generation-core/site-spec.generated.ts +13 -26
  30. package/styles/iox-design.css +62 -0
@@ -0,0 +1,698 @@
1
+ import { readOwnedTrait, writeOwnedTrait, elementStyleById, switchElementStyle, LinkType } from '@vectoriox/iox-ui/design-core';
2
+
3
+ const usesTraits = (node) => Array.isArray(node.traits);
4
+ /** A content trait's value — `traits[].default` on a builder node, `props[name]` on a saved one. */
5
+ function readProp(node, name) {
6
+ return usesTraits(node) ? node.traits.find(t => t.name === name)?.default : node.props?.[name];
7
+ }
8
+ /** Write a content trait. False when a builder node has no such trait (the value would be dropped). */
9
+ function writeProp(node, name, value) {
10
+ if (usesTraits(node)) {
11
+ const trait = node.traits.find(t => t.name === name);
12
+ if (!trait)
13
+ return false;
14
+ trait.default = value;
15
+ return true;
16
+ }
17
+ node.props = { ...(node.props ?? {}), [name]: value };
18
+ return true;
19
+ }
20
+ /** A base style value (the node's own, either shape). */
21
+ function readStyle(node, name) {
22
+ return readOwnedTrait(node, { trait: name });
23
+ }
24
+ /** Write base style values; returns the names the node has no slot for (a builder node drops them). */
25
+ function writeStyles(node, values) {
26
+ const missing = [];
27
+ for (const [name, value] of Object.entries(values)) {
28
+ if (!writeOwnedTrait(node, { trait: name }, value))
29
+ missing.push(name);
30
+ }
31
+ return missing;
32
+ }
33
+ /** Set style values in one breakpoint layer (`responsive[bp].styleProps`, the shape both renderers'
34
+ * shared `planResponsiveRules` reads). */
35
+ function writeLayerStyles(node, breakpoint, values) {
36
+ const responsive = { ...(node.responsive ?? {}) };
37
+ const layer = { ...(responsive[breakpoint] ?? {}) };
38
+ layer.styleProps = { ...(layer.styleProps ?? {}), ...values };
39
+ responsive[breakpoint] = layer;
40
+ node.responsive = responsive;
41
+ }
42
+ /** Remove style keys from one breakpoint layer; an emptied layer (and an emptied map) is dropped. */
43
+ function clearLayerStyles(node, breakpoint, names) {
44
+ const layer = node.responsive?.[breakpoint];
45
+ if (!layer?.styleProps)
46
+ return;
47
+ const styleProps = { ...layer.styleProps };
48
+ for (const name of names)
49
+ delete styleProps[name];
50
+ const next = { ...layer };
51
+ if (Object.keys(styleProps).length)
52
+ next.styleProps = styleProps;
53
+ else
54
+ delete next.styleProps;
55
+ const responsive = { ...(node.responsive ?? {}) };
56
+ if (Object.keys(next).length)
57
+ responsive[breakpoint] = next;
58
+ else
59
+ delete responsive[breakpoint];
60
+ node.responsive = Object.keys(responsive).length ? responsive : undefined;
61
+ }
62
+ /** A breakpoint layer's own value for one style key. */
63
+ function readLayerStyle(node, breakpoint, name) {
64
+ return node.responsive?.[breakpoint]?.styleProps?.[name];
65
+ }
66
+ /** Depth-first: `node`, then every descendant. */
67
+ function walkComposite(node, visit, parent = null) {
68
+ visit(node, parent);
69
+ for (const child of node.children ?? [])
70
+ walkComposite(child, visit, node);
71
+ }
72
+ /**
73
+ * The first descendant of `root` playing `role`. Never descends into a NESTED composite of the same
74
+ * root role (a card inside a card's content keeps its own parts).
75
+ */
76
+ function findByRole(root, role) {
77
+ for (const child of root.children ?? []) {
78
+ if (child.role === role)
79
+ return child;
80
+ if (child.role === root.role && root.role)
81
+ continue;
82
+ const hit = findByRole(child, role);
83
+ if (hit)
84
+ return hit;
85
+ }
86
+ return undefined;
87
+ }
88
+ /** The node's parent inside `root`, or null. */
89
+ function parentInComposite(root, node) {
90
+ for (const child of root.children ?? []) {
91
+ if (child === node)
92
+ return root;
93
+ const hit = parentInComposite(child, node);
94
+ if (hit)
95
+ return hit;
96
+ }
97
+ return null;
98
+ }
99
+ /** Detach `node` from its parent inside `root`. True when it was found. */
100
+ function removeFromComposite(root, node) {
101
+ const parent = parentInComposite(root, node);
102
+ if (!parent?.children)
103
+ return false;
104
+ parent.children.splice(parent.children.indexOf(node), 1);
105
+ return true;
106
+ }
107
+
108
+ /**
109
+ * BADGE + CHIP (composite T25; complex-components board 8). A Badge is a Container wearing
110
+ * `badge-<tone>` › Icon + Text; its TONE is its element style, so Badge ▸ Tone writes the style and
111
+ * nothing else, and a re-theme restyles every badge. A Chip is the outlined static style only (no
112
+ * behaviour until the K5 Data view). Inside a card the badge plays `card.badge`; anywhere else it is
113
+ * recognised by its style.
114
+ */
115
+ var BadgeTone;
116
+ (function (BadgeTone) {
117
+ BadgeTone["Neutral"] = "neutral";
118
+ BadgeTone["Success"] = "success";
119
+ BadgeTone["Warning"] = "warning";
120
+ BadgeTone["Accent"] = "accent";
121
+ })(BadgeTone || (BadgeTone = {}));
122
+ const BADGE_TONES = [BadgeTone.Neutral, BadgeTone.Success, BadgeTone.Warning, BadgeTone.Accent];
123
+ /** The tone's element style (`badge-<tone>`, the contract's). */
124
+ function badgeStyleId(tone) {
125
+ return `badge-${tone}`;
126
+ }
127
+ const CHIP_STYLE = 'chip';
128
+ /** The glyph a fresh badge of each tone shows (the board's tone grid). */
129
+ const BADGE_TONE_ICONS = {
130
+ [BadgeTone.Neutral]: 'ph ph-tag',
131
+ [BadgeTone.Success]: 'ph ph-check',
132
+ [BadgeTone.Warning]: 'ph ph-warning',
133
+ [BadgeTone.Accent]: 'ph ph-sparkle',
134
+ };
135
+ const CHIP_ICON = 'ph ph-tag';
136
+ const TONE_BY_STYLE = new Map(BADGE_TONES.map(tone => [badgeStyleId(tone), tone]));
137
+ /** The badge tone a node wears, or null when it is no badge. */
138
+ function badgeToneOf(node) {
139
+ return TONE_BY_STYLE.get(node?.elementStyle ?? '') ?? null;
140
+ }
141
+ /** True for a node wearing a badge tone or the chip style — the pills (their Icon reads their ink). */
142
+ function isPillNode(node) {
143
+ return badgeToneOf(node) !== null || node?.elementStyle === CHIP_STYLE;
144
+ }
145
+ /**
146
+ * Badge ▸ Tone: move the badge to another tone's element style (an edit the author made to the old
147
+ * tone's look is kept, design-core's switch rule). Nothing else changes — not the icon, not the words.
148
+ */
149
+ function setBadgeTone(node, tone, baseline = {}) {
150
+ const current = node.elementStyle ?? null;
151
+ const next = badgeStyleId(tone);
152
+ if (!badgeToneOf(node) || current === next || !elementStyleById(next))
153
+ return false;
154
+ const result = switchElementStyle(node, current, next, baseline);
155
+ if (result.applied)
156
+ node.elementStyle = next;
157
+ return result.applied;
158
+ }
159
+ /** A pill's box: hugs its words, never stretches in a column. */
160
+ const PILL_BASE = {
161
+ display: 'inline-flex', alignItems: 'center', gap: '5px', width: 'auto', minHeight: 'auto', alignSelf: 'flex-start', lineHeight: '1.4',
162
+ };
163
+ const PILL_ICON = { fontSize: '1em' };
164
+ const PILL_WORDS = { width: 'auto', minHeight: 'auto', padding: '0px 0px 0px 0px', margin: '0px 0px 0px 0px' };
165
+ /** The pill's words inherit the pill's type and ink (the contract's inherit-everything text style). */
166
+ const PILL_WORDS_STYLE = 'button-label';
167
+ function buildPill(create, elementStyle, text, icon, key) {
168
+ const pill = create({ key, type: 'Container', elementStyle });
169
+ writeStyles(pill, PILL_BASE);
170
+ const glyph = create({ key: `${key}-icon`, type: 'Icon', props: { iconClass: icon }, elementStyle: 'icon' });
171
+ writeStyles(glyph, PILL_ICON);
172
+ const words = create({ key: `${key}-text`, type: 'Text', props: { content: text, tag: 'span' }, elementStyle: PILL_WORDS_STYLE });
173
+ writeStyles(words, PILL_WORDS);
174
+ pill.children = [glyph, words];
175
+ return pill;
176
+ }
177
+ /** A fresh Badge: Container `badge-<tone>` › Icon + Text. */
178
+ function buildBadge(create, options = {}) {
179
+ const tone = options.tone ?? BadgeTone.Accent;
180
+ const badge = buildPill(create, badgeStyleId(tone), options.text ?? 'New', options.icon ?? BADGE_TONE_ICONS[tone], options.key ?? 'badge');
181
+ badge.label = 'Badge';
182
+ return badge;
183
+ }
184
+ /** A fresh Chip: Container `chip` › Icon + Text (outlined, static). */
185
+ function buildChip(create, options = {}) {
186
+ const chip = buildPill(create, CHIP_STYLE, options.text ?? 'Chip', options.icon ?? CHIP_ICON, options.key ?? 'chip');
187
+ chip.label = 'Chip';
188
+ return chip;
189
+ }
190
+
191
+ /**
192
+ * CARD v2 (composite T23; complex-components board 8, APPROVED 2026-10-20). Not a component: a recipe of
193
+ * ordinary nodes, each part carrying a role, so every part is selectable on the canvas, in the layer tree
194
+ * and the breadcrumb, wears its own element style and is edited on its own (R2). No runtime and no design
195
+ * state — nothing is hidden at rest.
196
+ *
197
+ * Container `card` role card
198
+ * ├─ Container `box` role card.media (optional: Image)
199
+ * │ ├─ Image `media`
200
+ * │ └─ Container `badge-accent` role card.badge (optional) › Icon + Text
201
+ * └─ Container `box` role card.content (locked)
202
+ * ├─ Text `eyebrow` role card.eyebrow (optional)
203
+ * ├─ Heading h3 `h3` role card.title (locked — the card's link when linked)
204
+ * ├─ Text `body` role card.text (optional)
205
+ * └─ Container `box` role card.actions (optional) › ButtonBlock primary + ghost
206
+ *
207
+ * One recipe for every consumer: the builder's palette (its `createConfig`), generation-core's composer
208
+ * (its `createSerializedNode`) and the site build — each passes its own node factory.
209
+ */
210
+ var CardRole;
211
+ (function (CardRole) {
212
+ CardRole["Card"] = "card";
213
+ CardRole["Media"] = "card.media";
214
+ CardRole["Badge"] = "card.badge";
215
+ CardRole["Content"] = "card.content";
216
+ CardRole["Eyebrow"] = "card.eyebrow";
217
+ CardRole["Title"] = "card.title";
218
+ CardRole["Text"] = "card.text";
219
+ CardRole["Actions"] = "card.actions";
220
+ })(CardRole || (CardRole = {}));
221
+ /** The optional parts — Card ▸ Parts, one switch each, in the board's order. Title and Content are locked. */
222
+ var CardPart;
223
+ (function (CardPart) {
224
+ CardPart["Media"] = "media";
225
+ CardPart["Badge"] = "badge";
226
+ CardPart["Eyebrow"] = "eyebrow";
227
+ CardPart["Text"] = "text";
228
+ CardPart["Actions"] = "actions";
229
+ })(CardPart || (CardPart = {}));
230
+ const CARD_PARTS = [CardPart.Media, CardPart.Badge, CardPart.Eyebrow, CardPart.Text, CardPart.Actions];
231
+ const CARD_PART_ROLE = {
232
+ [CardPart.Media]: CardRole.Media,
233
+ [CardPart.Badge]: CardRole.Badge,
234
+ [CardPart.Eyebrow]: CardRole.Eyebrow,
235
+ [CardPart.Text]: CardRole.Text,
236
+ [CardPart.Actions]: CardRole.Actions,
237
+ };
238
+ /** Card ▸ Layout — a preset, never a stored trait: `applyCardLayout` writes the card's layout styles. */
239
+ var CardLayout;
240
+ (function (CardLayout) {
241
+ CardLayout["Top"] = "top";
242
+ CardLayout["Side"] = "side";
243
+ CardLayout["Overlay"] = "overlay";
244
+ CardLayout["Compact"] = "compact";
245
+ })(CardLayout || (CardLayout = {}));
246
+ const CARD_LAYOUTS = [CardLayout.Top, CardLayout.Side, CardLayout.Overlay, CardLayout.Compact];
247
+ /** What Card ▸ Layout shows once the author has changed the layout styles by hand. */
248
+ const CARD_LAYOUT_CUSTOM = 'custom';
249
+ const CARD_WORDS = {
250
+ eyebrow: 'Category',
251
+ title: 'Card title',
252
+ text: 'One or two sentences about this item.',
253
+ primary: 'Learn more',
254
+ secondary: 'Save',
255
+ badge: 'New',
256
+ };
257
+ /** The breakpoint whose layer stacks an "Image side" card (Breakpoint.Mobile, ≤767px). */
258
+ const CARD_STACK_BREAKPOINT = 'mobile';
259
+ var Type;
260
+ (function (Type) {
261
+ Type["Container"] = "Container";
262
+ Type["Image"] = "Image";
263
+ Type["Heading"] = "Heading";
264
+ Type["Text"] = "Text";
265
+ Type["Icon"] = "Icon";
266
+ Type["ButtonBlock"] = "ButtonBlock";
267
+ })(Type || (Type = {}));
268
+ var Style;
269
+ (function (Style) {
270
+ Style["Card"] = "card";
271
+ Style["Box"] = "box";
272
+ Style["Media"] = "media";
273
+ Style["Eyebrow"] = "eyebrow";
274
+ Style["Title"] = "h3";
275
+ Style["Body"] = "body";
276
+ Style["Label"] = "button-label";
277
+ Style["Primary"] = "button-primary";
278
+ Style["Ghost"] = "button-ghost";
279
+ Style["Icon"] = "icon";
280
+ Style["Scrim"] = "media-scrim";
281
+ })(Style || (Style = {}));
282
+ var Scheme;
283
+ (function (Scheme) {
284
+ Scheme["Default"] = "default";
285
+ Scheme["Inverse"] = "inverse";
286
+ })(Scheme || (Scheme = {}));
287
+ const COLOR_SCHEME = 'colorScheme';
288
+ const LINK_TYPE = 'linkType';
289
+ const SAVE_ICON = 'ph ph-bookmark-simple';
290
+ /** A value an element style writes — read from the contract, never retyped. */
291
+ const styleValue = (style, trait) => String(elementStyleById(style)?.base[trait] ?? '');
292
+ const CARD_PAD = styleValue(Style.Card, 'padding');
293
+ const IMAGE_RADIUS = styleValue(Style.Media, 'borderRadius');
294
+ const TITLE_SIZE = styleValue(Style.Title, 'fontSize');
295
+ const LEAD_SIZE = 'var(--iox-size-lead)';
296
+ const ZERO_BOX = '0px 0px 0px 0px';
297
+ /** What every card part holds in every layout — the recipe's box. */
298
+ const CARD_BASE = {
299
+ [CardRole.Card]: { display: 'flex', gap: '14px', width: '100%', height: '100%', minWidth: '0px', overflow: 'hidden', position: 'relative' },
300
+ [CardRole.Media]: { display: 'block', width: '100%', minHeight: 'auto', flexGrow: '0', flexShrink: '0' },
301
+ [CardRole.Content]: { display: 'flex', flexDirection: 'column', gap: '8px', width: '100%', minWidth: '0px', minHeight: 'auto' },
302
+ [CardRole.Eyebrow]: { margin: ZERO_BOX, padding: ZERO_BOX, minHeight: 'auto' },
303
+ [CardRole.Title]: { margin: ZERO_BOX },
304
+ [CardRole.Text]: { margin: ZERO_BOX, padding: ZERO_BOX, minHeight: 'auto' },
305
+ // Buttons pinned to the bottom (board 8 Q: equal heights + buttons pinned): the content column grows,
306
+ // and the row's auto top margin takes the slack.
307
+ [CardRole.Actions]: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '8px', margin: 'auto 0px 0px 0px', padding: '6px 0px 0px 0px', width: '100%', minHeight: 'auto' },
308
+ };
309
+ const IMAGE_BASE = { display: 'block', width: '100%', maxWidth: '100%' };
310
+ /** A button's label and a pill's words: inherit the type and ink of what holds them. */
311
+ const LABEL_BASE = { width: 'auto', minHeight: 'auto', padding: ZERO_BOX, margin: ZERO_BOX };
312
+ const INLINE_ICON = { fontSize: '1em' };
313
+ const IN_FLOW = { position: 'relative', top: 'auto', right: 'auto', bottom: 'auto', left: 'auto' };
314
+ const GROWING_CONTENT = { position: 'static', flexGrow: '1', flexShrink: '1', flexBasis: '0%', padding: ZERO_BOX };
315
+ const LAYOUTS = {
316
+ [CardLayout.Top]: {
317
+ card: { flexDirection: 'column', alignItems: 'stretch', justifyContent: 'flex-start', minHeight: 'auto', padding: CARD_PAD },
318
+ media: { ...IN_FLOW, flexBasis: 'auto' },
319
+ image: { aspectRatio: '4 / 3', height: 'auto', minHeight: 'auto', borderRadius: IMAGE_RADIUS },
320
+ content: GROWING_CONTENT, contentStyle: Style.Box, contentScheme: Scheme.Default,
321
+ title: { fontSize: TITLE_SIZE },
322
+ badgeOnMedia: true,
323
+ },
324
+ [CardLayout.Side]: {
325
+ card: { flexDirection: 'row', alignItems: 'stretch', justifyContent: 'flex-start', minHeight: 'auto', padding: CARD_PAD },
326
+ media: { ...IN_FLOW, flexBasis: '42%' },
327
+ image: { aspectRatio: 'auto', height: '100%', minHeight: '180px', borderRadius: IMAGE_RADIUS },
328
+ content: GROWING_CONTENT, contentStyle: Style.Box, contentScheme: Scheme.Default,
329
+ title: { fontSize: TITLE_SIZE },
330
+ badgeOnMedia: true,
331
+ stack: {
332
+ card: { flexDirection: 'column' },
333
+ media: { flexBasis: 'auto' },
334
+ image: { aspectRatio: '4 / 3', height: 'auto', minHeight: 'auto' },
335
+ },
336
+ },
337
+ [CardLayout.Overlay]: {
338
+ card: { flexDirection: 'column', alignItems: 'stretch', justifyContent: 'flex-end', minHeight: '340px', padding: ZERO_BOX },
339
+ media: { position: 'absolute', top: '0px', right: '0px', bottom: '0px', left: '0px', flexBasis: 'auto' },
340
+ image: { aspectRatio: 'auto', height: '100%', minHeight: 'auto', borderRadius: '0px' },
341
+ content: { position: 'relative', flexGrow: '0', flexShrink: '0', flexBasis: 'auto', padding: `72px ${CARD_PAD} ${CARD_PAD} ${CARD_PAD}` },
342
+ contentStyle: Style.Scrim, contentScheme: Scheme.Inverse,
343
+ title: { fontSize: TITLE_SIZE },
344
+ badgeOnMedia: false,
345
+ },
346
+ [CardLayout.Compact]: {
347
+ card: { flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', minHeight: 'auto', padding: CARD_PAD },
348
+ media: { ...IN_FLOW, flexBasis: '72px' },
349
+ image: { aspectRatio: '1 / 1', height: 'auto', minHeight: 'auto', borderRadius: IMAGE_RADIUS },
350
+ content: GROWING_CONTENT, contentStyle: Style.Box, contentScheme: Scheme.Default,
351
+ title: { fontSize: LEAD_SIZE },
352
+ badgeOnMedia: false,
353
+ partsOff: [CardPart.Text, CardPart.Actions],
354
+ },
355
+ };
356
+ const STACK_KEYS = {
357
+ card: Object.keys(LAYOUTS[CardLayout.Side].stack.card),
358
+ media: Object.keys(LAYOUTS[CardLayout.Side].stack.media),
359
+ image: Object.keys(LAYOUTS[CardLayout.Side].stack.image),
360
+ };
361
+ function make(create, key, type, role, elementStyle, props = {}, label) {
362
+ const node = create({ key, type, props, elementStyle });
363
+ if (role)
364
+ node.role = role;
365
+ if (label)
366
+ node.label = label;
367
+ writeStyles(node, role ? CARD_BASE[role] ?? {} : {});
368
+ return node;
369
+ }
370
+ function buttonLabel(create, key, words) {
371
+ const label = make(create, key, Type.Text, null, Style.Label, { content: words, tag: 'span' });
372
+ writeStyles(label, LABEL_BASE);
373
+ return label;
374
+ }
375
+ /** Each optional part (and the locked title) as the recipe builds it. */
376
+ const PART_BUILDERS = {
377
+ [CardPart.Media]: (create) => {
378
+ const media = make(create, 'media', Type.Container, CardRole.Media, Style.Box, {}, 'Media');
379
+ const image = make(create, 'image', Type.Image, null, Style.Media, { src: '', alt: '' });
380
+ writeStyles(image, IMAGE_BASE);
381
+ media.children = [image];
382
+ return media;
383
+ },
384
+ [CardPart.Badge]: (create, words, tone) => {
385
+ const badge = buildBadge(create, { tone, text: words.badge, key: 'badge' });
386
+ badge.role = CardRole.Badge;
387
+ badge.label = 'Badge';
388
+ return badge;
389
+ },
390
+ [CardPart.Eyebrow]: (create, words) => make(create, 'eyebrow', Type.Text, CardRole.Eyebrow, Style.Eyebrow, { content: words.eyebrow, tag: 'p' }, 'Eyebrow'),
391
+ [CardPart.Text]: (create, words) => make(create, 'text', Type.Text, CardRole.Text, Style.Body, { content: words.text, tag: 'p' }, 'Text'),
392
+ [CardPart.Actions]: (create, words) => {
393
+ const actions = make(create, 'actions', Type.Container, CardRole.Actions, Style.Box, {}, 'Buttons');
394
+ const primary = make(create, 'primary', Type.ButtonBlock, null, Style.Primary);
395
+ primary.children = [buttonLabel(create, 'primary-label', words.primary)];
396
+ const ghost = make(create, 'secondary', Type.ButtonBlock, null, Style.Ghost);
397
+ const icon = make(create, 'secondary-icon', Type.Icon, null, Style.Icon, { iconClass: SAVE_ICON });
398
+ writeStyles(icon, INLINE_ICON);
399
+ ghost.children = [icon, buttonLabel(create, 'secondary-label', words.secondary)];
400
+ actions.children = [primary, ghost];
401
+ return actions;
402
+ },
403
+ };
404
+ function buildTitle(create, words) {
405
+ return make(create, 'title', Type.Heading, CardRole.Title, Style.Title, { content: words.title, level: 'h3', [LINK_TYPE]: LinkType.None }, 'Title');
406
+ }
407
+ /** A fresh Card v2: every part a node with its role and element style, laid out by `options.layout`. */
408
+ function buildCardRecipe(create, options = {}) {
409
+ const words = { ...CARD_WORDS, ...(options.words ?? {}) };
410
+ const tone = options.badgeTone ?? BadgeTone.Accent;
411
+ const on = (part) => options.parts?.[part] !== false;
412
+ const card = make(create, 'card', Type.Container, CardRole.Card, Style.Card, {}, 'Card');
413
+ const content = make(create, 'content', Type.Container, CardRole.Content, Style.Box, {}, 'Content');
414
+ content.children = [
415
+ ...(on(CardPart.Eyebrow) ? [PART_BUILDERS[CardPart.Eyebrow](create, words, tone)] : []),
416
+ buildTitle(create, words),
417
+ ...(on(CardPart.Text) ? [PART_BUILDERS[CardPart.Text](create, words, tone)] : []),
418
+ ...(on(CardPart.Actions) ? [PART_BUILDERS[CardPart.Actions](create, words, tone)] : []),
419
+ ];
420
+ card.children = [...(on(CardPart.Media) ? [PART_BUILDERS[CardPart.Media](create, words, tone)] : []), content];
421
+ if (on(CardPart.Badge))
422
+ placeBadge(card, PART_BUILDERS[CardPart.Badge](create, words, tone), true);
423
+ applyCardLayout(card, options.layout ?? CardLayout.Top, options);
424
+ return card;
425
+ }
426
+ /** True when `node` is a Card v2 root. */
427
+ function isCardRoot(node) {
428
+ return node?.role === CardRole.Card;
429
+ }
430
+ /** The Card a part belongs to: the nearest ancestor (or the node itself) playing `card`. `path` = the
431
+ * node's ancestors, root first. */
432
+ function cardRootOf(node, path) {
433
+ if (isCardRoot(node))
434
+ return node;
435
+ for (let i = path.length - 1; i >= 0; i--)
436
+ if (isCardRoot(path[i]))
437
+ return path[i];
438
+ return null;
439
+ }
440
+ /** Card ▸ Parts: which optional parts the card holds now. */
441
+ function cardPartsOf(card) {
442
+ return Object.fromEntries(CARD_PARTS.map(part => [part, !!findByRole(card, CARD_PART_ROLE[part])]));
443
+ }
444
+ const imageOf = (media) => media?.children?.find(c => c.type === Type.Image);
445
+ /** Put the badge where the layout says: the Media's top corner, or the head of the content column
446
+ * (Image behind text, Compact, or no Media at all). */
447
+ function placeBadge(card, badge, onMedia) {
448
+ const media = findByRole(card, CardRole.Media);
449
+ const content = findByRole(card, CardRole.Content);
450
+ removeFromComposite(card, badge);
451
+ if (onMedia && media)
452
+ media.children = [...(media.children ?? []), badge];
453
+ else if (content)
454
+ content.children = [badge, ...(content.children ?? [])];
455
+ }
456
+ function writeScheme(node, scheme) {
457
+ writeProp(node, COLOR_SCHEME, scheme);
458
+ }
459
+ /** Write one role's layout values from a spec (base + layers). */
460
+ function applyPartLayout(card, spec, part, baseline) {
461
+ switch (part.role) {
462
+ case CardRole.Card:
463
+ writeStyles(part, { ...CARD_BASE[CardRole.Card], ...spec.card });
464
+ setStack(part, spec.stack?.card, STACK_KEYS.card);
465
+ return;
466
+ case CardRole.Media: {
467
+ writeStyles(part, { ...CARD_BASE[CardRole.Media], ...spec.media });
468
+ setStack(part, spec.stack?.media, STACK_KEYS.media);
469
+ const image = imageOf(part);
470
+ if (image) {
471
+ writeStyles(image, { ...IMAGE_BASE, ...spec.image });
472
+ setStack(image, spec.stack?.image, STACK_KEYS.image);
473
+ }
474
+ return;
475
+ }
476
+ case CardRole.Content: {
477
+ const current = part.elementStyle;
478
+ if (current !== spec.contentStyle) {
479
+ switchElementStyle(part, current, spec.contentStyle, baseline(part.type));
480
+ part.elementStyle = spec.contentStyle;
481
+ }
482
+ writeStyles(part, { ...CARD_BASE[CardRole.Content], ...spec.content });
483
+ writeScheme(part, spec.contentScheme);
484
+ return;
485
+ }
486
+ case CardRole.Title:
487
+ writeStyles(part, spec.title);
488
+ return;
489
+ default:
490
+ return;
491
+ }
492
+ }
493
+ function setStack(node, values, keys) {
494
+ if (values)
495
+ writeLayerStyles(node, CARD_STACK_BREAKPOINT, values);
496
+ else
497
+ clearLayerStyles(node, CARD_STACK_BREAKPOINT, keys);
498
+ }
499
+ /**
500
+ * Card ▸ Layout. Rewrites the card's LAYOUT styles for `layout` — the card's direction and box, the
501
+ * Media's placement, the Image's aspect, the content block (Image behind text: the `media-scrim` style +
502
+ * the Inverse scheme), the title size (Compact) and, for Image side, a Mobile layer that stacks it — and
503
+ * moves the badge to where the layout puts it. Nothing else on any part. One undo step for the caller.
504
+ */
505
+ function applyCardLayout(card, layout, options = {}) {
506
+ const spec = LAYOUTS[layout];
507
+ const baseline = options.baselineStyles ?? (() => ({}));
508
+ const removed = [];
509
+ for (const part of spec.partsOff ?? [])
510
+ if (setCardPart(card, part, false))
511
+ removed.push(part);
512
+ applyPartLayout(card, spec, card, baseline);
513
+ for (const role of [CardRole.Media, CardRole.Content, CardRole.Title]) {
514
+ const part = findByRole(card, role);
515
+ if (part)
516
+ applyPartLayout(card, spec, part, baseline);
517
+ }
518
+ const badge = findByRole(card, CardRole.Badge);
519
+ if (badge)
520
+ placeBadge(card, badge, spec.badgeOnMedia);
521
+ return { layout, removed };
522
+ }
523
+ const same = (a, b) => String(a ?? '').trim() === String(b ?? '').trim();
524
+ const holds = (node, values) => Object.entries(values).every(([k, v]) => same(readStyle(node, k), v));
525
+ const holdsLayer = (node, values, keys) => values ? Object.entries(values).every(([k, v]) => same(readLayerStyle(node, CARD_STACK_BREAKPOINT, k), v))
526
+ : keys.every(k => readLayerStyle(node, CARD_STACK_BREAKPOINT, k) === undefined);
527
+ function matches(card, spec) {
528
+ if (!holds(card, spec.card) || !holdsLayer(card, spec.stack?.card, STACK_KEYS.card))
529
+ return false;
530
+ const media = findByRole(card, CardRole.Media);
531
+ if (media) {
532
+ if (!holds(media, spec.media) || !holdsLayer(media, spec.stack?.media, STACK_KEYS.media))
533
+ return false;
534
+ const image = imageOf(media);
535
+ if (image && (!holds(image, spec.image) || !holdsLayer(image, spec.stack?.image, STACK_KEYS.image)))
536
+ return false;
537
+ }
538
+ const content = findByRole(card, CardRole.Content);
539
+ if (content) {
540
+ if (content.elementStyle !== spec.contentStyle || !holds(content, spec.content))
541
+ return false;
542
+ if (!same(readProp(content, COLOR_SCHEME) ?? Scheme.Default, spec.contentScheme))
543
+ return false;
544
+ }
545
+ const title = findByRole(card, CardRole.Title);
546
+ if (title && !holds(title, spec.title))
547
+ return false;
548
+ const badge = findByRole(card, CardRole.Badge);
549
+ if (badge && media) {
550
+ const onMedia = parentInComposite(card, badge) === media;
551
+ if (onMedia !== spec.badgeOnMedia)
552
+ return false;
553
+ }
554
+ return true;
555
+ }
556
+ /** The layout the card's styles hold now, or `custom` once they were changed by hand (Card ▸ Layout). */
557
+ function detectCardLayout(card) {
558
+ return CARD_LAYOUTS.find(layout => matches(card, LAYOUTS[layout])) ?? CARD_LAYOUT_CUSTOM;
559
+ }
560
+ /**
561
+ * Card ▸ Parts. Off removes the part (the badge first moves out of a removed Media); on stamps it back
562
+ * from the recipe into its role's slot, wearing its element style and laid out like the rest of the card
563
+ * (the card's current layout; Image top when it is Custom). Returns the node touched, or null when the
564
+ * card already was that way. Title and Content are not parts: they are locked (structure guard).
565
+ */
566
+ function setCardPart(card, part, on, create, options = {}) {
567
+ const role = CARD_PART_ROLE[part];
568
+ const existing = findByRole(card, role);
569
+ if (!on) {
570
+ if (!existing)
571
+ return null;
572
+ if (part === CardPart.Media) {
573
+ const badge = findByRole(existing, CardRole.Badge);
574
+ if (badge)
575
+ placeBadge(card, badge, false);
576
+ }
577
+ removeFromComposite(card, existing);
578
+ return existing;
579
+ }
580
+ if (existing || !create)
581
+ return null;
582
+ const layout = detectCardLayout(card);
583
+ const spec = LAYOUTS[layout === CARD_LAYOUT_CUSTOM ? CardLayout.Top : layout];
584
+ const words = { ...CARD_WORDS, ...(options.words ?? {}) };
585
+ const node = PART_BUILDERS[part](create, words, options.badgeTone ?? BadgeTone.Accent);
586
+ const content = findByRole(card, CardRole.Content);
587
+ switch (part) {
588
+ case CardPart.Media: {
589
+ card.children = [node, ...(card.children ?? [])];
590
+ applyPartLayout(card, spec, node, () => ({}));
591
+ const badge = findByRole(card, CardRole.Badge);
592
+ if (badge)
593
+ placeBadge(card, badge, spec.badgeOnMedia);
594
+ break;
595
+ }
596
+ case CardPart.Badge:
597
+ placeBadge(card, node, spec.badgeOnMedia);
598
+ break;
599
+ default:
600
+ if (content)
601
+ insertInContent(content, part, node);
602
+ break;
603
+ }
604
+ return node;
605
+ }
606
+ /** The content column's order: [badge], eyebrow, title, text, buttons. */
607
+ const CONTENT_ORDER = [CardRole.Badge, CardRole.Eyebrow, CardRole.Title, CardRole.Text, CardRole.Actions];
608
+ function insertInContent(content, part, node) {
609
+ const rank = (n) => { const i = CONTENT_ORDER.indexOf(n.role ?? ''); return i < 0 ? CONTENT_ORDER.length : i; };
610
+ const children = [...(content.children ?? [])];
611
+ const own = rank(node);
612
+ const at = children.findIndex(c => rank(c) > own);
613
+ children.splice(at < 0 ? children.length : at, 0, node);
614
+ content.children = children;
615
+ }
616
+ // ── Link the whole card ──────────────────────────────────────────────────────────────────────
617
+ /** The card's title — what "Link the whole card" makes the link (its words are the link's name). */
618
+ function cardTitleOf(card) {
619
+ return findByRole(card, CardRole.Title);
620
+ }
621
+ /** True when the card links as a whole: its title carries a link (the stretched link renders from it). */
622
+ function isCardLinked(card) {
623
+ const title = cardTitleOf(card);
624
+ if (!title)
625
+ return false;
626
+ const type = readProp(title, LINK_TYPE);
627
+ return type === LinkType.Page || type === LinkType.External;
628
+ }
629
+ /**
630
+ * Card ▸ Link ▸ "Link the whole card". On: the title links (to a website page, unless it already pointed
631
+ * at a URL — the target it held is kept); off: the title links nowhere (its target is kept for the next
632
+ * time). The stretched link itself is the shared CSS (iox-design.css), keyed on the title's anchor.
633
+ */
634
+ function setCardLinked(card, on) {
635
+ const title = cardTitleOf(card);
636
+ if (!title)
637
+ return false;
638
+ const current = readProp(title, LINK_TYPE);
639
+ const next = on ? (current === LinkType.External ? LinkType.External : LinkType.Page) : LinkType.None;
640
+ return writeProp(title, LINK_TYPE, next);
641
+ }
642
+
643
+ const COMPOSITE_ROLE_SPECS = {
644
+ [CardRole.Card]: { parent: null },
645
+ [CardRole.Media]: { parent: CardRole.Card, part: CardPart.Media },
646
+ [CardRole.Badge]: { parent: null, part: CardPart.Badge },
647
+ [CardRole.Content]: { parent: CardRole.Card, locked: true },
648
+ [CardRole.Eyebrow]: { parent: CardRole.Content, part: CardPart.Eyebrow },
649
+ [CardRole.Title]: { parent: CardRole.Content, locked: true },
650
+ [CardRole.Text]: { parent: CardRole.Content, part: CardPart.Text },
651
+ [CardRole.Actions]: { parent: CardRole.Content, part: CardPart.Actions },
652
+ };
653
+ var StructureEdit;
654
+ (function (StructureEdit) {
655
+ StructureEdit["Delete"] = "delete";
656
+ /** A drop, paste or wrap that would put the node somewhere new. */
657
+ StructureEdit["Move"] = "move";
658
+ })(StructureEdit || (StructureEdit = {}));
659
+ var StructureRefusal;
660
+ (function (StructureRefusal) {
661
+ /** A locked part cannot be deleted (the card's Content and Title). */
662
+ StructureRefusal["LockedPart"] = "locked-part";
663
+ /** A locked part can only live directly inside its own parent role. */
664
+ StructureRefusal["LockedPlacement"] = "locked-placement";
665
+ })(StructureRefusal || (StructureRefusal = {}));
666
+ /**
667
+ * Why the structure guard refuses `edit` on `node`, or null when it is allowed. `ancestors` (Move only) =
668
+ * the new parent chain, root first, the direct parent last.
669
+ */
670
+ function structureRefusal(node, edit, ancestors = []) {
671
+ const spec = node?.role ? COMPOSITE_ROLE_SPECS[node.role] : undefined;
672
+ if (!spec?.locked)
673
+ return null;
674
+ if (edit === StructureEdit.Delete)
675
+ return StructureRefusal.LockedPart;
676
+ const parent = ancestors[ancestors.length - 1];
677
+ return parent?.role === spec.parent ? null : StructureRefusal.LockedPlacement;
678
+ }
679
+ /** True when `node` is a locked part (shown with a lock and no delete in the layer tree). Deleting a whole
680
+ * composite is always allowed — only its locked parts are refused. */
681
+ function isLockedPart(node) {
682
+ return !!(node?.role && COMPOSITE_ROLE_SPECS[node.role]?.locked);
683
+ }
684
+
685
+ /**
686
+ * @vectoriox/iox-ui/composite-core — composites built from the builder's own parts (composite-components
687
+ * plan §3a). FRAMEWORK-FREE: the builder, the SSR engine and iox-cms (generation-core's composer, the site
688
+ * build) read the same recipes, roles and structure rules. Card v2 + Badge / Chip (T23 / T25) are the
689
+ * first; Accordion and Tabs join with their own phases.
690
+ * ⚠️ Consumers wire the usual trio — ng-package raw-ts assets, jest moduleNameMapper, tsconfig paths.
691
+ */
692
+
693
+ /**
694
+ * Generated bundle index. Do not edit.
695
+ */
696
+
697
+ export { BADGE_TONES, BADGE_TONE_ICONS, BadgeTone, CARD_LAYOUTS, CARD_LAYOUT_CUSTOM, CARD_PARTS, CARD_PART_ROLE, CARD_STACK_BREAKPOINT, CARD_WORDS, CHIP_ICON, CHIP_STYLE, COMPOSITE_ROLE_SPECS, CardLayout, CardPart, CardRole, StructureEdit, StructureRefusal, applyCardLayout, badgeStyleId, badgeToneOf, buildBadge, buildCardRecipe, buildChip, cardPartsOf, cardRootOf, cardTitleOf, clearLayerStyles, detectCardLayout, findByRole, isCardLinked, isCardRoot, isLockedPart, isPillNode, parentInComposite, readLayerStyle, readProp, readStyle, removeFromComposite, setBadgeTone, setCardLinked, setCardPart, structureRefusal, walkComposite, writeLayerStyles, writeProp, writeStyles };
698
+ //# sourceMappingURL=vectoriox-iox-ui-composite-core.mjs.map