@vectoriox/iox-ui 5.36.0 → 5.38.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 (43) hide show
  1. package/actions-core/index.d.ts +17 -3
  2. package/composite-core/index.d.ts +463 -31
  3. package/design-core/index.d.ts +130 -3
  4. package/fesm2022/vectoriox-iox-ui-actions-core.mjs +33 -3
  5. package/fesm2022/vectoriox-iox-ui-actions-core.mjs.map +1 -1
  6. package/fesm2022/vectoriox-iox-ui-composite-core.mjs +1062 -121
  7. package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -1
  8. package/fesm2022/vectoriox-iox-ui-design-core.mjs +172 -3
  9. package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
  10. package/fesm2022/vectoriox-iox-ui-generation-core.mjs +410 -10
  11. package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
  12. package/fesm2022/vectoriox-iox-ui-media-core.mjs +571 -0
  13. package/fesm2022/vectoriox-iox-ui-media-core.mjs.map +1 -0
  14. package/fesm2022/vectoriox-iox-ui.mjs +482 -120
  15. package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
  16. package/generation-core/index.d.ts +167 -4
  17. package/index.d.ts +209 -25
  18. package/media-core/index.d.ts +277 -0
  19. package/media-core/package.json +3 -0
  20. package/package.json +5 -1
  21. package/src/actions-core/action.model.ts +4 -2
  22. package/src/actions-core/element-actions.ts +37 -2
  23. package/src/composite-core/accordion-runtime.ts +117 -28
  24. package/src/composite-core/accordion.ts +89 -45
  25. package/src/composite-core/composite-kinds.ts +110 -0
  26. package/src/composite-core/composite-node.ts +15 -0
  27. package/src/composite-core/disclosure.ts +125 -0
  28. package/src/composite-core/lightbox.ts +192 -0
  29. package/src/composite-core/public-api.ts +7 -1
  30. package/src/composite-core/slider-runtime.ts +180 -0
  31. package/src/composite-core/slider.ts +378 -0
  32. package/src/composite-core/structure.ts +25 -5
  33. package/src/design-core/element-styles.ts +27 -0
  34. package/src/design-core/flavor-spec.generated.ts +146 -2
  35. package/src/generation-core/baseline-roles.ts +2 -0
  36. package/src/generation-core/compose-site.ts +80 -4
  37. package/src/generation-core/node-baselines.generated.ts +148 -4
  38. package/src/generation-core/patterns.ts +17 -0
  39. package/src/generation-core/site-spec.generated.ts +164 -0
  40. package/src/media-core/public-api.ts +9 -0
  41. package/src/media-core/video-runtime.ts +239 -0
  42. package/src/media-core/video.ts +443 -0
  43. package/styles/iox-design.css +96 -0
@@ -1,5 +1,24 @@
1
1
  import { readOwnedTrait, writeOwnedTrait, elementStyleById, switchElementStyle, LinkType, setColorScheme, COLOR_SCHEME_TRAIT } from '@vectoriox/iox-ui/design-core';
2
2
 
3
+ /** What a recipe asks its consumer to create: a NEW node of `type`, the way that consumer creates one
4
+ * (the builder's `createConfig`, generation-core's `createSerializedNode`), with these props and this
5
+ * element style. `key` is stable per recipe node — a consumer that needs deterministic ids builds them
6
+ * from it (generation-core: `<root id>-<key>`). */
7
+ /** `elementStyle: NO_ELEMENT_STYLE` asks for a node that wears NO element style (undefined / null = the type's default,
8
+ * as both node factories have always read it): a migrated slider's bare chevrons, a lightbox's slide (CC3). */
9
+ const NO_ELEMENT_STYLE = '';
10
+ /** A recipe part: a NEW node (`create`) playing `role` under `label`, with the layout `base` styles its recipe
11
+ * gives it — its LOOK is its element style. Shared by every recipe (Accordion, Disclosure). */
12
+ function makeCompositePart(create, spec, role, label, base) {
13
+ const node = create(spec);
14
+ if (role)
15
+ node.role = role;
16
+ if (label)
17
+ node.label = label;
18
+ if (base)
19
+ writeStyles(node, base);
20
+ return node;
21
+ }
3
22
  const usesTraits = (node) => Array.isArray(node.traits);
4
23
  /** A content trait's value — `traits[].default` on a builder node, `props[name]` on a saved one. */
5
24
  function readProp(node, name) {
@@ -105,6 +124,101 @@ function removeFromComposite(root, node) {
105
124
  return true;
106
125
  }
107
126
 
127
+ /**
128
+ * The open/close composites' kinds and roles (CC2 Accordion, CC2.1 Disclosure — composite plan T9 / T21). ONE
129
+ * family: a Disclosure is a one-item accordion whose ROOT is its item, so both run the same state machine, the
130
+ * same attribute planners and the same runtime (`accordion-runtime.ts`). Every consumer that treats "an item",
131
+ * "a header" or "a panel" alike asks these predicates, never a role literal.
132
+ */
133
+ var CompositeKind;
134
+ (function (CompositeKind) {
135
+ CompositeKind["Accordion"] = "accordion";
136
+ CompositeKind["Disclosure"] = "disclosure";
137
+ })(CompositeKind || (CompositeKind = {}));
138
+ var AccordionRole;
139
+ (function (AccordionRole) {
140
+ AccordionRole["Accordion"] = "accordion";
141
+ AccordionRole["Item"] = "accordion.item";
142
+ AccordionRole["Trigger"] = "accordion.trigger";
143
+ AccordionRole["Icon"] = "accordion.icon";
144
+ AccordionRole["Panel"] = "accordion.panel";
145
+ })(AccordionRole || (AccordionRole = {}));
146
+ /**
147
+ * DISCLOSURE ("Read more") — one trigger, one panel. The root IS its item (it carries the item key and
148
+ * `data-iox-on`); the trigger holds two words, the one shown while closed (`more`) and the one shown while open
149
+ * (`less`), swapped by the Item open state — never by script.
150
+ */
151
+ var DisclosureRole;
152
+ (function (DisclosureRole) {
153
+ DisclosureRole["Disclosure"] = "disclosure";
154
+ DisclosureRole["Trigger"] = "disclosure.trigger";
155
+ DisclosureRole["More"] = "disclosure.more";
156
+ DisclosureRole["Less"] = "disclosure.less";
157
+ DisclosureRole["Icon"] = "disclosure.icon";
158
+ DisclosureRole["Panel"] = "disclosure.panel";
159
+ })(DisclosureRole || (DisclosureRole = {}));
160
+ const COMPOSITE_KIND_ROLES = {
161
+ [CompositeKind.Accordion]: { root: AccordionRole.Accordion, item: AccordionRole.Item, trigger: AccordionRole.Trigger, panel: AccordionRole.Panel, icon: AccordionRole.Icon },
162
+ [CompositeKind.Disclosure]: { root: DisclosureRole.Disclosure, item: DisclosureRole.Disclosure, trigger: DisclosureRole.Trigger, panel: DisclosureRole.Panel, icon: DisclosureRole.Icon },
163
+ };
164
+ const KINDS = Object.values(CompositeKind);
165
+ const rolesOf = (pick) => new Set(KINDS.map(k => COMPOSITE_KIND_ROLES[k][pick]));
166
+ const ROOT_ROLES = rolesOf('root');
167
+ const ITEM_ROLES = rolesOf('item');
168
+ const TRIGGER_ROLES = rolesOf('trigger');
169
+ const PANEL_ROLES = rolesOf('panel');
170
+ /** The kind a role belongs to (`disclosure.panel` → Disclosure), or null for any other role. */
171
+ function compositeKindOfRole(role) {
172
+ const head = role?.split('.')[0];
173
+ return KINDS.includes(head ?? '') ? head : null;
174
+ }
175
+ /** A root that runs the open/close runtime (an Accordion or a Disclosure). */
176
+ function isCompositeRoot(node) {
177
+ return !!node?.role && ROOT_ROLES.has(node.role);
178
+ }
179
+ /** An ITEM — an accordion's item, or a disclosure (its own item). */
180
+ function isCompositeItem(node) {
181
+ return !!node?.role && ITEM_ROLES.has(node.role);
182
+ }
183
+ /** A trigger — rendered as a `<button>` that opens its item, never a link. */
184
+ function isCompositeTrigger(node) {
185
+ return !!node?.role && TRIGGER_ROLES.has(node.role);
186
+ }
187
+ /** A panel — rendered inside the track + region the one hiding rule collapses. */
188
+ function isCompositePanel(node) {
189
+ return !!node?.role && PANEL_ROLES.has(node.role);
190
+ }
191
+ function isDisclosureRoot(node) {
192
+ return node?.role === DisclosureRole.Disclosure;
193
+ }
194
+ /** True for any part of a disclosure (its root included). */
195
+ function isDisclosurePart(node) {
196
+ return compositeKindOfRole(node?.role) === CompositeKind.Disclosure;
197
+ }
198
+ /** The composite root a node belongs to: the nearest ancestor (or the node itself) that is one. `path` = the
199
+ * node's ancestors, root first. */
200
+ function compositeRootOf(node, path) {
201
+ if (isCompositeRoot(node))
202
+ return node;
203
+ for (let i = path.length - 1; i >= 0; i--)
204
+ if (isCompositeRoot(path[i]))
205
+ return path[i];
206
+ return null;
207
+ }
208
+ /** The item a node sits in — the nearest item of either kind (itself when it is one) — or null. An accordion
209
+ * root ends the walk (its items are below it). */
210
+ function compositeItemOf(node, path) {
211
+ if (isCompositeItem(node))
212
+ return node;
213
+ for (let i = path.length - 1; i >= 0; i--) {
214
+ if (isCompositeItem(path[i]))
215
+ return path[i];
216
+ if (isCompositeRoot(path[i]))
217
+ return null;
218
+ }
219
+ return null;
220
+ }
221
+
108
222
  /**
109
223
  * ACCORDION (CC2 — composite K1; complex-components boards 3, 4 and 9, APPROVED 2026-10-20). Not a
110
224
  * component: a recipe of ordinary nodes, each part carrying a role, so every part is selectable on the
@@ -123,14 +237,6 @@ function removeFromComposite(root, node) {
123
237
  * (`nextAccordionState`), the ARIA and DOM state one planner (`accordionItemState` and the attribute
124
238
  * functions), the keyboard one key map — read by the builder canvas, both engine paths and the composer.
125
239
  */
126
- var AccordionRole;
127
- (function (AccordionRole) {
128
- AccordionRole["Accordion"] = "accordion";
129
- AccordionRole["Item"] = "accordion.item";
130
- AccordionRole["Trigger"] = "accordion.trigger";
131
- AccordionRole["Icon"] = "accordion.icon";
132
- AccordionRole["Panel"] = "accordion.panel";
133
- })(AccordionRole || (AccordionRole = {}));
134
240
  /** The behaviour options a composite root stores as ordinary TRAITS (composite plan §3). */
135
241
  var CompositeBehaviourTrait;
136
242
  (function (CompositeBehaviourTrait) {
@@ -172,7 +278,7 @@ const ACCORDION_WORDS = [
172
278
  { question: 'Write the third question', answer: 'Answer it plainly, in two or three short sentences.' },
173
279
  ];
174
280
  const ACCORDION_NEW_ITEM_WORDS = { question: 'Write a new question', answer: 'Answer it plainly, in two or three short sentences.' };
175
- var Type$1;
281
+ var Type$3;
176
282
  (function (Type) {
177
283
  Type["Container"] = "Container";
178
284
  Type["LinkedContainer"] = "LinkedContainer";
@@ -180,8 +286,8 @@ var Type$1;
180
286
  Type["Text"] = "Text";
181
287
  Type["Icon"] = "Icon";
182
288
  Type["Repeater"] = "Repeater";
183
- })(Type$1 || (Type$1 = {}));
184
- var Style$1;
289
+ })(Type$3 || (Type$3 = {}));
290
+ var Style$3;
185
291
  (function (Style) {
186
292
  Style["Root"] = "box";
187
293
  Style["Item"] = "accordion-item";
@@ -190,14 +296,14 @@ var Style$1;
190
296
  Style["Icon"] = "icon";
191
297
  Style["Panel"] = "accordion-panel";
192
298
  Style["Answer"] = "body";
193
- })(Style$1 || (Style$1 = {}));
194
- const ZERO_BOX$1 = '0px 0px 0px 0px';
299
+ })(Style$3 || (Style$3 = {}));
300
+ const ZERO_BOX$2 = '0px 0px 0px 0px';
195
301
  const HEADING_LEVELS = ['h2', 'h3', 'h4', 'h5', 'h6'];
196
302
  const DEFAULT_LEVEL = 'h3';
197
303
  /** A linked trait the header must never carry: it is a button, not a link. */
198
304
  const LINK_TYPE$1 = 'linkType';
199
305
  /** What each part holds in the recipe — layout only; its LOOK is its element style. */
200
- const PART_BASE = {
306
+ const PART_BASE$1 = {
201
307
  // The items are a column a token apart (`accordion-gap`: 0 for a divider list, 12px for cards).
202
308
  [AccordionRole.Accordion]: { display: 'flex', flexDirection: 'column', gap: 'var(--iox-accordion-gap)', width: '100%', minHeight: 'auto', height: 'auto' },
203
309
  [AccordionRole.Item]: { display: 'block', width: '100%', minHeight: 'auto', height: 'auto' },
@@ -208,22 +314,16 @@ const PART_BASE = {
208
314
  };
209
315
  /** The question INHERITS the header's ink (board 4's wears table: the Question reads no colour token), so the
210
316
  * header's hover ink reaches the words; and it sits in the row with no margin of its own. */
211
- const QUESTION_BASE = { color: 'inherit', margin: ZERO_BOX$1, flexGrow: '1', minWidth: '0px' };
212
- const ANSWER_BASE = { margin: ZERO_BOX$1, padding: ZERO_BOX$1, minHeight: 'auto' };
213
- function make$1(create, key, type, role, style, props = {}, label) {
214
- const node = create({ key, type, props, elementStyle: style });
215
- if (role)
216
- node.role = role;
217
- if (label)
218
- node.label = label;
219
- if (role && PART_BASE[role])
220
- writeStyles(node, PART_BASE[role]);
221
- return node;
222
- }
223
- /** The item's icon; its Item-open state (the turn) is driven by ITS item (`stateTriggers`). */
224
- function buildIcon(create, key, icon, itemId) {
317
+ const QUESTION_BASE = { color: 'inherit', margin: ZERO_BOX$2, flexGrow: '1', minWidth: '0px' };
318
+ const ANSWER_BASE = { margin: ZERO_BOX$2, padding: ZERO_BOX$2, minHeight: 'auto' };
319
+ function make$2(create, key, type, role, style, props = {}, label) {
320
+ return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE$1[role] : undefined);
321
+ }
322
+ /** The icon of an item (an accordion's, or a disclosure's — `role`); its Item-open state (the turn) is driven by
323
+ * ITS item (`stateTriggers`). ONE builder for both kinds. */
324
+ function buildCompositeIcon(create, key, icon, itemId, role = AccordionRole.Icon) {
225
325
  const glyph = ACCORDION_ICON_GLYPH[icon];
226
- const node = make$1(create, key, Type$1.Icon, AccordionRole.Icon, Style$1.Icon, { iconClass: glyph.iconClass }, 'Icon');
326
+ const node = makeCompositePart(create, { key, type: Type$3.Icon, props: { iconClass: glyph.iconClass }, elementStyle: Style$3.Icon }, role, 'Icon', PART_BASE$1[AccordionRole.Icon]);
227
327
  node.stateStyles = { ...(node.stateStyles ?? {}), [ITEM_OPEN_STATE]: { rotate: glyph.turn } };
228
328
  if (itemId)
229
329
  node.stateTriggers = { ...(node.stateTriggers ?? {}), [ITEM_OPEN_STATE]: itemId };
@@ -231,14 +331,14 @@ function buildIcon(create, key, icon, itemId) {
231
331
  }
232
332
  /** One item: header (question + icon) and panel (answer). `key` makes every part's recipe key unique. */
233
333
  function buildAccordionItem(create, words, itemKey, icon = AccordionIcon.Chevron) {
234
- const item = make$1(create, `${itemKey}`, Type$1.Container, AccordionRole.Item, Style$1.Item, {}, 'Item');
334
+ const item = make$2(create, `${itemKey}`, Type$3.Container, AccordionRole.Item, Style$3.Item, {}, 'Item');
235
335
  item.itemKey = itemKey;
236
- const trigger = make$1(create, `${itemKey}-trigger`, Type$1.LinkedContainer, AccordionRole.Trigger, Style$1.Trigger, {}, 'Header');
237
- const question = make$1(create, `${itemKey}-question`, Type$1.Heading, null, Style$1.Question, { content: words.question, level: DEFAULT_LEVEL }, 'Heading');
336
+ const trigger = make$2(create, `${itemKey}-trigger`, Type$3.LinkedContainer, AccordionRole.Trigger, Style$3.Trigger, {}, 'Header');
337
+ const question = make$2(create, `${itemKey}-question`, Type$3.Heading, null, Style$3.Question, { content: words.question, level: DEFAULT_LEVEL }, 'Heading');
238
338
  writeStyles(question, QUESTION_BASE);
239
- trigger.children = [question, ...(icon === AccordionIcon.None ? [] : [buildIcon(create, `${itemKey}-icon`, icon, item.id)])];
240
- const panel = make$1(create, `${itemKey}-panel`, Type$1.Container, AccordionRole.Panel, Style$1.Panel, {}, 'Panel');
241
- const answer = make$1(create, `${itemKey}-answer`, Type$1.Text, null, Style$1.Answer, { content: words.answer, tag: 'p' }, 'Text');
339
+ trigger.children = [question, ...(icon === AccordionIcon.None ? [] : [buildCompositeIcon(create, `${itemKey}-icon`, icon, item.id)])];
340
+ const panel = make$2(create, `${itemKey}-panel`, Type$3.Container, AccordionRole.Panel, Style$3.Panel, {}, 'Panel');
341
+ const answer = make$2(create, `${itemKey}-answer`, Type$3.Text, null, Style$3.Answer, { content: words.answer, tag: 'p' }, 'Text');
242
342
  writeStyles(answer, ANSWER_BASE);
243
343
  panel.children = [answer];
244
344
  item.children = [trigger, panel];
@@ -246,7 +346,7 @@ function buildAccordionItem(create, words, itemKey, icon = AccordionIcon.Chevron
246
346
  }
247
347
  /** A fresh Accordion: three items, one open at a time, the first open by default (board 4). */
248
348
  function buildAccordionRecipe(create, options = {}) {
249
- const root = make$1(create, 'accordion', Type$1.Container, AccordionRole.Accordion, Style$1.Root, {}, 'Accordion');
349
+ const root = make$2(create, 'accordion', Type$3.Container, AccordionRole.Accordion, Style$3.Root, {}, 'Accordion');
250
350
  writeProp(root, CompositeBehaviourTrait.Multiple, options.multiple ?? false);
251
351
  writeProp(root, CompositeBehaviourTrait.DefaultOn, options.defaultOn ?? AccordionDefaultOn.First);
252
352
  const words = options.items ?? ACCORDION_WORDS;
@@ -285,7 +385,7 @@ function accordionItemOf(node, path) {
285
385
  }
286
386
  /** True when the root takes its items from content (a Repeater bound to a list, R3). */
287
387
  function isAccordionFromContent(root) {
288
- return !!root && root.type === Type$1.Repeater;
388
+ return !!root && root.type === Type$3.Repeater;
289
389
  }
290
390
  /** The root's items in order — every item (Manual), or the one template item (From content). */
291
391
  function accordionItems(root) {
@@ -307,29 +407,43 @@ function setAccordionBehaviour(root, patch) {
307
407
  ok = writeProp(root, CompositeBehaviourTrait.DefaultOn, patch.defaultOn) && ok;
308
408
  return ok;
309
409
  }
310
- /** The header of an item. */
410
+ /** The roles of the kind an item belongs to (an accordion item, or a disclosure — its own item). */
411
+ function kindRolesOf(node) {
412
+ return COMPOSITE_KIND_ROLES[compositeKindOfRole(node.role) ?? CompositeKind.Accordion];
413
+ }
414
+ /** The header of an item (either kind). */
311
415
  function accordionTriggerOf(item) {
312
- return findByRole(item, AccordionRole.Trigger);
416
+ return findByRole(item, kindRolesOf(item).trigger);
313
417
  }
314
- /** The panel of an item. */
418
+ /** The panel of an item (either kind). */
315
419
  function accordionPanelOf(item) {
316
- return findByRole(item, AccordionRole.Panel);
420
+ return findByRole(item, kindRolesOf(item).panel);
317
421
  }
318
- /** The question: the first Heading inside the header. */
422
+ /** A root's items — an accordion's (`accordionItems`), or a disclosure, which is its own one item. */
423
+ function compositeItems(root) {
424
+ return compositeKindOfRole(root.role) === CompositeKind.Disclosure ? [root] : accordionItems(root);
425
+ }
426
+ /** The question: the first Heading inside the header — a disclosure's toggle holds no heading, so its first word
427
+ * (the Text shown while closed, "Read more"). */
319
428
  function accordionQuestionOf(item) {
320
429
  const trigger = accordionTriggerOf(item);
321
430
  let hit;
431
+ let text;
322
432
  if (trigger)
323
- walkComposite(trigger, n => { if (!hit && n.type === Type$1.Heading)
324
- hit = n; });
325
- return hit;
433
+ walkComposite(trigger, n => {
434
+ if (!hit && n.type === Type$3.Heading)
435
+ hit = n;
436
+ if (!text && n.type === Type$3.Text)
437
+ text = n;
438
+ });
439
+ return hit ?? text;
326
440
  }
327
441
  /** The answer: the first Text inside the panel. */
328
442
  function accordionAnswerOf(item) {
329
443
  const panel = accordionPanelOf(item);
330
444
  let hit;
331
445
  if (panel)
332
- walkComposite(panel, n => { if (!hit && n.type === Type$1.Text)
446
+ walkComposite(panel, n => { if (!hit && n.type === Type$3.Text)
333
447
  hit = n; });
334
448
  return hit;
335
449
  }
@@ -350,7 +464,7 @@ function accordionHeadingLevel(trigger) {
350
464
  const visit = (n) => {
351
465
  if (level !== undefined)
352
466
  return;
353
- if (n.type === Type$1.Heading) {
467
+ if (n.type === Type$3.Heading) {
354
468
  level = readProp(n, 'level');
355
469
  return;
356
470
  }
@@ -406,10 +520,10 @@ function accordionRemovalRefusal(root, item) {
406
520
  return null;
407
521
  return accordionItems(root).length <= 1 ? AccordionEditRefusal.LastItem : null;
408
522
  }
409
- /** The icon the accordion's items wear now (the first item's; None when it has no icon). */
523
+ /** The icon the composite's items wear now (the first item's; None when it has no icon). Either kind. */
410
524
  function detectAccordionIcon(root) {
411
- const first = accordionItems(root)[0];
412
- const icon = first ? findByRole(first, AccordionRole.Icon) : undefined;
525
+ const first = compositeItems(root)[0];
526
+ const icon = first ? findByRole(first, kindRolesOf(root).icon) : undefined;
413
527
  if (!icon)
414
528
  return first ? AccordionIcon.None : AccordionIcon.Chevron;
415
529
  const cls = String(readProp(icon, 'iconClass') ?? '');
@@ -422,11 +536,12 @@ function detectAccordionIcon(root) {
422
536
  */
423
537
  function setAccordionIcon(root, icon, create) {
424
538
  let changed = 0;
425
- for (const item of accordionItems(root)) {
539
+ const iconRole = kindRolesOf(root).icon;
540
+ for (const item of compositeItems(root)) {
426
541
  const trigger = accordionTriggerOf(item);
427
542
  if (!trigger)
428
543
  continue;
429
- const existing = findByRole(trigger, AccordionRole.Icon);
544
+ const existing = findByRole(trigger, iconRole);
430
545
  if (icon === AccordionIcon.None) {
431
546
  if (existing && removeFromComposite(trigger, existing))
432
547
  changed++;
@@ -439,7 +554,7 @@ function setAccordionIcon(root, icon, create) {
439
554
  changed++;
440
555
  }
441
556
  else if (create) {
442
- trigger.children = [...(trigger.children ?? []), buildIcon(create, `${item.itemKey ?? 'item'}-icon`, icon, item.id)];
557
+ trigger.children = [...(trigger.children ?? []), buildCompositeIcon(create, `${item.itemKey ?? 'item'}-icon`, icon, item.id, iconRole)];
443
558
  changed++;
444
559
  }
445
560
  }
@@ -475,10 +590,16 @@ function nextAccordionState(open, key, behaviour) {
475
590
  }
476
591
  return behaviour.multiple ? new Set([...open, key]) : new Set([key]);
477
592
  }
478
- /** The open keys after OPENING `key` (find-in-page, selecting into a closed panel). */
593
+ /** The open keys after OPENING `key` (find-in-page, selecting into a closed panel, the `openItem` action). */
479
594
  function openAccordionItem(open, key, behaviour) {
480
595
  return open.has(key) ? new Set(open) : nextAccordionState(open, key, behaviour);
481
596
  }
597
+ /** The open keys after CLOSING `key` (the `closeItem` action). */
598
+ function closeAccordionItem(open, key) {
599
+ const next = new Set(open);
600
+ next.delete(key);
601
+ return next;
602
+ }
482
603
  var AccordionKeyAction;
483
604
  (function (AccordionKeyAction) {
484
605
  AccordionKeyAction["Next"] = "next";
@@ -520,17 +641,30 @@ function rowItemKey(row, index) {
520
641
  const id = r && typeof r === 'object' ? (r._id ?? r.id) : undefined;
521
642
  return id !== undefined && id !== null && id !== '' ? domSafeKey(id) : `row-${index}`;
522
643
  }
523
- /** The ARIA pairing ids of an item: unique per accordion (its root's node id) and per item (its key). */
524
- function accordionDomIds(rootId, itemKey) {
525
- const base = `iox-acc-${domSafeKey(rootId)}-${domSafeKey(itemKey)}`;
644
+ const DOM_ID_PREFIX = {
645
+ [CompositeKind.Accordion]: 'iox-acc',
646
+ [CompositeKind.Disclosure]: 'iox-dis',
647
+ };
648
+ /** The ARIA pairing ids of an item: unique per composite (its SCOPE id — `compositeScopeId`) and per item (its key). */
649
+ function accordionDomIds(rootId, itemKey, kind = CompositeKind.Accordion) {
650
+ const base = `${DOM_ID_PREFIX[kind]}-${domSafeKey(rootId)}-${domSafeKey(itemKey)}`;
526
651
  return { trigger: `${base}-h`, panel: `${base}-p` };
527
652
  }
528
- function accordionScope(rootId, behaviour, keys) {
529
- return { rootId, behaviour, keys, open: initialAccordionOpen(keys, behaviour.defaultOn) };
653
+ /**
654
+ * The id a rendered composite's ARIA ids are built from: its node id, and — when it sits inside repeated rows —
655
+ * every enclosing row's identity, outermost first (vectoriox/iox-ui#20: an accordion repeated per card used to
656
+ * pair every row's header with the FIRST row's panel). The engine folds each row's `rowItemKey`; the builder
657
+ * canvas its preview row's index. Unrepeated, it is the node id unchanged.
658
+ */
659
+ function compositeScopeId(rootId, rowKeys = []) {
660
+ return rowKeys.length ? [rootId, ...rowKeys.map(domSafeKey)].join('-') : rootId;
661
+ }
662
+ function accordionScope(rootId, behaviour, keys, kind = CompositeKind.Accordion) {
663
+ return { kind, rootId, behaviour, keys, open: initialAccordionOpen(keys, behaviour.defaultOn) };
530
664
  }
531
665
  function accordionItemState(scope, index) {
532
666
  const key = scope.keys[index] ?? `item-${index}`;
533
- return { key, index, on: scope.open.has(key), ids: accordionDomIds(scope.rootId, key) };
667
+ return { kind: scope.kind, key, index, on: scope.open.has(key), ids: accordionDomIds(scope.rootId, key, scope.kind) };
534
668
  }
535
669
  /** `data-iox-on` — the ONE attribute every "on" style keys on (Open, Item open). */
536
670
  const ON_ATTRIBUTE = 'data-iox-on';
@@ -538,6 +672,9 @@ const ITEM_KEY_ATTRIBUTE = 'data-iox-item-key';
538
672
  const COMPOSITE_ATTRIBUTE = 'data-iox-composite';
539
673
  const MULTIPLE_ATTRIBUTE = 'data-iox-multiple';
540
674
  const DEFAULT_ON_ATTRIBUTE = 'data-iox-default-on';
675
+ /** The scope id the ARIA ids are built from (`compositeScopeId`), written by the engine so the runtime re-stamps
676
+ * exactly the ids the server rendered. */
677
+ const SCOPE_ATTRIBUTE = 'data-iox-scope';
541
678
  /** The value a closed panel's region carries: find-in-page can still reveal it (D9, board 4). */
542
679
  const UNTIL_FOUND = 'until-found';
543
680
  /** The structural classes the renderers write around an accordion's parts (no node owns them). */
@@ -548,11 +685,12 @@ var AccordionClass;
548
685
  AccordionClass["Track"] = "iox-accordion-track";
549
686
  AccordionClass["Region"] = "iox-accordion-region";
550
687
  })(AccordionClass || (AccordionClass = {}));
551
- function accordionRootAttributes(behaviour) {
688
+ function accordionRootAttributes(behaviour, kind = CompositeKind.Accordion, scopeId) {
552
689
  return {
553
- [COMPOSITE_ATTRIBUTE]: AccordionRole.Accordion,
690
+ [COMPOSITE_ATTRIBUTE]: kind,
554
691
  [DEFAULT_ON_ATTRIBUTE]: behaviour.defaultOn,
555
- ...(behaviour.multiple ? { [MULTIPLE_ATTRIBUTE]: '' } : {}),
692
+ ...(behaviour.multiple && kind === CompositeKind.Accordion ? { [MULTIPLE_ATTRIBUTE]: '' } : {}),
693
+ ...(scopeId ? { [SCOPE_ATTRIBUTE]: scopeId } : {}),
556
694
  };
557
695
  }
558
696
  function accordionItemAttributes(state) {
@@ -571,15 +709,21 @@ function accordionTriggerAttributes(state) {
571
709
  function accordionTrackAttributes(state) {
572
710
  return state.on ? { [ON_ATTRIBUTE]: '' } : {};
573
711
  }
574
- /** The region the panel sits in: `hidden="until-found"` while closed, so its words stay in the page. */
712
+ /** The region the panel sits in: `hidden="until-found"` while closed, so its words stay in the page. An accordion's
713
+ * is a labelled `region` (APG accordion); a disclosure's is a plain block (APG disclosure: the button names it). */
575
714
  function accordionRegionAttributes(state) {
715
+ const labelled = hasLabelledRegion(state.kind);
576
716
  return {
577
- role: 'region',
717
+ ...(labelled ? { role: 'region' } : {}),
578
718
  id: state.ids.panel,
579
- 'aria-labelledby': state.ids.trigger,
719
+ ...(labelled ? { 'aria-labelledby': state.ids.trigger } : {}),
580
720
  ...(state.on ? {} : { hidden: UNTIL_FOUND }),
581
721
  };
582
722
  }
723
+ /** True when a region of this kind is a labelled `role=region`. */
724
+ function hasLabelledRegion(kind) {
725
+ return kind === CompositeKind.Accordion;
726
+ }
583
727
  function accordionPanelAttributes(state) {
584
728
  return state.on ? { [ON_ATTRIBUTE]: '' } : {};
585
729
  }
@@ -654,13 +798,17 @@ function accordionPartStyle(node, name) {
654
798
  * paths render the same markup). It reads the markup the renderers write (iox-design.css + accordion.ts's
655
799
  * attribute planners), so the server's first paint and every later state agree:
656
800
  *
657
- * [root] data-iox-composite="accordion" data-iox-default-on [data-iox-multiple] (the node's own element —
658
- * its role class may sit on a wrapper host, as the engine's component-path Repeater draws it)
801
+ * [root] data-iox-composite="accordion" data-iox-default-on [data-iox-multiple] [data-iox-scope] (the node's
802
+ * own element — its role class may sit on a wrapper host, as the engine's component-path Repeater draws it)
659
803
  * .iox-role-accordion-item data-iox-item-key [data-iox-on]
660
804
  * hN.iox-accordion-heading > button.iox-accordion-button id aria-expanded aria-controls
661
805
  * .iox-accordion-track [data-iox-on] > .iox-accordion-region role=region [hidden=until-found]
662
806
  * .iox-role-accordion-panel
663
807
  *
808
+ * The DISCLOSURE (CC2.1) runs on this same runtime: `data-iox-composite="disclosure"` on a root that IS its one
809
+ * item (it carries the key and `data-iox-on`), its button straight inside it (no heading), its panel in the same
810
+ * track + region (a plain block — no `role=region`), and no arrow keys (one button).
811
+ *
664
812
  * It owns: a click on a header toggles its item; ↓ ↑ Home End move between the headers of THIS accordion
665
813
  * (a nested one keeps its own); find-in-page (`beforematch`) opens the item that holds the match; a closed
666
814
  * panel gets `hidden="until-found"` after its track has collapsed (at once under reduced motion). Every
@@ -670,23 +818,31 @@ function accordionPartStyle(node, name) {
670
818
  */
671
819
  const ITEM_ON_EVENT = 'iox-item-on';
672
820
  const ITEM_OFF_EVENT = 'iox-item-off';
673
- const ROOT_SELECTOR = `[${COMPOSITE_ATTRIBUTE}="${AccordionRole.Accordion}"]`;
674
- const ITEM_SELECTOR = '.iox-role-accordion-item';
675
- const PANEL_SELECTOR = '.iox-role-accordion-panel';
821
+ const KIND_ROOT = (kind) => `[${COMPOSITE_ATTRIBUTE}="${kind}"]`;
822
+ const ROOT_SELECTOR = Object.values(CompositeKind).map(KIND_ROOT).join(', ');
823
+ /** An accordion's items are its `.iox-role-accordion-item` children; a disclosure is its own item. */
824
+ const ACCORDION_ITEM_SELECTOR = '.iox-role-accordion-item';
825
+ const ITEM_SELECTOR = `${ACCORDION_ITEM_SELECTOR}, ${KIND_ROOT(CompositeKind.Disclosure)}`;
826
+ const PANEL_SELECTOR = '.iox-role-accordion-panel, .iox-role-disclosure-panel';
676
827
  const BUTTON_SELECTOR = `.${AccordionClass.Button}`;
677
828
  const TRACK_SELECTOR = `.${AccordionClass.Track}`;
678
829
  const REGION_SELECTOR = `.${AccordionClass.Region}`;
679
830
  /** How long a closing track may take before its region is hidden (dur-base is 200–320ms in every pack). */
680
831
  const MAX_CLOSE_MS = 600;
681
832
  const REDUCED_MOTION = '(prefers-reduced-motion: reduce)';
682
- /** The accordion an element belongs to (the element itself when it is a root). */
833
+ /** The composite (accordion or disclosure) an element belongs to (the element itself when it is a root). */
683
834
  function closestAccordion(el) {
684
835
  return el?.closest?.(ROOT_SELECTOR) ?? null;
685
836
  }
686
- /** The item an element sits in (itself when it is one). */
837
+ /** The item an element sits in (itself when it is one) — an accordion item, or a disclosure. */
687
838
  function closestAccordionItem(el) {
688
839
  return el?.closest?.(ITEM_SELECTOR) ?? null;
689
840
  }
841
+ const kindOf = (root) => root.getAttribute(COMPOSITE_ATTRIBUTE) === CompositeKind.Disclosure ? CompositeKind.Disclosure : CompositeKind.Accordion;
842
+ /** The composite root whose items an item belongs to: a disclosure is its own root. */
843
+ function rootOfItem(item) {
844
+ return kindOf(item) === CompositeKind.Disclosure && item.matches(KIND_ROOT(CompositeKind.Disclosure)) ? item : closestAccordion(item.parentElement);
845
+ }
690
846
  /** Subscribe to an item turning on / off — what the `itemOn` / `itemOff` interaction triggers listen to. The
691
847
  * item is the element's own item (the element itself when it is one). Returns the unsubscribe. */
692
848
  function onAccordionItemState(el, on, handler) {
@@ -745,6 +901,9 @@ class AccordionRuntime {
745
901
  this.toggle(this.keyOf(item));
746
902
  };
747
903
  this.onKeydown = (event) => {
904
+ // A disclosure has one button: Enter / Space are the button's own, and there is nowhere to move.
905
+ if (this.kind !== CompositeKind.Accordion)
906
+ return;
748
907
  const e = event;
749
908
  const action = accordionKeyAction(e.key);
750
909
  const button = e.target?.closest?.(BUTTON_SELECTOR);
@@ -761,12 +920,15 @@ class AccordionRuntime {
761
920
  this.onBeforeMatch = (event) => {
762
921
  const region = event.target?.closest?.(REGION_SELECTOR);
763
922
  const item = closestAccordionItem(region);
764
- if (!item || closestAccordion(item.parentElement) !== this.root)
923
+ const items = this.items();
924
+ if (!item || !items.includes(item))
765
925
  return;
766
- this.open(this.keyOf(item));
926
+ this.open(this.keyOf(item, items));
767
927
  };
928
+ this.kind = options.kind ?? kindOf(root);
768
929
  this.behaviourValue = options.behaviour ?? behaviourOf(root);
769
- root.setAttribute(COMPOSITE_ATTRIBUTE, AccordionRole.Accordion);
930
+ root.setAttribute(COMPOSITE_ATTRIBUTE, this.kind);
931
+ RUNTIMES.set(root, this);
770
932
  // The server rendered the first state; with no server state the root's default decides.
771
933
  // (The builder passes the behaviour and starts from the saved default; on the live site the server
772
934
  // stamped every item's key and state, and that is the truth.)
@@ -805,9 +967,11 @@ class AccordionRuntime {
805
967
  else
806
968
  this.stamp();
807
969
  }
808
- /** THIS accordion's items, in document order (a nested accordion's items are its own). */
970
+ /** THIS composite's items, in document order (a nested composite's items are its own); a disclosure's is itself. */
809
971
  items() {
810
- return Array.from(this.root.querySelectorAll(ITEM_SELECTOR)).filter(i => closestAccordion(i.parentElement) === this.root);
972
+ if (this.kind === CompositeKind.Disclosure)
973
+ return [this.root];
974
+ return Array.from(this.root.querySelectorAll(ACCORDION_ITEM_SELECTOR)).filter(i => closestAccordion(i.parentElement) === this.root);
811
975
  }
812
976
  /** The open keys the live state holds (before anything forced). */
813
977
  get liveKeys() {
@@ -828,6 +992,20 @@ class AccordionRuntime {
828
992
  this.live = openAccordionItem(this.live, key, this.behaviourValue);
829
993
  this.stamp();
830
994
  }
995
+ close(key) {
996
+ this.live = closeAccordionItem(this.live, key);
997
+ this.stamp();
998
+ }
999
+ /** Run an item action (`openItem` / `closeItem` / `toggleItem`, T14) on `key`. True when it shows open after. */
1000
+ run(action, key) {
1001
+ if (action === CompositeItemAction.Open)
1002
+ this.open(key);
1003
+ else if (action === CompositeItemAction.Close)
1004
+ this.close(key);
1005
+ else
1006
+ this.toggle(key);
1007
+ return this.shown.has(key);
1008
+ }
831
1009
  /** Back to the saved default ("Showing on canvas ▸ As saved"). */
832
1010
  resetLive() {
833
1011
  const items = this.items();
@@ -839,6 +1017,8 @@ class AccordionRuntime {
839
1017
  this.stamp();
840
1018
  }
841
1019
  destroy() {
1020
+ if (RUNTIMES.get(this.root) === this)
1021
+ RUNTIMES.delete(this.root);
842
1022
  this.root.removeEventListener('click', this.onClick, true);
843
1023
  this.root.removeEventListener('keydown', this.onKeydown);
844
1024
  this.root.removeEventListener('beforematch', this.onBeforeMatch, true);
@@ -878,7 +1058,8 @@ class AccordionRuntime {
878
1058
  first = true;
879
1059
  }
880
1060
  const forced = this.options.forced?.() ?? new Set();
881
- const rootId = this.options.rootId ?? nodeIdOf(this.root);
1061
+ const own = this.options.rootId;
1062
+ const rootId = (typeof own === 'function' ? own() : own) ?? this.root.getAttribute(SCOPE_ATTRIBUTE) ?? nodeIdOf(this.root);
882
1063
  const next = new Set();
883
1064
  items.forEach((item, index) => {
884
1065
  const key = this.keyOf(item, items);
@@ -901,7 +1082,7 @@ class AccordionRuntime {
901
1082
  }
902
1083
  }
903
1084
  stampItem(item, key, index, on, rootId, first) {
904
- const ids = accordionDomIds(rootId, key);
1085
+ const ids = accordionDomIds(rootId, key, this.kind);
905
1086
  if (!item.hasAttribute(ITEM_KEY_ATTRIBUTE))
906
1087
  item.setAttribute(ITEM_KEY_ATTRIBUTE, key);
907
1088
  setAttr(item, ON_ATTRIBUTE, on ? '' : null);
@@ -916,9 +1097,10 @@ class AccordionRuntime {
916
1097
  setAttr(button, ON_ATTRIBUTE, on ? '' : null);
917
1098
  setAttr(track, ON_ATTRIBUTE, on ? '' : null);
918
1099
  setAttr(panel, ON_ATTRIBUTE, on ? '' : null);
919
- setAttr(region, 'role', 'region');
1100
+ const labelled = hasLabelledRegion(this.kind);
1101
+ setAttr(region, 'role', labelled ? 'region' : null);
920
1102
  setAttr(region, 'id', ids.panel);
921
- setAttr(region, 'aria-labelledby', ids.trigger);
1103
+ setAttr(region, 'aria-labelledby', labelled ? ids.trigger : null);
922
1104
  if (!region)
923
1105
  return;
924
1106
  const pending = this.closing.get(key);
@@ -969,14 +1151,12 @@ function dispatch(el, type) {
969
1151
  if (Ctor)
970
1152
  el.dispatchEvent(new Ctor(type, { bubbles: false }));
971
1153
  }
972
- /** Attach the runtime to one rendered accordion root, unless it already runs one. Returns the NEW runtime, or
1154
+ /** Attach the runtime to one rendered composite root, unless it already runs one. Returns the NEW runtime, or
973
1155
  * null when the root already had one (so a caller destroys only what it created). */
974
1156
  function attachAccordionAt(root, options = {}) {
975
- if (ATTACHED.has(root))
1157
+ if (RUNTIMES.has(root))
976
1158
  return null;
977
- const runtime = new AccordionRuntime(root, options);
978
- ATTACHED.set(root, runtime);
979
- return runtime;
1159
+ return new AccordionRuntime(root, options);
980
1160
  }
981
1161
  /** Attach the runtime to every accordion under `container` that has none yet. Returns the new runtimes. */
982
1162
  function attachAccordions(container, options = {}) {
@@ -991,9 +1171,134 @@ function attachAccordions(container, options = {}) {
991
1171
  /** Detach a runtime `attachAccordions` created. */
992
1172
  function detachAccordion(runtime) {
993
1173
  runtime.destroy();
994
- ATTACHED.delete(runtime.root);
995
1174
  }
996
- const ATTACHED = new WeakMap();
1175
+ /** Every live runtime by its root — the canvas's and the site's alike (a runtime registers itself and leaves on
1176
+ * destroy), so an item action finds the runtime of whatever item it targets. */
1177
+ const RUNTIMES = new WeakMap();
1178
+ const ROW_INDEX_ATTRIBUTE$1 = 'data-iox-row-idx';
1179
+ /** The repeated rows an element sits in, outermost first — each row's index as its row wrappers stamp it
1180
+ * (`data-iox-row-idx`: the builder's Repeater rows, the engine's component path). The builder folds them into a
1181
+ * composite's scope id (`compositeScopeId`, vectoriox/iox-ui#20); the element itself is not counted. */
1182
+ function compositeRowKeys(el) {
1183
+ const keys = [];
1184
+ for (let at = el?.parentElement?.closest?.(`[${ROW_INDEX_ATTRIBUTE$1}]`); at; at = at.parentElement?.closest(`[${ROW_INDEX_ATTRIBUTE$1}]`)) {
1185
+ keys.unshift(at.getAttribute(ROW_INDEX_ATTRIBUTE$1) ?? '');
1186
+ }
1187
+ return keys;
1188
+ }
1189
+ /** The runtime running a composite root, or null. */
1190
+ function accordionRuntimeOf(root) {
1191
+ return (root && RUNTIMES.get(root)) || null;
1192
+ }
1193
+ // ── T14: open / close / toggle an item from anywhere ─────────────────────────────────────────────
1194
+ /** What an item action does to its target item (actions-core `openItem` / `closeItem` / `toggleItem`). */
1195
+ var CompositeItemAction;
1196
+ (function (CompositeItemAction) {
1197
+ CompositeItemAction["Open"] = "open";
1198
+ CompositeItemAction["Close"] = "close";
1199
+ CompositeItemAction["Toggle"] = "toggle";
1200
+ })(CompositeItemAction || (CompositeItemAction = {}));
1201
+ /**
1202
+ * Run an item action on the element of an ITEM (an accordion item, or a disclosure) — the `openItem` /
1203
+ * `closeItem` / `toggleItem` interaction actions, on the canvas and on the live site through this one function.
1204
+ * Returns whether the item shows open afterwards, or null when `el` is in no running composite.
1205
+ */
1206
+ function runCompositeItemAction(el, action) {
1207
+ const item = closestAccordionItem(el);
1208
+ const runtime = item ? accordionRuntimeOf(rootOfItem(item)) : null;
1209
+ if (!item || !runtime)
1210
+ return null;
1211
+ return runtime.run(action, runtime.keyOf(item));
1212
+ }
1213
+ /** How much of an opened item must already be on screen for the action NOT to scroll to it (its header row). */
1214
+ const VISIBLE_HEAD_PX = 48;
1215
+ /** True when an item an action just opened is out of view and the page should scroll to it (T14: "opens that item
1216
+ * and scrolls to it") — never when its head is already on screen, so a button beside it does not jolt the page. */
1217
+ function shouldScrollToItem(top, viewportHeight) {
1218
+ return top < 0 || top + VISIBLE_HEAD_PX > viewportHeight;
1219
+ }
1220
+
1221
+ /** The words a fresh disclosure starts with — generic, the author's to replace. */
1222
+ const DISCLOSURE_WORDS = {
1223
+ more: 'Read more',
1224
+ less: 'Read less',
1225
+ body: 'Write the rest of the story here. It stays out of the way until someone asks for it.',
1226
+ };
1227
+ /** A disclosure's one item key — its root IS its item. */
1228
+ const DISCLOSURE_ITEM_KEY = 'k1';
1229
+ var Type$2;
1230
+ (function (Type) {
1231
+ Type["Container"] = "Container";
1232
+ Type["LinkedContainer"] = "LinkedContainer";
1233
+ Type["Text"] = "Text";
1234
+ })(Type$2 || (Type$2 = {}));
1235
+ var Style$2;
1236
+ (function (Style) {
1237
+ Style["Root"] = "box";
1238
+ Style["Trigger"] = "accordion-trigger";
1239
+ Style["Word"] = "body";
1240
+ Style["Panel"] = "accordion-panel";
1241
+ Style["Body"] = "body";
1242
+ })(Style$2 || (Style$2 = {}));
1243
+ const ZERO_BOX$1 = '0px 0px 0px 0px';
1244
+ const HIDDEN = 'none';
1245
+ const INHERIT = 'inherit';
1246
+ /** A word is a flex item of the toggle — `block` (never `inline`: the compiler drops a value equal to the CSS initial,
1247
+ * and the Text's own default would come back in its place). */
1248
+ const SHOWN = 'block';
1249
+ /** What each part holds — layout only; its LOOK is its element style. The trigger sizes to its words (a button in
1250
+ * the flow, not a full-width header row — `fit-content` keeps it from stretching) and the panel spans the root. The
1251
+ * trigger is `flex`, not `inline-flex`: it is a flex item of the root, which blockifies it on the site anyway. */
1252
+ const PART_BASE = {
1253
+ [DisclosureRole.Disclosure]: { display: 'flex', flexDirection: 'column', width: '100%', minHeight: 'auto', height: 'auto' },
1254
+ // The toggle reads the text around it (the page's body type, through the root) — a LinkedContainer is born with
1255
+ // its own literal font, which an accordion's heading masks and a disclosure's words would show.
1256
+ [DisclosureRole.Trigger]: {
1257
+ display: 'flex', alignItems: 'center', gap: '8px', width: 'fit-content', minHeight: 'auto', height: 'auto', textAlign: 'start', cursor: 'pointer',
1258
+ fontFamily: INHERIT, fontSize: INHERIT, fontWeight: INHERIT, lineHeight: INHERIT,
1259
+ },
1260
+ // A word is just its words: none of a Text's own box (its type's 10px padding, its full width) — the toggle's
1261
+ // padding is the click target.
1262
+ [DisclosureRole.More]: { display: SHOWN, margin: ZERO_BOX$1, padding: ZERO_BOX$1, width: 'auto' },
1263
+ [DisclosureRole.Less]: { display: HIDDEN, margin: ZERO_BOX$1, padding: ZERO_BOX$1, width: 'auto' },
1264
+ [DisclosureRole.Panel]: { display: 'block', width: '100%', minHeight: 'auto', height: 'auto' },
1265
+ };
1266
+ const BODY_BASE = { margin: ZERO_BOX$1, padding: ZERO_BOX$1, minHeight: 'auto' };
1267
+ function make$1(create, key, type, role, style, props, label) {
1268
+ return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE[role] : undefined);
1269
+ }
1270
+ /** One of the trigger's two words: shown while the disclosure is closed (`more`) or open (`less`). */
1271
+ function buildWord(create, role, words, rootId) {
1272
+ const node = make$1(create, role === DisclosureRole.More ? 'more' : 'less', Type$2.Text, role, Style$2.Word, { content: words, tag: 'span' }, 'Text');
1273
+ node.stateStyles = { ...(node.stateStyles ?? {}), [ITEM_OPEN_STATE]: { display: role === DisclosureRole.More ? HIDDEN : SHOWN } };
1274
+ if (rootId)
1275
+ node.stateTriggers = { ...(node.stateTriggers ?? {}), [ITEM_OPEN_STATE]: rootId };
1276
+ return node;
1277
+ }
1278
+ /** A fresh Disclosure: "Read more" opens the text below and reads "Read less" while it is open; closed by default. */
1279
+ function buildDisclosureRecipe(create, options = {}) {
1280
+ const words = { ...DISCLOSURE_WORDS, ...(options.words ?? {}) };
1281
+ const root = make$1(create, 'disclosure', Type$2.Container, DisclosureRole.Disclosure, Style$2.Root, {}, 'Disclosure');
1282
+ root.itemKey = DISCLOSURE_ITEM_KEY;
1283
+ writeProp(root, CompositeBehaviourTrait.DefaultOn, options.open ? AccordionDefaultOn.First : AccordionDefaultOn.None);
1284
+ const icon = options.icon ?? AccordionIcon.Chevron;
1285
+ const trigger = make$1(create, 'trigger', Type$2.LinkedContainer, DisclosureRole.Trigger, Style$2.Trigger, {}, 'Toggle');
1286
+ trigger.children = [
1287
+ buildWord(create, DisclosureRole.More, words.more, root.id),
1288
+ buildWord(create, DisclosureRole.Less, words.less, root.id),
1289
+ ...(icon === AccordionIcon.None ? [] : [buildCompositeIcon(create, 'icon', icon, root.id, DisclosureRole.Icon)]),
1290
+ ];
1291
+ const panel = make$1(create, 'panel', Type$2.Container, DisclosureRole.Panel, Style$2.Panel, {}, 'Panel');
1292
+ const body = make$1(create, 'body', Type$2.Text, null, Style$2.Body, { content: words.body, tag: 'p' }, 'Text');
1293
+ writeStyles(body, BODY_BASE);
1294
+ panel.children = [body];
1295
+ root.children = [trigger, panel];
1296
+ return root;
1297
+ }
1298
+ /** Behaviour ▸ Open by default (a disclosure: on or off). */
1299
+ function disclosureOpenByDefault(root) {
1300
+ return accordionBehaviour(root).defaultOn !== AccordionDefaultOn.None;
1301
+ }
997
1302
 
998
1303
  /**
999
1304
  * BADGE + CHIP (composite T25; complex-components board 8). A Badge is a Container wearing
@@ -1147,7 +1452,7 @@ const CARD_WORDS = {
1147
1452
  };
1148
1453
  /** The breakpoint whose layer stacks an "Image side" card (Breakpoint.Mobile, ≤767px). */
1149
1454
  const CARD_STACK_BREAKPOINT = 'mobile';
1150
- var Type;
1455
+ var Type$1;
1151
1456
  (function (Type) {
1152
1457
  Type["Container"] = "Container";
1153
1458
  Type["Image"] = "Image";
@@ -1155,8 +1460,8 @@ var Type;
1155
1460
  Type["Text"] = "Text";
1156
1461
  Type["Icon"] = "Icon";
1157
1462
  Type["ButtonBlock"] = "ButtonBlock";
1158
- })(Type || (Type = {}));
1159
- var Style;
1463
+ })(Type$1 || (Type$1 = {}));
1464
+ var Style$1;
1160
1465
  (function (Style) {
1161
1466
  Style["Card"] = "card";
1162
1467
  Style["Box"] = "box";
@@ -1169,7 +1474,7 @@ var Style;
1169
1474
  Style["Ghost"] = "button-ghost";
1170
1475
  Style["Icon"] = "icon";
1171
1476
  Style["Scrim"] = "media-scrim";
1172
- })(Style || (Style = {}));
1477
+ })(Style$1 || (Style$1 = {}));
1173
1478
  var Scheme;
1174
1479
  (function (Scheme) {
1175
1480
  Scheme["Default"] = "default";
@@ -1179,9 +1484,9 @@ const LINK_TYPE = 'linkType';
1179
1484
  const SAVE_ICON = 'ph ph-bookmark-simple';
1180
1485
  /** A value an element style writes — read from the contract, never retyped. */
1181
1486
  const styleValue = (style, trait) => String(elementStyleById(style)?.base[trait] ?? '');
1182
- const CARD_PAD = styleValue(Style.Card, 'padding');
1183
- const IMAGE_RADIUS = styleValue(Style.Media, 'borderRadius');
1184
- const TITLE_SIZE = styleValue(Style.Title, 'fontSize');
1487
+ const CARD_PAD = styleValue(Style$1.Card, 'padding');
1488
+ const IMAGE_RADIUS = styleValue(Style$1.Media, 'borderRadius');
1489
+ const TITLE_SIZE = styleValue(Style$1.Title, 'fontSize');
1185
1490
  const LEAD_SIZE = 'var(--iox-size-lead)';
1186
1491
  const ZERO_BOX = '0px 0px 0px 0px';
1187
1492
  /** What every card part holds in every layout — the recipe's box. */
@@ -1198,7 +1503,7 @@ const CARD_BASE = {
1198
1503
  // and the row's auto top margin takes the slack.
1199
1504
  [CardRole.Actions]: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '8px', margin: 'auto 0px 0px 0px', padding: '6px 0px 0px 0px', width: '100%', minHeight: 'auto' },
1200
1505
  };
1201
- const IMAGE_BASE = { display: 'block', width: '100%', maxWidth: '100%' };
1506
+ const IMAGE_BASE$1 = { display: 'block', width: '100%', maxWidth: '100%' };
1202
1507
  /** A button's label and a pill's words: inherit the type and ink of what holds them. */
1203
1508
  const LABEL_BASE = { width: 'auto', minHeight: 'auto', padding: ZERO_BOX, margin: ZERO_BOX };
1204
1509
  const INLINE_ICON = { fontSize: '1em' };
@@ -1209,7 +1514,7 @@ const LAYOUTS = {
1209
1514
  card: { flexDirection: 'column', alignItems: 'stretch', justifyContent: 'flex-start', minHeight: 'auto', padding: CARD_PAD },
1210
1515
  media: { ...IN_FLOW, flexBasis: 'auto' },
1211
1516
  image: { aspectRatio: '4 / 3', height: 'auto', minHeight: 'auto', borderRadius: IMAGE_RADIUS },
1212
- content: GROWING_CONTENT, contentStyle: Style.Box, contentScheme: Scheme.Default,
1517
+ content: GROWING_CONTENT, contentStyle: Style$1.Box, contentScheme: Scheme.Default,
1213
1518
  title: { fontSize: TITLE_SIZE },
1214
1519
  badgeOnMedia: true,
1215
1520
  },
@@ -1217,7 +1522,7 @@ const LAYOUTS = {
1217
1522
  card: { flexDirection: 'row', alignItems: 'stretch', justifyContent: 'flex-start', minHeight: 'auto', padding: CARD_PAD },
1218
1523
  media: { ...IN_FLOW, flexBasis: '42%' },
1219
1524
  image: { aspectRatio: 'auto', height: '100%', minHeight: '180px', borderRadius: IMAGE_RADIUS },
1220
- content: GROWING_CONTENT, contentStyle: Style.Box, contentScheme: Scheme.Default,
1525
+ content: GROWING_CONTENT, contentStyle: Style$1.Box, contentScheme: Scheme.Default,
1221
1526
  title: { fontSize: TITLE_SIZE },
1222
1527
  badgeOnMedia: true,
1223
1528
  stack: {
@@ -1231,7 +1536,7 @@ const LAYOUTS = {
1231
1536
  media: { position: 'absolute', top: '0px', right: '0px', bottom: '0px', left: '0px', flexBasis: 'auto' },
1232
1537
  image: { aspectRatio: 'auto', height: '100%', minHeight: 'auto', borderRadius: '0px' },
1233
1538
  content: { position: 'relative', flexGrow: '0', flexShrink: '0', flexBasis: 'auto', padding: `72px ${CARD_PAD} ${CARD_PAD} ${CARD_PAD}` },
1234
- contentStyle: Style.Scrim, contentScheme: Scheme.Inverse,
1539
+ contentStyle: Style$1.Scrim, contentScheme: Scheme.Inverse,
1235
1540
  title: { fontSize: TITLE_SIZE },
1236
1541
  badgeOnMedia: false,
1237
1542
  },
@@ -1239,7 +1544,7 @@ const LAYOUTS = {
1239
1544
  card: { flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', minHeight: 'auto', padding: CARD_PAD },
1240
1545
  media: { ...IN_FLOW, flexBasis: '72px' },
1241
1546
  image: { aspectRatio: '1 / 1', height: 'auto', minHeight: 'auto', borderRadius: IMAGE_RADIUS },
1242
- content: GROWING_CONTENT, contentStyle: Style.Box, contentScheme: Scheme.Default,
1547
+ content: GROWING_CONTENT, contentStyle: Style$1.Box, contentScheme: Scheme.Default,
1243
1548
  title: { fontSize: LEAD_SIZE },
1244
1549
  badgeOnMedia: false,
1245
1550
  partsOff: [CardPart.Text, CardPart.Actions],
@@ -1260,16 +1565,16 @@ function make(create, key, type, role, elementStyle, props = {}, label) {
1260
1565
  return node;
1261
1566
  }
1262
1567
  function buttonLabel(create, key, words) {
1263
- const label = make(create, key, Type.Text, null, Style.Label, { content: words, tag: 'span' });
1568
+ const label = make(create, key, Type$1.Text, null, Style$1.Label, { content: words, tag: 'span' });
1264
1569
  writeStyles(label, LABEL_BASE);
1265
1570
  return label;
1266
1571
  }
1267
1572
  /** Each optional part (and the locked title) as the recipe builds it. */
1268
1573
  const PART_BUILDERS = {
1269
1574
  [CardPart.Media]: (create) => {
1270
- const media = make(create, 'media', Type.Container, CardRole.Media, Style.Box, {}, 'Media');
1271
- const image = make(create, 'image', Type.Image, null, Style.Media, { src: '', alt: '' });
1272
- writeStyles(image, IMAGE_BASE);
1575
+ const media = make(create, 'media', Type$1.Container, CardRole.Media, Style$1.Box, {}, 'Media');
1576
+ const image = make(create, 'image', Type$1.Image, null, Style$1.Media, { src: '', alt: '' });
1577
+ writeStyles(image, IMAGE_BASE$1);
1273
1578
  media.children = [image];
1274
1579
  return media;
1275
1580
  },
@@ -1279,14 +1584,14 @@ const PART_BUILDERS = {
1279
1584
  badge.label = 'Badge';
1280
1585
  return badge;
1281
1586
  },
1282
- [CardPart.Eyebrow]: (create, words) => make(create, 'eyebrow', Type.Text, CardRole.Eyebrow, Style.Eyebrow, { content: words.eyebrow, tag: 'p' }, 'Eyebrow'),
1283
- [CardPart.Text]: (create, words) => make(create, 'text', Type.Text, CardRole.Text, Style.Body, { content: words.text, tag: 'p' }, 'Text'),
1587
+ [CardPart.Eyebrow]: (create, words) => make(create, 'eyebrow', Type$1.Text, CardRole.Eyebrow, Style$1.Eyebrow, { content: words.eyebrow, tag: 'p' }, 'Eyebrow'),
1588
+ [CardPart.Text]: (create, words) => make(create, 'text', Type$1.Text, CardRole.Text, Style$1.Body, { content: words.text, tag: 'p' }, 'Text'),
1284
1589
  [CardPart.Actions]: (create, words) => {
1285
- const actions = make(create, 'actions', Type.Container, CardRole.Actions, Style.Box, {}, 'Buttons');
1286
- const primary = make(create, 'primary', Type.ButtonBlock, null, Style.Primary);
1590
+ const actions = make(create, 'actions', Type$1.Container, CardRole.Actions, Style$1.Box, {}, 'Buttons');
1591
+ const primary = make(create, 'primary', Type$1.ButtonBlock, null, Style$1.Primary);
1287
1592
  primary.children = [buttonLabel(create, 'primary-label', words.primary)];
1288
- const ghost = make(create, 'secondary', Type.ButtonBlock, null, Style.Ghost);
1289
- const icon = make(create, 'secondary-icon', Type.Icon, null, Style.Icon, { iconClass: SAVE_ICON });
1593
+ const ghost = make(create, 'secondary', Type$1.ButtonBlock, null, Style$1.Ghost);
1594
+ const icon = make(create, 'secondary-icon', Type$1.Icon, null, Style$1.Icon, { iconClass: SAVE_ICON });
1290
1595
  writeStyles(icon, INLINE_ICON);
1291
1596
  ghost.children = [icon, buttonLabel(create, 'secondary-label', words.secondary)];
1292
1597
  actions.children = [primary, ghost];
@@ -1294,15 +1599,15 @@ const PART_BUILDERS = {
1294
1599
  },
1295
1600
  };
1296
1601
  function buildTitle(create, words) {
1297
- return make(create, 'title', Type.Heading, CardRole.Title, Style.Title, { content: words.title, level: 'h3', [LINK_TYPE]: LinkType.None }, 'Title');
1602
+ return make(create, 'title', Type$1.Heading, CardRole.Title, Style$1.Title, { content: words.title, level: 'h3', [LINK_TYPE]: LinkType.None }, 'Title');
1298
1603
  }
1299
1604
  /** A fresh Card v2: every part a node with its role and element style, laid out by `options.layout`. */
1300
1605
  function buildCardRecipe(create, options = {}) {
1301
1606
  const words = { ...CARD_WORDS, ...(options.words ?? {}) };
1302
1607
  const tone = options.badgeTone ?? BadgeTone.Accent;
1303
1608
  const on = (part) => options.parts?.[part] !== false;
1304
- const card = make(create, 'card', Type.Container, CardRole.Card, Style.Card, {}, 'Card');
1305
- const content = make(create, 'content', Type.Container, CardRole.Content, Style.Box, {}, 'Content');
1609
+ const card = make(create, 'card', Type$1.Container, CardRole.Card, Style$1.Card, {}, 'Card');
1610
+ const content = make(create, 'content', Type$1.Container, CardRole.Content, Style$1.Box, {}, 'Content');
1306
1611
  content.children = [
1307
1612
  ...(on(CardPart.Eyebrow) ? [PART_BUILDERS[CardPart.Eyebrow](create, words, tone)] : []),
1308
1613
  buildTitle(create, words),
@@ -1333,7 +1638,7 @@ function cardRootOf(node, path) {
1333
1638
  function cardPartsOf(card) {
1334
1639
  return Object.fromEntries(CARD_PARTS.map(part => [part, !!findByRole(card, CARD_PART_ROLE[part])]));
1335
1640
  }
1336
- const imageOf = (media) => media?.children?.find(c => c.type === Type.Image);
1641
+ const imageOf = (media) => media?.children?.find(c => c.type === Type$1.Image);
1337
1642
  /** Put the badge where the layout says: the Media's top corner, or the head of the content column
1338
1643
  * (Image behind text, Compact, or no Media at all). The corner's insets are logical, in iox-design.css
1339
1644
  * (mirrored in RTL); `position` is the node's own, because the editing canvas forces every node's
@@ -1365,7 +1670,7 @@ function applyPartLayout(card, spec, part, baseline) {
1365
1670
  setStack(part, spec.stack?.media, STACK_KEYS.media);
1366
1671
  const image = imageOf(part);
1367
1672
  if (image) {
1368
- writeStyles(image, { ...IMAGE_BASE, ...spec.image });
1673
+ writeStyles(image, { ...IMAGE_BASE$1, ...spec.image });
1369
1674
  setStack(image, spec.stack?.image, STACK_KEYS.image);
1370
1675
  }
1371
1676
  return;
@@ -1537,6 +1842,317 @@ function setCardLinked(card, on) {
1537
1842
  return writeProp(title, LINK_TYPE, next);
1538
1843
  }
1539
1844
 
1845
+ /**
1846
+ * THE ACCESSIBLE CAROUSEL (CC3 — complex-components plan §7c, board 6; board 9 Q13). The Slider's controls are no
1847
+ * longer drawn by Swiper: Previous, Next, Pause and Pagination are REAL nodes with roles — selectable on the canvas,
1848
+ * in the layer tree and the breadcrumb, and styled on their own (R2) — handed to Swiper as its navigation and
1849
+ * pagination elements.
1850
+ *
1851
+ * SliderContainer (.swiper)
1852
+ * ├─ ButtonBlock `button-secondary` role slider.pause FIRST focus stop; present while Autoplay is on, locked
1853
+ * ├─ ButtonBlock `button-secondary` role slider.prev › Icon caret (points the reading way in RTL)
1854
+ * ├─ ButtonBlock `button-secondary` role slider.next › Icon caret
1855
+ * ├─ (the slides — `.swiper-wrapper` › SliderSlide …)
1856
+ * └─ Container role slider.pagination Swiper draws the dots (or "3 / 6") inside it
1857
+ *
1858
+ * The parts render INSIDE the `.swiper` element, never inside `.swiper-wrapper` (slider.md: a `.swiper-slide` must be
1859
+ * the wrapper's direct child): the parts that come before the first slide in the tree render before the wrapper, the
1860
+ * rest after it. By default they sit over the slides like Swiper's own controls (the shared CSS, iox-design.css, at
1861
+ * zero specificity — a part's own styles win), so a saved slider migrated from Swiper-drawn controls keeps its look.
1862
+ *
1863
+ * The Bindings switches stay the source of truth: Navigation arrows ⇔ Previous + Next, Pagination ⇔ Pagination,
1864
+ * Autoplay ⇔ Pause (WCAG 2.2.2 — "added while Autoplay is on, cannot be removed"). `syncSliderParts` is the one edit.
1865
+ */
1866
+ var SliderRole;
1867
+ (function (SliderRole) {
1868
+ SliderRole["Pause"] = "slider.pause";
1869
+ SliderRole["Prev"] = "slider.prev";
1870
+ SliderRole["Next"] = "slider.next";
1871
+ SliderRole["Pagination"] = "slider.pagination";
1872
+ })(SliderRole || (SliderRole = {}));
1873
+ /** Parts in the order they are stamped: the pause control first (APG: the first focus stop), the dots last. */
1874
+ const SLIDER_PART_ROLES = [SliderRole.Pause, SliderRole.Prev, SliderRole.Next, SliderRole.Pagination];
1875
+ const SLIDER_TYPE = 'SliderContainer';
1876
+ const SLIDE_TYPE = 'SliderSlide';
1877
+ /** The Bindings switches that own the parts. */
1878
+ var SliderFlag;
1879
+ (function (SliderFlag) {
1880
+ SliderFlag["Navigation"] = "enableNavigation";
1881
+ SliderFlag["Pagination"] = "enablePagination";
1882
+ SliderFlag["Autoplay"] = "enableAutoplay";
1883
+ })(SliderFlag || (SliderFlag = {}));
1884
+ const FLAG_PARTS = [
1885
+ [SliderFlag.Autoplay, [SliderRole.Pause]],
1886
+ [SliderFlag.Navigation, [SliderRole.Prev, SliderRole.Next]],
1887
+ [SliderFlag.Pagination, [SliderRole.Pagination]],
1888
+ ];
1889
+ /** The trait that names the carousel (Slider ▸ Accessibility ▸ Carousel name) — the A11y trait every named part uses. */
1890
+ const SLIDER_NAME_TRAIT = 'ariaLabel';
1891
+ function isSliderPart(node) {
1892
+ return !!node?.role && SLIDER_PART_ROLES.includes(node.role);
1893
+ }
1894
+ function isSlider(node) {
1895
+ return node?.type === SLIDER_TYPE;
1896
+ }
1897
+ /** The slider's own part playing `role` (a direct child), or undefined. */
1898
+ function sliderPart(slider, role) {
1899
+ return (slider.children ?? []).find(c => c?.role === role);
1900
+ }
1901
+ /** True when the slider's controls are parts (it was created or migrated in CC3) — Swiper then draws none of its own. */
1902
+ function hasSliderParts(slider) {
1903
+ return (slider.children ?? []).some(isSliderPart);
1904
+ }
1905
+ /** A slider's children split as they render: parts before the first slide, the slides (into `.swiper-wrapper`), and
1906
+ * the parts after it. */
1907
+ function splitSliderChildren(children) {
1908
+ const lead = [];
1909
+ const slides = [];
1910
+ const trail = [];
1911
+ for (const child of children ?? []) {
1912
+ if (!isSliderPart(child))
1913
+ slides.push(child);
1914
+ else
1915
+ (slides.length ? trail : lead).push(child);
1916
+ }
1917
+ return { lead, slides, trail };
1918
+ }
1919
+ /** The slides only (the template slide of a bound slider is `sliderSlides(children)[0]`). */
1920
+ function sliderSlides(children) {
1921
+ return (children ?? []).filter(c => !isSliderPart(c));
1922
+ }
1923
+ // ── the parts' recipe ───────────────────────────────────────────────────────────────────────────────
1924
+ var Type;
1925
+ (function (Type) {
1926
+ Type["ButtonBlock"] = "ButtonBlock";
1927
+ Type["Icon"] = "Icon";
1928
+ Type["Container"] = "Container";
1929
+ })(Type || (Type = {}));
1930
+ var Style;
1931
+ (function (Style) {
1932
+ Style["Control"] = "button-secondary";
1933
+ Style["Icon"] = "icon";
1934
+ })(Style || (Style = {}));
1935
+ /** Each part's glyph (Phosphor) and its default accessible name. */
1936
+ const SLIDER_PART_GLYPH = {
1937
+ [SliderRole.Pause]: 'ph ph-pause',
1938
+ [SliderRole.Prev]: 'ph ph-caret-left',
1939
+ [SliderRole.Next]: 'ph ph-caret-right',
1940
+ };
1941
+ const SLIDER_PLAY_GLYPH = 'ph-fill ph-play';
1942
+ const PART_LABEL = {
1943
+ [SliderRole.Pause]: 'Pause',
1944
+ [SliderRole.Prev]: 'Previous',
1945
+ [SliderRole.Next]: 'Next',
1946
+ [SliderRole.Pagination]: 'Pagination',
1947
+ };
1948
+ const ZERO$1 = '0px 0px 0px 0px';
1949
+ /** A round 44px control (board 6 `.st-ctl`), over the slides (z above `.swiper-wrapper`). The PLACE (which edge) is the
1950
+ * shared CSS's, in logical insets, so a carousel reads the same way in RTL. */
1951
+ const CONTROL_BASE = {
1952
+ position: 'absolute', zIndex: '10', display: 'flex', alignItems: 'center', justifyContent: 'center',
1953
+ width: '44px', height: '44px', minWidth: '44px', padding: ZERO$1, margin: ZERO$1, borderRadius: '999px',
1954
+ };
1955
+ const ICON_BASE = { fontSize: '20px', color: 'inherit' };
1956
+ /** The arrows' vertical middle over the slides: half the 44px control above the centre line. */
1957
+ const ARROW_TOP = 'calc(50% - 22px)';
1958
+ const PAGINATION_BASE = {
1959
+ position: 'absolute', zIndex: '10', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px',
1960
+ width: 'auto', minHeight: 'auto', height: 'auto', padding: ZERO$1, margin: ZERO$1, color: 'var(--iox-accent)',
1961
+ };
1962
+ const SWIPER_THEME_COLOR = '#007aff';
1963
+ function legacyLookOf(slider) {
1964
+ const color = readProp(slider, 'navigationColor');
1965
+ const size = Number(readProp(slider, 'navigationSize'));
1966
+ const dots = readProp(slider, 'paginationColor');
1967
+ return {
1968
+ navigationColor: typeof color === 'string' && color ? color : SWIPER_THEME_COLOR,
1969
+ navigationSize: Number.isFinite(size) && size > 0 ? size : 44,
1970
+ paginationColor: typeof dots === 'string' && dots ? dots : SWIPER_THEME_COLOR,
1971
+ };
1972
+ }
1973
+ /** One part, from the recipe (board 6: `button-secondary` controls, token dots) or in a migrated slider's legacy look. */
1974
+ function buildSliderPart(create, role, legacy) {
1975
+ if (role === SliderRole.Pagination) {
1976
+ const dots = makeCompositePart(create, { key: 'pagination', type: Type.Container, props: {}, elementStyle: NO_ELEMENT_STYLE }, role, PART_LABEL[role], legacy?.paginationColor ? { ...PAGINATION_BASE, color: legacy.paginationColor } : PAGINATION_BASE);
1977
+ dots.children = [];
1978
+ return dots;
1979
+ }
1980
+ const key = role.split('.')[1];
1981
+ // Previous / Next sit at the vertical middle (their own `top`, so a part set back in flow drops it); the inline edge
1982
+ // is the shared CSS's, logical, so the arrows swap sides in RTL.
1983
+ const placed = role === SliderRole.Pause ? CONTROL_BASE : { ...CONTROL_BASE, top: ARROW_TOP };
1984
+ const base = legacy
1985
+ ? { ...placed, backgroundColor: 'transparent', borderWidth: ZERO$1, color: legacy.navigationColor ?? SWIPER_THEME_COLOR, boxShadow: 'none' }
1986
+ : placed;
1987
+ // Previous / Next carry their accessible name (Swiper's A11y module reads it); the pause control's name is its
1988
+ // runtime's — it says what pressing it will do.
1989
+ const props = { tag: 'button', type: 'button' };
1990
+ if (role !== SliderRole.Pause)
1991
+ props[SLIDER_NAME_TRAIT] = role === SliderRole.Prev ? SLIDER_WORDS.en.prev : SLIDER_WORDS.en.next;
1992
+ const button = makeCompositePart(create, { key, type: Type.ButtonBlock, props, elementStyle: legacy ? NO_ELEMENT_STYLE : Style.Control }, role, PART_LABEL[role], base);
1993
+ const iconBase = legacy ? { ...ICON_BASE, fontSize: `${Math.round((legacy.navigationSize ?? 44) * 0.6)}px` } : ICON_BASE;
1994
+ const icon = makeCompositePart(create, { key: `${key}-icon`, type: Type.Icon, props: { iconClass: SLIDER_PART_GLYPH[role] }, elementStyle: legacy ? NO_ELEMENT_STYLE : Style.Icon }, null, 'Icon', iconBase);
1995
+ button.children = [icon];
1996
+ return button;
1997
+ }
1998
+ const truthy = (v) => v === true || v === 'true';
1999
+ /** The parts the slider's switches call for, in stamping order. */
2000
+ function wantedSliderParts(slider) {
2001
+ return FLAG_PARTS.filter(([flag]) => truthy(readProp(slider, flag))).flatMap(([, roles]) => roles);
2002
+ }
2003
+ /**
2004
+ * Make the slider's parts match its switches — ONE edit (a palette switch, the migration, the composer). Missing
2005
+ * parts are stamped from the recipe (or, `legacy`, in the migrated look) at their place: the pause control, Previous
2006
+ * and Next before the first slide, the pagination after the last; parts whose switch is off are removed. Everything
2007
+ * else (the slides, the author's edits to an existing part) is kept.
2008
+ */
2009
+ function syncSliderParts(slider, create, legacy) {
2010
+ const wanted = new Set(wantedSliderParts(slider));
2011
+ const children = [...(slider.children ?? [])];
2012
+ const removed = children.filter(c => isSliderPart(c) && !wanted.has(c.role));
2013
+ const kept = children.filter(c => !removed.includes(c));
2014
+ const added = [];
2015
+ for (const role of SLIDER_PART_ROLES) {
2016
+ if (!wanted.has(role) || kept.some(c => c.role === role))
2017
+ continue;
2018
+ const part = buildSliderPart(create, role, legacy);
2019
+ added.push(part);
2020
+ if (role === SliderRole.Pagination) {
2021
+ kept.push(part);
2022
+ }
2023
+ else {
2024
+ // Before the first slide, after the parts that come before it in stamping order.
2025
+ const order = SLIDER_PART_ROLES.indexOf(role);
2026
+ const firstSlide = kept.findIndex(c => !isSliderPart(c));
2027
+ let at = firstSlide < 0 ? kept.length : firstSlide;
2028
+ for (let i = 0; i < kept.length; i++) {
2029
+ const r = kept[i].role;
2030
+ if (isSliderPart(kept[i]) && SLIDER_PART_ROLES.indexOf(r) > order && i < at) {
2031
+ at = i;
2032
+ break;
2033
+ }
2034
+ }
2035
+ kept.splice(at, 0, part);
2036
+ }
2037
+ }
2038
+ if (added.length || removed.length)
2039
+ slider.children = kept;
2040
+ return { added, removed };
2041
+ }
2042
+ /** True when a saved slider still relies on Swiper-drawn controls: a switch is on and no part exists yet. */
2043
+ function needsSliderMigration(slider) {
2044
+ return !!slider && isSlider(slider) && !hasSliderParts(slider) && wantedSliderParts(slider).length > 0;
2045
+ }
2046
+ /**
2047
+ * The migration of a saved slider (board 9 Q13): its Swiper-drawn arrows, dots — and, when Autoplay is on, the pause
2048
+ * control WCAG 2.2.2 requires — become parts, in the place and the colours Swiper drew them. Idempotent: a slider
2049
+ * that already has parts is left alone. True when it changed.
2050
+ */
2051
+ function migrateSliderControls(slider, create) {
2052
+ if (!needsSliderMigration(slider))
2053
+ return false;
2054
+ const change = syncSliderParts(slider, create, legacyLookOf(slider));
2055
+ return change.added.length > 0;
2056
+ }
2057
+ /**
2058
+ * A switch flipped in the panel: on a slider still on Swiper-drawn controls, migrate it first (so its other controls
2059
+ * keep their look), then stamp / remove the parts the new switch calls for. ONE edit.
2060
+ */
2061
+ function setSliderFlag(slider, flag, on, create) {
2062
+ const wasLegacy = !hasSliderParts(slider);
2063
+ const before = wantedSliderParts(slider);
2064
+ const migrated = [];
2065
+ if (wasLegacy && before.length) {
2066
+ syncSliderParts(slider, create, legacyLookOf(slider)).added.forEach(p => migrated.push(p));
2067
+ }
2068
+ writeProp(slider, flag, on);
2069
+ const change = syncSliderParts(slider, create);
2070
+ return { added: [...migrated, ...change.added], removed: change.removed };
2071
+ }
2072
+ const SLIDER_WORDS = {
2073
+ en: {
2074
+ stop: 'Stop automatic slide show', start: 'Start automatic slide show', prev: 'Previous slide', next: 'Next slide',
2075
+ carousel: 'carousel', slide: 'slide', slideLabel: '{{index}} of {{slidesLength}}', goTo: 'Go to slide {{index}}',
2076
+ },
2077
+ he: {
2078
+ stop: 'עצירת המצגת האוטומטית', start: 'הפעלת המצגת האוטומטית', prev: 'השקופית הקודמת', next: 'השקופית הבאה',
2079
+ carousel: 'קרוסלה', slide: 'שקופית', slideLabel: '{{index}} מתוך {{slidesLength}}', goTo: 'מעבר לשקופית {{index}}',
2080
+ },
2081
+ };
2082
+ function sliderWords(lang) {
2083
+ return (lang ?? '').toLowerCase().startsWith('he') ? SLIDER_WORDS.he : SLIDER_WORDS.en;
2084
+ }
2085
+ /** The wrapper's live region (APG): silent while the slides move on their own, polite otherwise. */
2086
+ function sliderLiveRegion(rotating) {
2087
+ return rotating ? 'off' : 'polite';
2088
+ }
2089
+ /**
2090
+ * The pause control's state (APG carousel): its NAME says what it will do ("Stop automatic slide show" while it
2091
+ * rotates, "Start automatic slide show" when stopped). An author who named the control keeps the name, and the
2092
+ * state is `aria-pressed` instead (pressed = stopped).
2093
+ */
2094
+ function sliderPauseAttributes(rotating, words = SLIDER_WORDS.en, authorName) {
2095
+ const name = (authorName ?? '').trim();
2096
+ return name
2097
+ ? { 'aria-label': name, 'aria-pressed': rotating ? 'false' : 'true' }
2098
+ : { 'aria-label': rotating ? words.stop : words.start };
2099
+ }
2100
+ /** The pause control's glyph: pause while it rotates, play when stopped. */
2101
+ function sliderPauseGlyph(rotating) {
2102
+ return rotating ? SLIDER_PART_GLYPH[SliderRole.Pause] : SLIDER_PLAY_GLYPH;
2103
+ }
2104
+ /** Board 6's sample quotes — PLACEHOLDERS: testimonials are facts the studio gives, never invented. */
2105
+ const CAROUSEL_QUOTES = [
2106
+ { quote: 'A sample quote, two lines long, in the client’s own words.', who: '[Client name] · [Project, city]' },
2107
+ { quote: 'Testimonials are facts: replace these with words a client actually said.', who: '[Client name] · [Project, city]' },
2108
+ { quote: 'A third sample quote so the rotation has somewhere to go.', who: '[Client name] · [Project, city]' },
2109
+ ];
2110
+ /** A quote card. The carousel's controls sit in a ROW above the slides and the dots below them (board 6), so the
2111
+ * card needs no room kept for an overlay. */
2112
+ const QUOTE_SLIDE_BASE = {
2113
+ width: '100%', aspectRatio: 'auto', minHeight: 'auto', height: 'auto', display: 'flex', flexDirection: 'column', gap: '12px', padding: '32px 32px 32px 32px',
2114
+ };
2115
+ /** Board 6's carousel: the pause control, Previous and Next in one row above the slides (in flow — they read in the
2116
+ * page's direction), the dots under them. */
2117
+ const ROW_CONTROL_BASE = { position: 'static', display: 'inline-flex', margin: '0px 4px 12px 4px' };
2118
+ const ROW_DOTS_BASE = { position: 'static', margin: '12px 0px 0px 0px' };
2119
+ const QUOTE_TEXT_BASE = { margin: ZERO$1, padding: ZERO$1, minHeight: 'auto', width: 'auto' };
2120
+ /**
2121
+ * The testimonial carousel (board 6): a named Slider, one quote per slide (each a `card`), rotating every 5s with its
2122
+ * control parts — the pause control first, Previous · Next, the dots. ONE recipe for the composer and the builder.
2123
+ */
2124
+ function buildCarouselRecipe(create, options = {}) {
2125
+ const slider = makeCompositePart(create, { key: 'carousel', type: SLIDER_TYPE, props: {}, elementStyle: NO_ELEMENT_STYLE }, null, 'Carousel', { width: '100%' });
2126
+ for (const [name, value] of Object.entries({
2127
+ // Rewind: Previous and Next are always there to press (board 6) — neither greys out at an end.
2128
+ slidesPerView: 1, spaceBetween: 0, loop: 'rewind', enableNavigation: true, enablePagination: true, enableAutoplay: true, autoplayDelay: 5000,
2129
+ autoplayPauseOnMouseEnter: true, autoHeight: true, [SLIDER_NAME_TRAIT]: options.name ?? 'What clients say',
2130
+ }))
2131
+ writeProp(slider, name, value);
2132
+ slider.children = (options.quotes ?? CAROUSEL_QUOTES).map((q, i) => {
2133
+ const slide = makeCompositePart(create, { key: `slide-${i + 1}`, type: SLIDE_TYPE, props: {}, elementStyle: 'card' }, null, 'Slide', QUOTE_SLIDE_BASE);
2134
+ slide.children = [
2135
+ makeCompositePart(create, { key: `quote-${i + 1}`, type: 'Text', props: { content: q.quote, tag: 'p' }, elementStyle: 'lead' }, null, 'Quote', QUOTE_TEXT_BASE),
2136
+ makeCompositePart(create, { key: `who-${i + 1}`, type: 'Text', props: { content: q.who, tag: 'p' }, elementStyle: 'small' }, null, 'Client', QUOTE_TEXT_BASE),
2137
+ ];
2138
+ return slide;
2139
+ });
2140
+ syncSliderParts(slider, create);
2141
+ for (const part of slider.children ?? []) {
2142
+ if (part.role === SliderRole.Pagination)
2143
+ writeStyles(part, ROW_DOTS_BASE);
2144
+ else if (isSliderPart(part))
2145
+ writeStyles(part, ROW_CONTROL_BASE);
2146
+ }
2147
+ return slider;
2148
+ }
2149
+ /** The slide a lightbox opens at for the clicked row (clamped to the slides there are). */
2150
+ function slideIndexForRow(rowIndex, slideCount) {
2151
+ if (!Number.isFinite(rowIndex) || rowIndex < 0 || slideCount <= 0)
2152
+ return 0;
2153
+ return Math.min(Math.floor(rowIndex), slideCount - 1);
2154
+ }
2155
+
1540
2156
  const COMPOSITE_ROLE_SPECS = {
1541
2157
  [CardRole.Card]: { parent: null },
1542
2158
  [CardRole.Media]: { parent: CardRole.Card, part: CardPart.Media },
@@ -1552,6 +2168,20 @@ const COMPOSITE_ROLE_SPECS = {
1552
2168
  [AccordionRole.Trigger]: { parent: AccordionRole.Item, locked: true },
1553
2169
  [AccordionRole.Panel]: { parent: AccordionRole.Item, locked: true },
1554
2170
  [AccordionRole.Icon]: { parent: AccordionRole.Trigger },
2171
+ // CC2.1 — a disclosure always keeps its toggle and its panel; the words and the icon are the author's.
2172
+ [DisclosureRole.Disclosure]: { parent: null },
2173
+ [DisclosureRole.Trigger]: { parent: DisclosureRole.Disclosure, locked: true },
2174
+ [DisclosureRole.Panel]: { parent: DisclosureRole.Disclosure, locked: true },
2175
+ [DisclosureRole.More]: { parent: DisclosureRole.Trigger },
2176
+ [DisclosureRole.Less]: { parent: DisclosureRole.Trigger },
2177
+ [DisclosureRole.Icon]: { parent: DisclosureRole.Trigger },
2178
+ // CC3 — a Slider's controls are its own direct children (Swiper is handed them). The pause control stays while
2179
+ // Autoplay is on (WCAG 2.2.2): the Autoplay switch removes it, never Delete. The arrows and the dots follow
2180
+ // their switches and may be deleted.
2181
+ [SliderRole.Pause]: { parent: null, parentType: SLIDER_TYPE, locked: true },
2182
+ [SliderRole.Prev]: { parent: null, parentType: SLIDER_TYPE, pinned: true },
2183
+ [SliderRole.Next]: { parent: null, parentType: SLIDER_TYPE, pinned: true },
2184
+ [SliderRole.Pagination]: { parent: null, parentType: SLIDER_TYPE, pinned: true },
1555
2185
  };
1556
2186
  var StructureEdit;
1557
2187
  (function (StructureEdit) {
@@ -1572,12 +2202,13 @@ var StructureRefusal;
1572
2202
  */
1573
2203
  function structureRefusal(node, edit, ancestors = []) {
1574
2204
  const spec = node?.role ? COMPOSITE_ROLE_SPECS[node.role] : undefined;
1575
- if (!spec?.locked)
2205
+ if (!spec?.locked && !spec?.pinned)
1576
2206
  return null;
1577
2207
  if (edit === StructureEdit.Delete)
1578
- return StructureRefusal.LockedPart;
2208
+ return spec.locked ? StructureRefusal.LockedPart : null;
1579
2209
  const parent = ancestors[ancestors.length - 1];
1580
- return parent?.role === spec.parent ? null : StructureRefusal.LockedPlacement;
2210
+ const placed = spec.parentType ? parent?.type === spec.parentType : parent?.role === spec.parent;
2211
+ return placed ? null : StructureRefusal.LockedPlacement;
1581
2212
  }
1582
2213
  /** True when `node` is a locked part (shown with a lock and no delete in the layer tree). Deleting a whole
1583
2214
  * composite is always allowed — only its locked parts are refused. */
@@ -1585,12 +2216,322 @@ function isLockedPart(node) {
1585
2216
  return !!(node?.role && COMPOSITE_ROLE_SPECS[node.role]?.locked);
1586
2217
  }
1587
2218
 
2219
+ const SLIDE_VISIBLE = 'swiper-slide-visible';
2220
+ const RUNTIME_KEY = '__ioxCarousel';
2221
+ const ROTATION_EVENTS = 'autoplayStart autoplayStop autoplayPause autoplayResume';
2222
+ const SLIDE_EVENTS = 'slideChange transitionEnd resize slidesLengthChange';
2223
+ const AUTHOR_NAME_KEY = '__ioxAuthorName';
2224
+ /** The name the AUTHOR gave the pause control (its rendered `aria-label` before this runtime first relabelled it). */
2225
+ function authorNameOf(el) {
2226
+ if (!el)
2227
+ return null;
2228
+ const host = el;
2229
+ if (host[AUTHOR_NAME_KEY] === undefined)
2230
+ host[AUTHOR_NAME_KEY] = el.getAttribute('aria-label') ?? '';
2231
+ return host[AUTHOR_NAME_KEY] || null;
2232
+ }
2233
+ /** The part of `role` that belongs to THIS slider (never one of a slider nested in a slide). */
2234
+ function sliderPartElement(sliderEl, role) {
2235
+ const cls = `iox-role-${role.replace('.', '-')}`;
2236
+ for (const el of Array.from(sliderEl.querySelectorAll(`.${cls}`))) {
2237
+ if (el.closest('.swiper') === sliderEl && !el.closest('.swiper-wrapper'))
2238
+ return el;
2239
+ }
2240
+ return null;
2241
+ }
2242
+ class CarouselA11y {
2243
+ constructor(swiper, options = {}) {
2244
+ this.swiper = swiper;
2245
+ /** The visitor pressed pause: rotation stays stopped until they press it again. */
2246
+ this.stoppedByVisitor = false;
2247
+ this.focusInside = false;
2248
+ /** The pointer is over the carousel and Swiper holds rotation for it (`pauseOnMouseEnter`). */
2249
+ this.hovering = false;
2250
+ this.cleanups = [];
2251
+ const el = swiper.el;
2252
+ this.words = options.words ?? sliderWords(el.ownerDocument?.documentElement?.getAttribute('lang'));
2253
+ this.pause = sliderPartElement(el, SliderRole.Pause);
2254
+ this.authorName = authorNameOf(this.pause);
2255
+ this.listen(el, 'focusin', () => this.onFocusIn());
2256
+ this.listen(el, 'pointerenter', () => { this.hovering = this.hoverPauses; this.sync(); });
2257
+ this.listen(el, 'pointerleave', () => { this.hovering = false; this.sync(); });
2258
+ this.listen(el, 'focusout', (e) => this.onFocusOut(e));
2259
+ if (this.pause)
2260
+ this.listen(this.pause, 'click', (e) => { e.preventDefault(); this.togglePause(); });
2261
+ const sync = () => this.sync();
2262
+ swiper.on(ROTATION_EVENTS, sync);
2263
+ swiper.on(SLIDE_EVENTS, sync);
2264
+ this.cleanups.push(() => { swiper.off(ROTATION_EVENTS, sync); swiper.off(SLIDE_EVENTS, sync); });
2265
+ el[RUNTIME_KEY] = this;
2266
+ this.sync();
2267
+ }
2268
+ /** True while the slides move on their own: autoplay runs and nothing holds it — not focus inside, not the
2269
+ * pointer over it. (Swiper's own `paused` also flips during every transition, so it is not read here.) */
2270
+ get rotating() {
2271
+ return !!this.swiper.autoplay?.running && !this.focusInside && !this.hovering;
2272
+ }
2273
+ get hoverPauses() {
2274
+ const autoplay = this.swiper.params?.autoplay;
2275
+ return typeof autoplay === 'object' && !!autoplay?.pauseOnMouseEnter;
2276
+ }
2277
+ /** The pause control: stop rotation for good, or start it again. */
2278
+ togglePause() {
2279
+ const a = this.swiper.autoplay;
2280
+ if (!a)
2281
+ return;
2282
+ if (a.running) {
2283
+ this.stoppedByVisitor = true;
2284
+ a.stop();
2285
+ }
2286
+ else {
2287
+ this.stoppedByVisitor = false;
2288
+ a.start();
2289
+ // Focus is on the pause control itself — inside the carousel: the rotation it just started waits until
2290
+ // focus leaves (APG), but the control already reads "stop".
2291
+ if (this.focusInside)
2292
+ a.pause();
2293
+ }
2294
+ this.sync();
2295
+ }
2296
+ destroy() {
2297
+ this.cleanups.splice(0).forEach(fn => fn());
2298
+ delete this.swiper.el[RUNTIME_KEY];
2299
+ }
2300
+ listen(target, type, fn) {
2301
+ target.addEventListener(type, fn);
2302
+ this.cleanups.push(() => target.removeEventListener(type, fn));
2303
+ }
2304
+ onFocusIn() {
2305
+ this.focusInside = true;
2306
+ if (this.swiper.autoplay?.running && !this.swiper.autoplay.paused)
2307
+ this.swiper.autoplay.pause();
2308
+ this.sync();
2309
+ }
2310
+ onFocusOut(e) {
2311
+ const next = e.relatedTarget;
2312
+ if (next && this.swiper.el.contains(next))
2313
+ return;
2314
+ this.focusInside = false;
2315
+ if (!this.stoppedByVisitor && this.swiper.autoplay?.running && this.swiper.autoplay.paused)
2316
+ this.swiper.autoplay.resume();
2317
+ this.sync();
2318
+ }
2319
+ /** Re-state the carousel: the live region, the pause control, the slides out of view. */
2320
+ sync() {
2321
+ if (this.swiper.destroyed)
2322
+ return;
2323
+ const rotating = this.rotating;
2324
+ this.swiper.wrapperEl?.setAttribute('aria-live', sliderLiveRegion(rotating));
2325
+ if (this.pause) {
2326
+ const running = !!this.swiper.autoplay?.running;
2327
+ const attrs = sliderPauseAttributes(running, this.words, this.authorName);
2328
+ for (const [name, value] of Object.entries(attrs))
2329
+ this.pause.setAttribute(name, value);
2330
+ if (!('aria-pressed' in attrs))
2331
+ this.pause.removeAttribute('aria-pressed');
2332
+ const icon = this.pause.querySelector('i');
2333
+ if (icon)
2334
+ icon.className = `${sliderPauseGlyph(running)} ${Array.from(icon.classList).filter(c => !c.startsWith('ph')).join(' ')}`.trim();
2335
+ }
2336
+ this.syncHiddenSlides();
2337
+ }
2338
+ syncHiddenSlides() {
2339
+ const slides = this.swiper.slides ?? [];
2340
+ const anyVisibleMarked = slides.some(s => s.classList?.contains(SLIDE_VISIBLE));
2341
+ slides.forEach((slide, i) => {
2342
+ const visible = anyVisibleMarked ? slide.classList.contains(SLIDE_VISIBLE) : i === this.swiper.activeIndex;
2343
+ if (visible) {
2344
+ slide.removeAttribute('inert');
2345
+ slide.removeAttribute('aria-hidden');
2346
+ }
2347
+ else {
2348
+ slide.setAttribute('inert', '');
2349
+ slide.setAttribute('aria-hidden', 'true');
2350
+ }
2351
+ });
2352
+ }
2353
+ }
2354
+ /** The carousel runtime attached to a slider element, if any. */
2355
+ function carouselOf(el) {
2356
+ return el?.[RUNTIME_KEY] ?? null;
2357
+ }
2358
+ /** Attach once per Swiper (a re-init gets a fresh runtime). */
2359
+ function attachCarouselA11y(swiper, options = {}) {
2360
+ carouselOf(swiper.el)?.destroy();
2361
+ return new CarouselA11y(swiper, options);
2362
+ }
2363
+
2364
+ /**
2365
+ * THE LIGHTBOX (CC3 — complex-components plan §7d, board 6). Not a component: a Dialog holding a Slider bound to the
2366
+ * SAME source as the grid of thumbnails. The one new thing is `openDialog`'s "Open at the clicked item"
2367
+ * (`slideTo: 'row'`): a thumbnail in a repeater row opens the lightbox at ITS slide.
2368
+ *
2369
+ * Dialog `dialog` · centre · full viewport · Inverse scheme "Lightbox" (a free-standing Dialog: board A4)
2370
+ * ├─ ButtonBlock `button-ghost` "Close" › Icon × click → closeDialog
2371
+ * └─ SliderContainer one slide in view · keyboard · the source's rows
2372
+ * ├─ Previous · Next (slider.prev / slider.next, button-secondary) hidden on a phone (swipe)
2373
+ * ├─ SliderSlide › Image `media` (contain, keeps its alt) + Text `small` caption
2374
+ * └─ Pagination (slider.pagination) — the counter "3 / 6"
2375
+ *
2376
+ * Keyboard ← → are the Slider's Keyboard module; Esc and the focus trap are the `<dialog>`'s; focus returns to the
2377
+ * thumbnail that opened it (the dialog opener's own rule).
2378
+ */
2379
+ const SLIDE_TO_ROW = 'row';
2380
+ /** The attribute a renderer writes on every repeated row (the context-state provenance, both engine paths, the canvas). */
2381
+ const ROW_INDEX_ATTRIBUTE = 'data-iox-row-idx';
2382
+ const LIGHTBOX_LABEL = 'Lightbox';
2383
+ const DIALOG = 'Dialog';
2384
+ const REPEATER = 'Repeater';
2385
+ const SOURCE = 'source';
2386
+ const SOURCE_PATH = 'sourcePath';
2387
+ const ZERO = '0px 0px 0px 0px';
2388
+ const DIALOG_BASE = {
2389
+ width: '100vw', maxWidth: '100vw', height: '100dvh', maxHeight: '100dvh', borderRadius: '0px', padding: '18px 18px 18px 18px',
2390
+ display: 'flex', flexDirection: 'column', gap: '0px',
2391
+ };
2392
+ /** The top row (board 6): the counter at its start, Close at its end — Close sits above the slider, which rises into
2393
+ * the row so its counter shares it. */
2394
+ const CLOSE_BASE = {
2395
+ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '44px', height: '44px', minWidth: '44px',
2396
+ padding: ZERO, margin: ZERO, borderRadius: '999px', alignSelf: 'flex-end', flexShrink: '0', position: 'relative', zIndex: '20',
2397
+ };
2398
+ const SLIDER_BASE = { width: '100%', height: '100%', minHeight: '0px', flexGrow: '1', margin: '-44px 0px 0px 0px' };
2399
+ const SLIDE_BASE = {
2400
+ width: '100%', height: '100%', aspectRatio: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'center',
2401
+ justifyContent: 'center', gap: '12px', padding: '56px 64px 16px 64px',
2402
+ };
2403
+ const IMAGE_BASE = { width: '100%', height: 'auto', maxHeight: '72dvh', minHeight: '0px', objectFit: 'contain', aspectRatio: '4 / 3', flexShrink: '1' };
2404
+ const CAPTION_BASE = { textAlign: 'center', margin: ZERO, padding: ZERO, minHeight: 'auto', width: 'auto' };
2405
+ /** The counter "3 / 6" sits at the START of the top row, Close at its end (board 6). */
2406
+ const COUNTER_BASE = { color: 'var(--iox-text-muted)', top: '12px', alignItems: 'flex-start', justifyContent: 'flex-start' };
2407
+ /** On a phone the arrows give way to a swipe (board 6, "the arrows hide on touch-only widths"). */
2408
+ const LIGHTBOX_ARROWS_HIDE_AT = 'mobile';
2409
+ /** A click on `node` runs one instant action on `target`. */
2410
+ function onClick(node, type, target, params) {
2411
+ node.interactions = [{
2412
+ id: `${node.id ?? 'node'}-click`, trigger: 'click',
2413
+ actions: [{ type, target, duration: 0, delay: 0, easing: 'ease', ...(params ? { params } : {}) }],
2414
+ }];
2415
+ }
2416
+ /** A binding of `prop` to the row field `field` of `source` (`item` = the row itself). */
2417
+ function bindRow(node, prop, source, field) {
2418
+ const bindings = (node.bindings ?? []).filter((b) => b.prop !== prop);
2419
+ bindings.push({ prop, source, path: field === 'item' ? '' : field });
2420
+ node.bindings = bindings;
2421
+ }
2422
+ function buildSlide(create, key, options, rowAlias) {
2423
+ const slide = makeCompositePart(create, { key, type: SLIDE_TYPE, props: {}, elementStyle: NO_ELEMENT_STYLE }, null, 'Slide', SLIDE_BASE);
2424
+ const image = makeCompositePart(create, { key: `${key}-image`, type: 'Image', props: { src: '', alt: '' }, elementStyle: 'media' }, null, 'Image', IMAGE_BASE);
2425
+ const caption = makeCompositePart(create, { key: `${key}-caption`, type: 'Text', props: { content: 'Caption', tag: 'p' }, elementStyle: 'small' }, null, 'Caption', CAPTION_BASE);
2426
+ if (rowAlias) {
2427
+ bindRow(image, 'src', rowAlias, options.imageField ?? 'item');
2428
+ if (options.altField)
2429
+ bindRow(image, 'alt', rowAlias, options.altField);
2430
+ if (options.captionField)
2431
+ bindRow(caption, 'content', rowAlias, options.captionField);
2432
+ }
2433
+ slide.children = options.captionField || !rowAlias ? [image, caption] : [image];
2434
+ return slide;
2435
+ }
2436
+ /** A fresh lightbox: the Dialog, its Close part and the Slider (bound to `source`, or three manual slides). */
2437
+ function buildLightboxRecipe(create, options = {}) {
2438
+ const dialog = makeCompositePart(create, { key: 'lightbox', type: DIALOG, props: {}, elementStyle: 'dialog' }, null, LIGHTBOX_LABEL, DIALOG_BASE);
2439
+ writeProp(dialog, 'placement', 'center');
2440
+ writeProp(dialog, 'closeOnBackdrop', true);
2441
+ writeProp(dialog, 'showCloseButton', false);
2442
+ writeProp(dialog, 'ariaLabel', 'Gallery');
2443
+ setColorScheme(dialog, 'inverse');
2444
+ const close = makeCompositePart(create, { key: 'close', type: 'ButtonBlock', props: { tag: 'button', type: 'button', ariaLabel: 'Close' }, elementStyle: 'button-ghost' }, null, 'Close', CLOSE_BASE);
2445
+ close.children = [makeCompositePart(create, { key: 'close-icon', type: 'Icon', props: { iconClass: 'ph ph-x' }, elementStyle: 'icon' }, null, 'Icon', { fontSize: '22px', color: 'inherit' })];
2446
+ onClick(close, 'closeDialog', dialog.id ?? '');
2447
+ const slider = makeCompositePart(create, { key: 'slider', type: SLIDER_TYPE, props: {}, elementStyle: NO_ELEMENT_STYLE }, null, 'Slider', SLIDER_BASE);
2448
+ for (const [name, value] of Object.entries({
2449
+ slidesPerView: 1, spaceBetween: 0, loop: 'off', enableNavigation: true, enablePagination: true, paginationType: 'fraction',
2450
+ enableKeyboard: true, ariaLabel: 'Gallery', limit: 50,
2451
+ }))
2452
+ writeProp(slider, name, value);
2453
+ const rowAlias = options.source ? lastSegment(options.sourcePath) || options.source : null;
2454
+ if (options.source) {
2455
+ writeProp(slider, SOURCE, options.source);
2456
+ if (options.sourcePath)
2457
+ writeProp(slider, SOURCE_PATH, options.sourcePath);
2458
+ }
2459
+ const prev = buildSliderPart(create, SliderRole.Prev);
2460
+ const next = buildSliderPart(create, SliderRole.Next);
2461
+ for (const arrow of [prev, next]) {
2462
+ const layers = { ...(arrow.responsive ?? {}) };
2463
+ layers[LIGHTBOX_ARROWS_HIDE_AT] = { styleProps: { display: 'none' } };
2464
+ arrow.responsive = layers;
2465
+ }
2466
+ const counter = buildSliderPart(create, SliderRole.Pagination);
2467
+ counter.label = 'Counter';
2468
+ writeStyles(counter, COUNTER_BASE);
2469
+ const slides = rowAlias ? [buildSlide(create, 'slide', options, rowAlias)] : [1, 2, 3].map(n => buildSlide(create, `slide-${n}`, options, null));
2470
+ slider.children = [prev, next, ...slides, counter];
2471
+ dialog.children = [close, slider];
2472
+ return dialog;
2473
+ }
2474
+ const lastSegment = (path) => (path ? String(path).split('.').filter(Boolean).pop() ?? '' : '');
2475
+ /** What a Repeater (its `source` binding) or a bound Slider (its `source` / `sourcePath` traits) repeats over. */
2476
+ function repeatSourceOf(node) {
2477
+ if (!node || (node.type !== REPEATER && node.type !== SLIDER_TYPE))
2478
+ return null;
2479
+ const binding = (node.bindings ?? []).find(b => b?.prop === SOURCE);
2480
+ if (binding?.source)
2481
+ return { source: binding.source, path: binding.path ?? '' };
2482
+ const source = readProp(node, SOURCE);
2483
+ if (typeof source !== 'string' || !source)
2484
+ return null;
2485
+ const path = readProp(node, SOURCE_PATH);
2486
+ return { source, path: typeof path === 'string' ? path : '' };
2487
+ }
2488
+ function sameRepeatSource(a, b) {
2489
+ return !!a && !!b && a.source === b.source && (a.path ?? '') === (b.path ?? '');
2490
+ }
2491
+ /** The Slider a lightbox Dialog holds (the first one, depth first), or null. */
2492
+ function lightboxSliderOf(dialog) {
2493
+ for (const child of dialog?.children ?? []) {
2494
+ if (child.type === SLIDER_TYPE)
2495
+ return child;
2496
+ const below = lightboxSliderOf(child);
2497
+ if (below)
2498
+ return below;
2499
+ }
2500
+ return null;
2501
+ }
2502
+ /** The repeated rows the element is in: the nearest Repeater or bound Slider among its ancestors (`path` = root → the
2503
+ * element, inclusive). */
2504
+ function rowSourceOf(path) {
2505
+ for (let i = path.length - 2; i >= 0; i--) {
2506
+ const source = repeatSourceOf(path[i]);
2507
+ if (source)
2508
+ return source;
2509
+ }
2510
+ return null;
2511
+ }
2512
+ /**
2513
+ * Interactions ▸ openDialog ▸ "Open at the clicked item" is offered only when it can work (board 9): the clicked
2514
+ * element sits in a repeater row AND the target Dialog holds a Slider bound to the same source.
2515
+ */
2516
+ function canOpenAtClickedItem(ownerPath, dialog) {
2517
+ if (!dialog || dialog.type !== DIALOG)
2518
+ return false;
2519
+ return sameRepeatSource(rowSourceOf(ownerPath), repeatSourceOf(lightboxSliderOf(dialog)));
2520
+ }
2521
+ /** The repeated row an element sits in (its row index), 0 when it is in none (the canvas' template row). */
2522
+ function rowIndexOf(el) {
2523
+ const row = el?.closest?.(`[${ROW_INDEX_ATTRIBUTE}]`);
2524
+ const index = Number(row?.getAttribute(ROW_INDEX_ATTRIBUTE));
2525
+ return Number.isInteger(index) && index >= 0 ? index : 0;
2526
+ }
2527
+
1588
2528
  /**
1589
2529
  * @vectoriox/iox-ui/composite-core — composites built from the builder's own parts (composite-components
1590
2530
  * plan §3a). FRAMEWORK-FREE: the builder, the SSR engine and iox-cms (generation-core's composer, the site
1591
2531
  * build) read the same recipes, roles and structure rules. Card v2 + Badge / Chip (T23 / T25) are the
1592
2532
  * first; the Accordion (CC2) adds the first composite RUNTIME (accordion-runtime.ts — DOM only, no
1593
- * framework); Tabs join with their own phase.
2533
+ * framework), which the Disclosure (CC2.1) runs too — a one-item accordion whose root is its item; Tabs join with
2534
+ * their own phase.
1594
2535
  * ⚠️ Consumers wire the usual trio — ng-package raw-ts assets, jest moduleNameMapper, tsconfig paths.
1595
2536
  */
1596
2537
 
@@ -1598,5 +2539,5 @@ function isLockedPart(node) {
1598
2539
  * Generated bundle index. Do not edit.
1599
2540
  */
1600
2541
 
1601
- export { ACCORDION_BIND_PROP, ACCORDION_CONTENT_LIMIT, ACCORDION_DEFAULT_ONS, ACCORDION_ICONS, ACCORDION_ICON_GLYPH, ACCORDION_NEW_ITEM_WORDS, ACCORDION_WORDS, AccordionClass, AccordionDefaultOn, AccordionEditRefusal, AccordionIcon, AccordionKeyAction, AccordionRole, AccordionRuntime, 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_ATTRIBUTE, COMPOSITE_ROLE_SPECS, CardLayout, CardPart, CardRole, CompositeBehaviourTrait, DEFAULT_ON_ATTRIBUTE, ITEM_KEY_ATTRIBUTE, ITEM_OFF_EVENT, ITEM_ON_EVENT, ITEM_OPEN_STATE, MULTIPLE_ATTRIBUTE, ON_ATTRIBUTE, ON_STATE, PILL_LABEL_STYLE, StructureEdit, StructureRefusal, UNTIL_FOUND, accordionAnswerOf, accordionBehaviour, accordionDomIds, accordionFocusIndex, accordionHeadingLevel, accordionItemAttributes, accordionItemLabel, accordionItemOf, accordionItemState, accordionItems, accordionKeyAction, accordionPanelAttributes, accordionPanelOf, accordionPartStyle, accordionQuestionOf, accordionRegionAttributes, accordionRemovalRefusal, accordionRootAttributes, accordionRootOf, accordionScope, accordionToContent, accordionToManual, accordionTrackAttributes, accordionTriggerAttributes, accordionTriggerOf, addAccordionItem, applyCardLayout, attachAccordionAt, attachAccordions, badgeStyleId, badgeToneOf, buildAccordionItem, buildAccordionRecipe, buildBadge, buildCardRecipe, buildChip, cardPartsOf, cardRootOf, cardTitleOf, clearLayerStyles, closestAccordion, closestAccordionItem, detachAccordion, detectAccordionIcon, detectCardLayout, domSafeKey, findByRole, initialAccordionOpen, isAccordionFromContent, isAccordionItemOn, isAccordionPart, isAccordionRoot, isCardLinked, isCardRoot, isLockedPart, isPillNode, itemOpenTriggerIds, moveAccordionItem, nextAccordionState, nextItemKey, onAccordionItemState, openAccordionItem, parentInComposite, pickAccordionAutoBinds, readLayerStyle, readProp, readStyle, removeFromComposite, renameAccordionItem, rowItemKey, serializeDomAttributes, setAccordionBehaviour, setAccordionIcon, setBadgeTone, setCardLinked, setCardPart, structureRefusal, walkComposite, writeLayerStyles, writeProp, writeStyles };
2542
+ export { ACCORDION_BIND_PROP, ACCORDION_CONTENT_LIMIT, ACCORDION_DEFAULT_ONS, ACCORDION_ICONS, ACCORDION_ICON_GLYPH, ACCORDION_NEW_ITEM_WORDS, ACCORDION_WORDS, AccordionClass, AccordionDefaultOn, AccordionEditRefusal, AccordionIcon, AccordionKeyAction, AccordionRole, AccordionRuntime, BADGE_TONES, BADGE_TONE_ICONS, BadgeTone, CARD_LAYOUTS, CARD_LAYOUT_CUSTOM, CARD_PARTS, CARD_PART_ROLE, CARD_STACK_BREAKPOINT, CARD_WORDS, CAROUSEL_QUOTES, CHIP_ICON, CHIP_STYLE, COMPOSITE_ATTRIBUTE, COMPOSITE_KIND_ROLES, COMPOSITE_ROLE_SPECS, CardLayout, CardPart, CardRole, CarouselA11y, CompositeBehaviourTrait, CompositeItemAction, CompositeKind, DEFAULT_ON_ATTRIBUTE, DISCLOSURE_ITEM_KEY, DISCLOSURE_WORDS, DisclosureRole, ITEM_KEY_ATTRIBUTE, ITEM_OFF_EVENT, ITEM_ON_EVENT, ITEM_OPEN_STATE, LIGHTBOX_ARROWS_HIDE_AT, LIGHTBOX_LABEL, MULTIPLE_ATTRIBUTE, NO_ELEMENT_STYLE, ON_ATTRIBUTE, ON_STATE, PILL_LABEL_STYLE, ROW_INDEX_ATTRIBUTE, SCOPE_ATTRIBUTE, SLIDER_NAME_TRAIT, SLIDER_PART_GLYPH, SLIDER_PART_ROLES, SLIDER_PLAY_GLYPH, SLIDER_TYPE, SLIDER_WORDS, SLIDE_TO_ROW, SLIDE_TYPE, SWIPER_THEME_COLOR, SliderFlag, SliderRole, StructureEdit, StructureRefusal, UNTIL_FOUND, accordionAnswerOf, accordionBehaviour, accordionDomIds, accordionFocusIndex, accordionHeadingLevel, accordionItemAttributes, accordionItemLabel, accordionItemOf, accordionItemState, accordionItems, accordionKeyAction, accordionPanelAttributes, accordionPanelOf, accordionPartStyle, accordionQuestionOf, accordionRegionAttributes, accordionRemovalRefusal, accordionRootAttributes, accordionRootOf, accordionRuntimeOf, accordionScope, accordionToContent, accordionToManual, accordionTrackAttributes, accordionTriggerAttributes, accordionTriggerOf, addAccordionItem, applyCardLayout, attachAccordionAt, attachAccordions, attachCarouselA11y, badgeStyleId, badgeToneOf, buildAccordionItem, buildAccordionRecipe, buildBadge, buildCardRecipe, buildCarouselRecipe, buildChip, buildCompositeIcon, buildDisclosureRecipe, buildLightboxRecipe, buildSliderPart, canOpenAtClickedItem, cardPartsOf, cardRootOf, cardTitleOf, carouselOf, clearLayerStyles, closeAccordionItem, closestAccordion, closestAccordionItem, compositeItemOf, compositeItems, compositeKindOfRole, compositeRootOf, compositeRowKeys, compositeScopeId, detachAccordion, detectAccordionIcon, detectCardLayout, disclosureOpenByDefault, domSafeKey, findByRole, hasLabelledRegion, hasSliderParts, initialAccordionOpen, isAccordionFromContent, isAccordionItemOn, isAccordionPart, isAccordionRoot, isCardLinked, isCardRoot, isCompositeItem, isCompositePanel, isCompositeRoot, isCompositeTrigger, isDisclosurePart, isDisclosureRoot, isLockedPart, isPillNode, isSlider, isSliderPart, itemOpenTriggerIds, lightboxSliderOf, makeCompositePart, migrateSliderControls, moveAccordionItem, needsSliderMigration, nextAccordionState, nextItemKey, onAccordionItemState, openAccordionItem, parentInComposite, pickAccordionAutoBinds, readLayerStyle, readProp, readStyle, removeFromComposite, renameAccordionItem, repeatSourceOf, rowIndexOf, rowItemKey, rowSourceOf, runCompositeItemAction, sameRepeatSource, serializeDomAttributes, setAccordionBehaviour, setAccordionIcon, setBadgeTone, setCardLinked, setCardPart, setSliderFlag, shouldScrollToItem, slideIndexForRow, sliderLiveRegion, sliderPart, sliderPartElement, sliderPauseAttributes, sliderPauseGlyph, sliderSlides, sliderWords, splitSliderChildren, structureRefusal, syncSliderParts, walkComposite, wantedSliderParts, writeLayerStyles, writeProp, writeStyles };
1602
2543
  //# sourceMappingURL=vectoriox-iox-ui-composite-core.mjs.map