@vectoriox/iox-ui 5.37.0 → 5.39.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 (45) hide show
  1. package/actions-core/index.d.ts +6 -1
  2. package/composite-core/index.d.ts +684 -28
  3. package/design-core/index.d.ts +452 -6
  4. package/fesm2022/vectoriox-iox-ui-actions-core.mjs +12 -4
  5. package/fesm2022/vectoriox-iox-ui-actions-core.mjs.map +1 -1
  6. package/fesm2022/vectoriox-iox-ui-composite-core.mjs +1761 -143
  7. package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -1
  8. package/fesm2022/vectoriox-iox-ui-design-core.mjs +555 -6
  9. package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
  10. package/fesm2022/vectoriox-iox-ui-generation-core.mjs +534 -12
  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 +494 -127
  15. package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
  16. package/generation-core/index.d.ts +210 -4
  17. package/index.d.ts +217 -30
  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/element-actions.ts +12 -3
  22. package/src/composite-core/accordion-runtime.ts +3 -15
  23. package/src/composite-core/accordion.ts +4 -2
  24. package/src/composite-core/composite-kinds.ts +121 -7
  25. package/src/composite-core/composite-node.ts +4 -0
  26. package/src/composite-core/composite-runtime.ts +68 -0
  27. package/src/composite-core/exclusive-runtime.ts +358 -0
  28. package/src/composite-core/lightbox.ts +192 -0
  29. package/src/composite-core/public-api.ts +8 -2
  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 +38 -5
  33. package/src/composite-core/tabs.ts +628 -0
  34. package/src/design-core/contrast-audit.ts +5 -0
  35. package/src/design-core/element-styles.ts +34 -3
  36. package/src/design-core/flavor-spec.generated.ts +517 -2
  37. package/src/generation-core/baseline-roles.ts +2 -0
  38. package/src/generation-core/compose-site.ts +147 -4
  39. package/src/generation-core/node-baselines.generated.ts +156 -6
  40. package/src/generation-core/patterns.ts +20 -0
  41. package/src/generation-core/site-spec.generated.ts +210 -0
  42. package/src/media-core/public-api.ts +9 -0
  43. package/src/media-core/video-runtime.ts +239 -0
  44. package/src/media-core/video.ts +443 -0
  45. package/styles/iox-design.css +112 -0
@@ -1,5 +1,12 @@
1
- import { readOwnedTrait, writeOwnedTrait, elementStyleById, switchElementStyle, LinkType, setColorScheme, COLOR_SCHEME_TRAIT } from '@vectoriox/iox-ui/design-core';
1
+ import { readOwnedTrait, writeOwnedTrait, elementStyleById, switchElementStyle, LinkType, setColorScheme, COLOR_SCHEME_TRAIT, roleClass } 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 = '';
3
10
  /** A recipe part: a NEW node (`create`) playing `role` under `label`, with the layout `base` styles its recipe
4
11
  * gives it — its LOOK is its element style. Shared by every recipe (Accordion, Disclosure). */
5
12
  function makeCompositePart(create, spec, role, label, base) {
@@ -118,15 +125,18 @@ function removeFromComposite(root, node) {
118
125
  }
119
126
 
120
127
  /**
121
- * The open/close composites' kinds and roles (CC2 Accordion, CC2.1 Disclosure — composite plan T9 / T21). ONE
122
- * family: a Disclosure is a one-item accordion whose ROOT is its item, so both run the same state machine, the
123
- * same attribute planners and the same runtime (`accordion-runtime.ts`). Every consumer that treats "an item",
124
- * "a header" or "a panel" alike asks these predicates, never a role literal.
128
+ * The composites' kinds and roles. Two families:
129
+ * - OPEN/CLOSE (CC2 Accordion, CC2.1 Disclosure — composite plan T9 / T21): a Disclosure is a one-item accordion whose
130
+ * ROOT is its item, so both run the same state machine, attribute planners and runtime (`accordion-runtime.ts`);
131
+ * - EXCLUSIVE (CC4 Tabs — composite T17; the Segmented control joins in CC4b): exactly one item is on, a trigger and
132
+ * its panel paired by `itemKey` (`tabs.ts`, `exclusive-runtime.ts`).
133
+ * Every consumer that treats "an item", "a trigger" or "a panel" alike asks these predicates, never a role literal.
125
134
  */
126
135
  var CompositeKind;
127
136
  (function (CompositeKind) {
128
137
  CompositeKind["Accordion"] = "accordion";
129
138
  CompositeKind["Disclosure"] = "disclosure";
139
+ CompositeKind["Tabs"] = "tabs";
130
140
  })(CompositeKind || (CompositeKind = {}));
131
141
  var AccordionRole;
132
142
  (function (AccordionRole) {
@@ -150,21 +160,53 @@ var DisclosureRole;
150
160
  DisclosureRole["Icon"] = "disclosure.icon";
151
161
  DisclosureRole["Panel"] = "disclosure.panel";
152
162
  })(DisclosureRole || (DisclosureRole = {}));
163
+ /**
164
+ * TABS (CC4a — composite T17). The tab LIST holds the triggers, the PANELS holder holds the panels; a tab and its panel
165
+ * share an `itemKey` (never their index — moving a tab keeps its own panel). From content (R3) the list and the panels
166
+ * holder are both Repeaters over the SAME source, one row giving its tab AND its panel (paired by the row's id).
167
+ */
168
+ var TabsRole;
169
+ (function (TabsRole) {
170
+ TabsRole["Tabs"] = "tabs";
171
+ TabsRole["List"] = "tabs.list";
172
+ TabsRole["Trigger"] = "tabs.trigger";
173
+ TabsRole["Panels"] = "tabs.panels";
174
+ TabsRole["Panel"] = "tabs.panel";
175
+ })(TabsRole || (TabsRole = {}));
153
176
  const COMPOSITE_KIND_ROLES = {
154
177
  [CompositeKind.Accordion]: { root: AccordionRole.Accordion, item: AccordionRole.Item, trigger: AccordionRole.Trigger, panel: AccordionRole.Panel, icon: AccordionRole.Icon },
155
178
  [CompositeKind.Disclosure]: { root: DisclosureRole.Disclosure, item: DisclosureRole.Disclosure, trigger: DisclosureRole.Trigger, panel: DisclosureRole.Panel, icon: DisclosureRole.Icon },
156
179
  };
180
+ const EXCLUSIVE_KIND_ROLES = {
181
+ [CompositeKind.Tabs]: { root: TabsRole.Tabs, list: TabsRole.List, trigger: TabsRole.Trigger, panels: TabsRole.Panels, panel: TabsRole.Panel },
182
+ };
157
183
  const KINDS = Object.values(CompositeKind);
158
- const rolesOf = (pick) => new Set(KINDS.map(k => COMPOSITE_KIND_ROLES[k][pick]));
184
+ const TOGGLE_KINDS = Object.keys(COMPOSITE_KIND_ROLES);
185
+ const EXCLUSIVE_KINDS$1 = Object.keys(EXCLUSIVE_KIND_ROLES);
186
+ const rolesOf = (pick) => new Set(TOGGLE_KINDS.map(k => COMPOSITE_KIND_ROLES[k][pick]));
187
+ const exclusiveRolesOf = (pick) => new Set(EXCLUSIVE_KINDS$1.map(k => EXCLUSIVE_KIND_ROLES[k][pick]).filter((r) => !!r));
159
188
  const ROOT_ROLES = rolesOf('root');
160
189
  const ITEM_ROLES = rolesOf('item');
161
190
  const TRIGGER_ROLES = rolesOf('trigger');
162
191
  const PANEL_ROLES = rolesOf('panel');
192
+ const EXCLUSIVE_ROOT_ROLES = exclusiveRolesOf('root');
193
+ const EXCLUSIVE_LIST_ROLES = exclusiveRolesOf('list');
194
+ const EXCLUSIVE_TRIGGER_ROLES = exclusiveRolesOf('trigger');
195
+ const EXCLUSIVE_PANELS_ROLES = exclusiveRolesOf('panels');
196
+ const EXCLUSIVE_PANEL_ROLES = exclusiveRolesOf('panel');
163
197
  /** The kind a role belongs to (`disclosure.panel` → Disclosure), or null for any other role. */
164
198
  function compositeKindOfRole(role) {
165
199
  const head = role?.split('.')[0];
166
200
  return KINDS.includes(head ?? '') ? head : null;
167
201
  }
202
+ /** True for a kind the open/close runtime runs. */
203
+ function isToggleKind(kind) {
204
+ return !!kind && TOGGLE_KINDS.includes(kind);
205
+ }
206
+ /** True for a kind the exclusive runtime runs. */
207
+ function isExclusiveKind(kind) {
208
+ return !!kind && EXCLUSIVE_KINDS$1.includes(kind);
209
+ }
168
210
  /** A root that runs the open/close runtime (an Accordion or a Disclosure). */
169
211
  function isCompositeRoot(node) {
170
212
  return !!node?.role && ROOT_ROLES.has(node.role);
@@ -211,6 +253,61 @@ function compositeItemOf(node, path) {
211
253
  }
212
254
  return null;
213
255
  }
256
+ // ── the exclusive family (CC4a Tabs) ───────────────────────────────────────────────────────────
257
+ /** A root that runs the exclusive runtime (Tabs). */
258
+ function isExclusiveRoot(node) {
259
+ return !!node?.role && EXCLUSIVE_ROOT_ROLES.has(node.role);
260
+ }
261
+ /** The holder of an exclusive composite's triggers (a tab list): it takes only triggers. */
262
+ function isExclusiveList(node) {
263
+ return !!node?.role && EXCLUSIVE_LIST_ROLES.has(node.role);
264
+ }
265
+ /** One of an exclusive composite's triggers (a tab): pressing it turns its item on. */
266
+ function isExclusiveTrigger(node) {
267
+ return !!node?.role && EXCLUSIVE_TRIGGER_ROLES.has(node.role);
268
+ }
269
+ /** The holder of an exclusive composite's panels: it takes only panels. */
270
+ function isExclusivePanels(node) {
271
+ return !!node?.role && EXCLUSIVE_PANELS_ROLES.has(node.role);
272
+ }
273
+ /** One of an exclusive composite's panels (a tab panel): shown only while its item is on. */
274
+ function isExclusivePanel(node) {
275
+ return !!node?.role && EXCLUSIVE_PANEL_ROLES.has(node.role);
276
+ }
277
+ /** True for a trigger or a panel — a part that IS one item of an exclusive composite (it carries the `itemKey`). */
278
+ function isExclusiveItemPart(node) {
279
+ return isExclusiveTrigger(node) || isExclusivePanel(node);
280
+ }
281
+ /** True for any part of a Tabs (its root included). */
282
+ function isTabsPart(node) {
283
+ return compositeKindOfRole(node?.role) === CompositeKind.Tabs;
284
+ }
285
+ /** A root of either family — anything that runs a composite runtime. */
286
+ function isAnyCompositeRoot(node) {
287
+ return isCompositeRoot(node) || isExclusiveRoot(node);
288
+ }
289
+ /** The exclusive root a node belongs to (the node itself when it is one), or null. `path` = its ancestors, root first. */
290
+ function exclusiveRootOf(node, path) {
291
+ if (isExclusiveRoot(node))
292
+ return node;
293
+ for (let i = path.length - 1; i >= 0; i--)
294
+ if (isExclusiveRoot(path[i]))
295
+ return path[i];
296
+ return null;
297
+ }
298
+ /** The trigger or panel a node sits in (itself when it is one) inside an exclusive composite, or null. A nested
299
+ * exclusive root ends the walk. */
300
+ function exclusiveItemPartOf(node, path) {
301
+ if (isExclusiveItemPart(node))
302
+ return node;
303
+ for (let i = path.length - 1; i >= 0; i--) {
304
+ if (isExclusiveItemPart(path[i]))
305
+ return path[i];
306
+ if (isExclusiveRoot(path[i]))
307
+ return null;
308
+ }
309
+ return null;
310
+ }
214
311
 
215
312
  /**
216
313
  * ACCORDION (CC2 — composite K1; complex-components boards 3, 4 and 9, APPROVED 2026-10-20). Not a
@@ -271,7 +368,7 @@ const ACCORDION_WORDS = [
271
368
  { question: 'Write the third question', answer: 'Answer it plainly, in two or three short sentences.' },
272
369
  ];
273
370
  const ACCORDION_NEW_ITEM_WORDS = { question: 'Write a new question', answer: 'Answer it plainly, in two or three short sentences.' };
274
- var Type$2;
371
+ var Type$4;
275
372
  (function (Type) {
276
373
  Type["Container"] = "Container";
277
374
  Type["LinkedContainer"] = "LinkedContainer";
@@ -279,8 +376,8 @@ var Type$2;
279
376
  Type["Text"] = "Text";
280
377
  Type["Icon"] = "Icon";
281
378
  Type["Repeater"] = "Repeater";
282
- })(Type$2 || (Type$2 = {}));
283
- var Style$2;
379
+ })(Type$4 || (Type$4 = {}));
380
+ var Style$4;
284
381
  (function (Style) {
285
382
  Style["Root"] = "box";
286
383
  Style["Item"] = "accordion-item";
@@ -289,14 +386,14 @@ var Style$2;
289
386
  Style["Icon"] = "icon";
290
387
  Style["Panel"] = "accordion-panel";
291
388
  Style["Answer"] = "body";
292
- })(Style$2 || (Style$2 = {}));
293
- const ZERO_BOX$2 = '0px 0px 0px 0px';
389
+ })(Style$4 || (Style$4 = {}));
390
+ const ZERO_BOX$3 = '0px 0px 0px 0px';
294
391
  const HEADING_LEVELS = ['h2', 'h3', 'h4', 'h5', 'h6'];
295
392
  const DEFAULT_LEVEL = 'h3';
296
393
  /** A linked trait the header must never carry: it is a button, not a link. */
297
394
  const LINK_TYPE$1 = 'linkType';
298
395
  /** What each part holds in the recipe — layout only; its LOOK is its element style. */
299
- const PART_BASE$1 = {
396
+ const PART_BASE$2 = {
300
397
  // The items are a column a token apart (`accordion-gap`: 0 for a divider list, 12px for cards).
301
398
  [AccordionRole.Accordion]: { display: 'flex', flexDirection: 'column', gap: 'var(--iox-accordion-gap)', width: '100%', minHeight: 'auto', height: 'auto' },
302
399
  [AccordionRole.Item]: { display: 'block', width: '100%', minHeight: 'auto', height: 'auto' },
@@ -307,16 +404,16 @@ const PART_BASE$1 = {
307
404
  };
308
405
  /** The question INHERITS the header's ink (board 4's wears table: the Question reads no colour token), so the
309
406
  * header's hover ink reaches the words; and it sits in the row with no margin of its own. */
310
- const QUESTION_BASE = { color: 'inherit', margin: ZERO_BOX$2, flexGrow: '1', minWidth: '0px' };
311
- const ANSWER_BASE = { margin: ZERO_BOX$2, padding: ZERO_BOX$2, minHeight: 'auto' };
312
- function make$2(create, key, type, role, style, props = {}, label) {
313
- return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE$1[role] : undefined);
407
+ const QUESTION_BASE = { color: 'inherit', margin: ZERO_BOX$3, flexGrow: '1', minWidth: '0px' };
408
+ const ANSWER_BASE = { margin: ZERO_BOX$3, padding: ZERO_BOX$3, minHeight: 'auto' };
409
+ function make$3(create, key, type, role, style, props = {}, label) {
410
+ return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE$2[role] : undefined);
314
411
  }
315
412
  /** The icon of an item (an accordion's, or a disclosure's — `role`); its Item-open state (the turn) is driven by
316
413
  * ITS item (`stateTriggers`). ONE builder for both kinds. */
317
414
  function buildCompositeIcon(create, key, icon, itemId, role = AccordionRole.Icon) {
318
415
  const glyph = ACCORDION_ICON_GLYPH[icon];
319
- const node = makeCompositePart(create, { key, type: Type$2.Icon, props: { iconClass: glyph.iconClass }, elementStyle: Style$2.Icon }, role, 'Icon', PART_BASE$1[AccordionRole.Icon]);
416
+ const node = makeCompositePart(create, { key, type: Type$4.Icon, props: { iconClass: glyph.iconClass }, elementStyle: Style$4.Icon }, role, 'Icon', PART_BASE$2[AccordionRole.Icon]);
320
417
  node.stateStyles = { ...(node.stateStyles ?? {}), [ITEM_OPEN_STATE]: { rotate: glyph.turn } };
321
418
  if (itemId)
322
419
  node.stateTriggers = { ...(node.stateTriggers ?? {}), [ITEM_OPEN_STATE]: itemId };
@@ -324,14 +421,14 @@ function buildCompositeIcon(create, key, icon, itemId, role = AccordionRole.Icon
324
421
  }
325
422
  /** One item: header (question + icon) and panel (answer). `key` makes every part's recipe key unique. */
326
423
  function buildAccordionItem(create, words, itemKey, icon = AccordionIcon.Chevron) {
327
- const item = make$2(create, `${itemKey}`, Type$2.Container, AccordionRole.Item, Style$2.Item, {}, 'Item');
424
+ const item = make$3(create, `${itemKey}`, Type$4.Container, AccordionRole.Item, Style$4.Item, {}, 'Item');
328
425
  item.itemKey = itemKey;
329
- const trigger = make$2(create, `${itemKey}-trigger`, Type$2.LinkedContainer, AccordionRole.Trigger, Style$2.Trigger, {}, 'Header');
330
- const question = make$2(create, `${itemKey}-question`, Type$2.Heading, null, Style$2.Question, { content: words.question, level: DEFAULT_LEVEL }, 'Heading');
426
+ const trigger = make$3(create, `${itemKey}-trigger`, Type$4.LinkedContainer, AccordionRole.Trigger, Style$4.Trigger, {}, 'Header');
427
+ const question = make$3(create, `${itemKey}-question`, Type$4.Heading, null, Style$4.Question, { content: words.question, level: DEFAULT_LEVEL }, 'Heading');
331
428
  writeStyles(question, QUESTION_BASE);
332
429
  trigger.children = [question, ...(icon === AccordionIcon.None ? [] : [buildCompositeIcon(create, `${itemKey}-icon`, icon, item.id)])];
333
- const panel = make$2(create, `${itemKey}-panel`, Type$2.Container, AccordionRole.Panel, Style$2.Panel, {}, 'Panel');
334
- const answer = make$2(create, `${itemKey}-answer`, Type$2.Text, null, Style$2.Answer, { content: words.answer, tag: 'p' }, 'Text');
430
+ const panel = make$3(create, `${itemKey}-panel`, Type$4.Container, AccordionRole.Panel, Style$4.Panel, {}, 'Panel');
431
+ const answer = make$3(create, `${itemKey}-answer`, Type$4.Text, null, Style$4.Answer, { content: words.answer, tag: 'p' }, 'Text');
335
432
  writeStyles(answer, ANSWER_BASE);
336
433
  panel.children = [answer];
337
434
  item.children = [trigger, panel];
@@ -339,7 +436,7 @@ function buildAccordionItem(create, words, itemKey, icon = AccordionIcon.Chevron
339
436
  }
340
437
  /** A fresh Accordion: three items, one open at a time, the first open by default (board 4). */
341
438
  function buildAccordionRecipe(create, options = {}) {
342
- const root = make$2(create, 'accordion', Type$2.Container, AccordionRole.Accordion, Style$2.Root, {}, 'Accordion');
439
+ const root = make$3(create, 'accordion', Type$4.Container, AccordionRole.Accordion, Style$4.Root, {}, 'Accordion');
343
440
  writeProp(root, CompositeBehaviourTrait.Multiple, options.multiple ?? false);
344
441
  writeProp(root, CompositeBehaviourTrait.DefaultOn, options.defaultOn ?? AccordionDefaultOn.First);
345
442
  const words = options.items ?? ACCORDION_WORDS;
@@ -378,7 +475,7 @@ function accordionItemOf(node, path) {
378
475
  }
379
476
  /** True when the root takes its items from content (a Repeater bound to a list, R3). */
380
477
  function isAccordionFromContent(root) {
381
- return !!root && root.type === Type$2.Repeater;
478
+ return !!root && root.type === Type$4.Repeater;
382
479
  }
383
480
  /** The root's items in order — every item (Manual), or the one template item (From content). */
384
481
  function accordionItems(root) {
@@ -402,7 +499,8 @@ function setAccordionBehaviour(root, patch) {
402
499
  }
403
500
  /** The roles of the kind an item belongs to (an accordion item, or a disclosure — its own item). */
404
501
  function kindRolesOf(node) {
405
- return COMPOSITE_KIND_ROLES[compositeKindOfRole(node.role) ?? CompositeKind.Accordion];
502
+ const kind = compositeKindOfRole(node.role);
503
+ return COMPOSITE_KIND_ROLES[isToggleKind(kind) ? kind : CompositeKind.Accordion];
406
504
  }
407
505
  /** The header of an item (either kind). */
408
506
  function accordionTriggerOf(item) {
@@ -424,9 +522,9 @@ function accordionQuestionOf(item) {
424
522
  let text;
425
523
  if (trigger)
426
524
  walkComposite(trigger, n => {
427
- if (!hit && n.type === Type$2.Heading)
525
+ if (!hit && n.type === Type$4.Heading)
428
526
  hit = n;
429
- if (!text && n.type === Type$2.Text)
527
+ if (!text && n.type === Type$4.Text)
430
528
  text = n;
431
529
  });
432
530
  return hit ?? text;
@@ -436,15 +534,15 @@ function accordionAnswerOf(item) {
436
534
  const panel = accordionPanelOf(item);
437
535
  let hit;
438
536
  if (panel)
439
- walkComposite(panel, n => { if (!hit && n.type === Type$2.Text)
537
+ walkComposite(panel, n => { if (!hit && n.type === Type$4.Text)
440
538
  hit = n; });
441
539
  return hit;
442
540
  }
443
- const stripTags = (v) => String(v ?? '').replace(/<[^>]*>/g, '').trim();
541
+ const stripTags$1 = (v) => String(v ?? '').replace(/<[^>]*>/g, '').trim();
444
542
  /** An item's name in the Items list and the layer tree: its question's words. */
445
543
  function accordionItemLabel(item) {
446
544
  const q = accordionQuestionOf(item);
447
- return q ? stripTags(readProp(q, 'content')) : '';
545
+ return q ? stripTags$1(readProp(q, 'content')) : '';
448
546
  }
449
547
  /** Items ▸ rename: the question's words. */
450
548
  function renameAccordionItem(item, question) {
@@ -457,7 +555,7 @@ function accordionHeadingLevel(trigger) {
457
555
  const visit = (n) => {
458
556
  if (level !== undefined)
459
557
  return;
460
- if (n.type === Type$2.Heading) {
558
+ if (n.type === Type$4.Heading) {
461
559
  level = readProp(n, 'level');
462
560
  return;
463
561
  }
@@ -637,6 +735,7 @@ function rowItemKey(row, index) {
637
735
  const DOM_ID_PREFIX = {
638
736
  [CompositeKind.Accordion]: 'iox-acc',
639
737
  [CompositeKind.Disclosure]: 'iox-dis',
738
+ [CompositeKind.Tabs]: 'iox-tab',
640
739
  };
641
740
  /** The ARIA pairing ids of an item: unique per composite (its SCOPE id — `compositeScopeId`) and per item (its key). */
642
741
  function accordionDomIds(rootId, itemKey, kind = CompositeKind.Accordion) {
@@ -724,14 +823,14 @@ function accordionPanelAttributes(state) {
724
823
  function serializeDomAttributes(attrs, esc) {
725
824
  return Object.entries(attrs).map(([k, v]) => (v === '' ? ` ${k}` : ` ${k}="${esc(v)}"`)).join('');
726
825
  }
727
- const SHORT_TEXT = new Set(['text']);
728
- const LONG_TEXT = new Set(['textarea', 'richText', 'editor']);
826
+ const SHORT_TEXT$1 = new Set(['text']);
827
+ const LONG_TEXT$1 = new Set(['textarea', 'richText', 'editor']);
729
828
  /** R3 (board 9 Q19): switching to From content binds the question to the first short-text field and the
730
829
  * answer to the first long-text field. ONE rule for the builder's switch and the composer. */
731
830
  function pickAccordionAutoBinds(fields) {
732
831
  const isString = (f) => !f.type || f.type === 'string';
733
- const question = fields.find(f => isString(f) && SHORT_TEXT.has(String(f.controller ?? 'text')))?.name ?? null;
734
- const answer = fields.find(f => isString(f) && LONG_TEXT.has(String(f.controller ?? '')) && f.name !== question)?.name ?? null;
832
+ const question = fields.find(f => isString(f) && SHORT_TEXT$1.has(String(f.controller ?? 'text')))?.name ?? null;
833
+ const answer = fields.find(f => isString(f) && LONG_TEXT$1.has(String(f.controller ?? '')) && f.name !== question)?.name ?? null;
735
834
  return { question, answer };
736
835
  }
737
836
  /** The bound props of the question and the answer (each a content trait). */
@@ -811,12 +910,12 @@ function accordionPartStyle(node, name) {
811
910
  */
812
911
  const ITEM_ON_EVENT = 'iox-item-on';
813
912
  const ITEM_OFF_EVENT = 'iox-item-off';
814
- const KIND_ROOT = (kind) => `[${COMPOSITE_ATTRIBUTE}="${kind}"]`;
815
- const ROOT_SELECTOR = Object.values(CompositeKind).map(KIND_ROOT).join(', ');
913
+ const KIND_ROOT$1 = (kind) => `[${COMPOSITE_ATTRIBUTE}="${kind}"]`;
914
+ const ROOT_SELECTOR$1 = Object.keys(COMPOSITE_KIND_ROLES).map(KIND_ROOT$1).join(', ');
816
915
  /** An accordion's items are its `.iox-role-accordion-item` children; a disclosure is its own item. */
817
916
  const ACCORDION_ITEM_SELECTOR = '.iox-role-accordion-item';
818
- const ITEM_SELECTOR = `${ACCORDION_ITEM_SELECTOR}, ${KIND_ROOT(CompositeKind.Disclosure)}`;
819
- const PANEL_SELECTOR = '.iox-role-accordion-panel, .iox-role-disclosure-panel';
917
+ const ITEM_SELECTOR = `${ACCORDION_ITEM_SELECTOR}, ${KIND_ROOT$1(CompositeKind.Disclosure)}`;
918
+ const PANEL_SELECTOR$1 = '.iox-role-accordion-panel, .iox-role-disclosure-panel';
820
919
  const BUTTON_SELECTOR = `.${AccordionClass.Button}`;
821
920
  const TRACK_SELECTOR = `.${AccordionClass.Track}`;
822
921
  const REGION_SELECTOR = `.${AccordionClass.Region}`;
@@ -825,16 +924,16 @@ const MAX_CLOSE_MS = 600;
825
924
  const REDUCED_MOTION = '(prefers-reduced-motion: reduce)';
826
925
  /** The composite (accordion or disclosure) an element belongs to (the element itself when it is a root). */
827
926
  function closestAccordion(el) {
828
- return el?.closest?.(ROOT_SELECTOR) ?? null;
927
+ return el?.closest?.(ROOT_SELECTOR$1) ?? null;
829
928
  }
830
929
  /** The item an element sits in (itself when it is one) — an accordion item, or a disclosure. */
831
930
  function closestAccordionItem(el) {
832
931
  return el?.closest?.(ITEM_SELECTOR) ?? null;
833
932
  }
834
- const kindOf = (root) => root.getAttribute(COMPOSITE_ATTRIBUTE) === CompositeKind.Disclosure ? CompositeKind.Disclosure : CompositeKind.Accordion;
933
+ const kindOf$1 = (root) => root.getAttribute(COMPOSITE_ATTRIBUTE) === CompositeKind.Disclosure ? CompositeKind.Disclosure : CompositeKind.Accordion;
835
934
  /** The composite root whose items an item belongs to: a disclosure is its own root. */
836
- function rootOfItem(item) {
837
- return kindOf(item) === CompositeKind.Disclosure && item.matches(KIND_ROOT(CompositeKind.Disclosure)) ? item : closestAccordion(item.parentElement);
935
+ function accordionRootOfItem(item) {
936
+ return kindOf$1(item) === CompositeKind.Disclosure && item.matches(KIND_ROOT$1(CompositeKind.Disclosure)) ? item : closestAccordion(item.parentElement);
838
937
  }
839
938
  /** Subscribe to an item turning on / off — what the `itemOn` / `itemOff` interaction triggers listen to. The
840
939
  * item is the element's own item (the element itself when it is one). Returns the unsubscribe. */
@@ -850,20 +949,20 @@ function onAccordionItemState(el, on, handler) {
850
949
  function isAccordionItemOn(el) {
851
950
  return !!closestAccordionItem(el)?.hasAttribute(ON_ATTRIBUTE);
852
951
  }
853
- const nodeIdOf = (el) => {
952
+ const nodeIdOf$1 = (el) => {
854
953
  for (const c of Array.from(el.classList))
855
954
  if (c.startsWith('iox-node-'))
856
955
  return c.slice('iox-node-'.length);
857
956
  return '';
858
957
  };
859
- function behaviourOf(root) {
958
+ function behaviourOf$1(root) {
860
959
  const raw = root.getAttribute(DEFAULT_ON_ATTRIBUTE);
861
960
  return {
862
961
  multiple: root.hasAttribute(MULTIPLE_ATTRIBUTE),
863
962
  defaultOn: ACCORDION_DEFAULT_ONS.includes(raw) ? raw : AccordionDefaultOn.First,
864
963
  };
865
964
  }
866
- function setAttr(el, name, value) {
965
+ function setAttr$1(el, name, value) {
867
966
  if (!el)
868
967
  return;
869
968
  if (value === null) {
@@ -918,8 +1017,8 @@ class AccordionRuntime {
918
1017
  return;
919
1018
  this.open(this.keyOf(item, items));
920
1019
  };
921
- this.kind = options.kind ?? kindOf(root);
922
- this.behaviourValue = options.behaviour ?? behaviourOf(root);
1020
+ this.kind = options.kind ?? kindOf$1(root);
1021
+ this.behaviourValue = options.behaviour ?? behaviourOf$1(root);
923
1022
  root.setAttribute(COMPOSITE_ATTRIBUTE, this.kind);
924
1023
  RUNTIMES.set(root, this);
925
1024
  // The server rendered the first state; with no server state the root's default decides.
@@ -1052,7 +1151,7 @@ class AccordionRuntime {
1052
1151
  }
1053
1152
  const forced = this.options.forced?.() ?? new Set();
1054
1153
  const own = this.options.rootId;
1055
- const rootId = (typeof own === 'function' ? own() : own) ?? this.root.getAttribute(SCOPE_ATTRIBUTE) ?? nodeIdOf(this.root);
1154
+ const rootId = (typeof own === 'function' ? own() : own) ?? this.root.getAttribute(SCOPE_ATTRIBUTE) ?? nodeIdOf$1(this.root);
1056
1155
  const next = new Set();
1057
1156
  items.forEach((item, index) => {
1058
1157
  const key = this.keyOf(item, items);
@@ -1069,31 +1168,31 @@ class AccordionRuntime {
1069
1168
  for (const item of items) {
1070
1169
  const key = this.keyOf(item, items);
1071
1170
  if (next.has(key) && !was.has(key))
1072
- dispatch(item, ITEM_ON_EVENT);
1171
+ dispatch$1(item, ITEM_ON_EVENT);
1073
1172
  else if (!next.has(key) && was.has(key))
1074
- dispatch(item, ITEM_OFF_EVENT);
1173
+ dispatch$1(item, ITEM_OFF_EVENT);
1075
1174
  }
1076
1175
  }
1077
1176
  stampItem(item, key, index, on, rootId, first) {
1078
1177
  const ids = accordionDomIds(rootId, key, this.kind);
1079
1178
  if (!item.hasAttribute(ITEM_KEY_ATTRIBUTE))
1080
1179
  item.setAttribute(ITEM_KEY_ATTRIBUTE, key);
1081
- setAttr(item, ON_ATTRIBUTE, on ? '' : null);
1180
+ setAttr$1(item, ON_ATTRIBUTE, on ? '' : null);
1082
1181
  const button = this.own(item, BUTTON_SELECTOR);
1083
1182
  const track = this.own(item, TRACK_SELECTOR);
1084
1183
  const region = this.own(item, REGION_SELECTOR);
1085
- const panel = this.own(item, PANEL_SELECTOR);
1086
- setAttr(button, 'type', 'button');
1087
- setAttr(button, 'id', ids.trigger);
1088
- setAttr(button, 'aria-expanded', on ? 'true' : 'false');
1089
- setAttr(button, 'aria-controls', ids.panel);
1090
- setAttr(button, ON_ATTRIBUTE, on ? '' : null);
1091
- setAttr(track, ON_ATTRIBUTE, on ? '' : null);
1092
- setAttr(panel, ON_ATTRIBUTE, on ? '' : null);
1184
+ const panel = this.own(item, PANEL_SELECTOR$1);
1185
+ setAttr$1(button, 'type', 'button');
1186
+ setAttr$1(button, 'id', ids.trigger);
1187
+ setAttr$1(button, 'aria-expanded', on ? 'true' : 'false');
1188
+ setAttr$1(button, 'aria-controls', ids.panel);
1189
+ setAttr$1(button, ON_ATTRIBUTE, on ? '' : null);
1190
+ setAttr$1(track, ON_ATTRIBUTE, on ? '' : null);
1191
+ setAttr$1(panel, ON_ATTRIBUTE, on ? '' : null);
1093
1192
  const labelled = hasLabelledRegion(this.kind);
1094
- setAttr(region, 'role', labelled ? 'region' : null);
1095
- setAttr(region, 'id', ids.panel);
1096
- setAttr(region, 'aria-labelledby', labelled ? ids.trigger : null);
1193
+ setAttr$1(region, 'role', labelled ? 'region' : null);
1194
+ setAttr$1(region, 'id', ids.panel);
1195
+ setAttr$1(region, 'aria-labelledby', labelled ? ids.trigger : null);
1097
1196
  if (!region)
1098
1197
  return;
1099
1198
  const pending = this.closing.get(key);
@@ -1102,7 +1201,7 @@ class AccordionRuntime {
1102
1201
  clearTimeout(pending);
1103
1202
  this.closing.delete(key);
1104
1203
  }
1105
- setAttr(region, 'hidden', null);
1204
+ setAttr$1(region, 'hidden', null);
1106
1205
  return;
1107
1206
  }
1108
1207
  if (region.hasAttribute('hidden') || pending)
@@ -1110,13 +1209,13 @@ class AccordionRuntime {
1110
1209
  // Closing: the track collapses first (its transition), THEN the region is hidden — hiding it at once
1111
1210
  // would collapse it with no motion. At first paint, or under reduced motion, it is hidden at once.
1112
1211
  if (first || this.reduced()) {
1113
- setAttr(region, 'hidden', UNTIL_FOUND);
1212
+ setAttr$1(region, 'hidden', UNTIL_FOUND);
1114
1213
  return;
1115
1214
  }
1116
1215
  const hide = () => {
1117
1216
  this.closing.delete(key);
1118
1217
  if (!item.hasAttribute(ON_ATTRIBUTE))
1119
- setAttr(region, 'hidden', UNTIL_FOUND);
1218
+ setAttr$1(region, 'hidden', UNTIL_FOUND);
1120
1219
  };
1121
1220
  this.closing.set(key, setTimeout(hide, this.closeDelay(track)));
1122
1221
  }
@@ -1139,7 +1238,7 @@ class AccordionRuntime {
1139
1238
  return this.items().map(i => this.own(i, BUTTON_SELECTOR)).filter((b) => !!b);
1140
1239
  }
1141
1240
  }
1142
- function dispatch(el, type) {
1241
+ function dispatch$1(el, type) {
1143
1242
  const Ctor = globalThis.CustomEvent;
1144
1243
  if (Ctor)
1145
1244
  el.dispatchEvent(new Ctor(type, { bubbles: false }));
@@ -1154,7 +1253,7 @@ function attachAccordionAt(root, options = {}) {
1154
1253
  /** Attach the runtime to every accordion under `container` that has none yet. Returns the new runtimes. */
1155
1254
  function attachAccordions(container, options = {}) {
1156
1255
  const out = [];
1157
- container.querySelectorAll(ROOT_SELECTOR).forEach(root => {
1256
+ container.querySelectorAll(ROOT_SELECTOR$1).forEach(root => {
1158
1257
  const runtime = attachAccordionAt(root, options);
1159
1258
  if (runtime)
1160
1259
  out.push(runtime);
@@ -1168,14 +1267,14 @@ function detachAccordion(runtime) {
1168
1267
  /** Every live runtime by its root — the canvas's and the site's alike (a runtime registers itself and leaves on
1169
1268
  * destroy), so an item action finds the runtime of whatever item it targets. */
1170
1269
  const RUNTIMES = new WeakMap();
1171
- const ROW_INDEX_ATTRIBUTE = 'data-iox-row-idx';
1270
+ const ROW_INDEX_ATTRIBUTE$1 = 'data-iox-row-idx';
1172
1271
  /** The repeated rows an element sits in, outermost first — each row's index as its row wrappers stamp it
1173
1272
  * (`data-iox-row-idx`: the builder's Repeater rows, the engine's component path). The builder folds them into a
1174
1273
  * composite's scope id (`compositeScopeId`, vectoriox/iox-ui#20); the element itself is not counted. */
1175
1274
  function compositeRowKeys(el) {
1176
1275
  const keys = [];
1177
- for (let at = el?.parentElement?.closest?.(`[${ROW_INDEX_ATTRIBUTE}]`); at; at = at.parentElement?.closest(`[${ROW_INDEX_ATTRIBUTE}]`)) {
1178
- keys.unshift(at.getAttribute(ROW_INDEX_ATTRIBUTE) ?? '');
1276
+ for (let at = el?.parentElement?.closest?.(`[${ROW_INDEX_ATTRIBUTE$1}]`); at; at = at.parentElement?.closest(`[${ROW_INDEX_ATTRIBUTE$1}]`)) {
1277
+ keys.unshift(at.getAttribute(ROW_INDEX_ATTRIBUTE$1) ?? '');
1179
1278
  }
1180
1279
  return keys;
1181
1280
  }
@@ -1191,18 +1290,6 @@ var CompositeItemAction;
1191
1290
  CompositeItemAction["Close"] = "close";
1192
1291
  CompositeItemAction["Toggle"] = "toggle";
1193
1292
  })(CompositeItemAction || (CompositeItemAction = {}));
1194
- /**
1195
- * Run an item action on the element of an ITEM (an accordion item, or a disclosure) — the `openItem` /
1196
- * `closeItem` / `toggleItem` interaction actions, on the canvas and on the live site through this one function.
1197
- * Returns whether the item shows open afterwards, or null when `el` is in no running composite.
1198
- */
1199
- function runCompositeItemAction(el, action) {
1200
- const item = closestAccordionItem(el);
1201
- const runtime = item ? accordionRuntimeOf(rootOfItem(item)) : null;
1202
- if (!item || !runtime)
1203
- return null;
1204
- return runtime.run(action, runtime.keyOf(item));
1205
- }
1206
1293
  /** How much of an opened item must already be on screen for the action NOT to scroll to it (its header row). */
1207
1294
  const VISIBLE_HEAD_PX = 48;
1208
1295
  /** True when an item an action just opened is out of view and the page should scroll to it (T14: "opens that item
@@ -1219,21 +1306,21 @@ const DISCLOSURE_WORDS = {
1219
1306
  };
1220
1307
  /** A disclosure's one item key — its root IS its item. */
1221
1308
  const DISCLOSURE_ITEM_KEY = 'k1';
1222
- var Type$1;
1309
+ var Type$3;
1223
1310
  (function (Type) {
1224
1311
  Type["Container"] = "Container";
1225
1312
  Type["LinkedContainer"] = "LinkedContainer";
1226
1313
  Type["Text"] = "Text";
1227
- })(Type$1 || (Type$1 = {}));
1228
- var Style$1;
1314
+ })(Type$3 || (Type$3 = {}));
1315
+ var Style$3;
1229
1316
  (function (Style) {
1230
1317
  Style["Root"] = "box";
1231
1318
  Style["Trigger"] = "accordion-trigger";
1232
1319
  Style["Word"] = "body";
1233
1320
  Style["Panel"] = "accordion-panel";
1234
1321
  Style["Body"] = "body";
1235
- })(Style$1 || (Style$1 = {}));
1236
- const ZERO_BOX$1 = '0px 0px 0px 0px';
1322
+ })(Style$3 || (Style$3 = {}));
1323
+ const ZERO_BOX$2 = '0px 0px 0px 0px';
1237
1324
  const HIDDEN = 'none';
1238
1325
  const INHERIT = 'inherit';
1239
1326
  /** A word is a flex item of the toggle — `block` (never `inline`: the compiler drops a value equal to the CSS initial,
@@ -1242,7 +1329,7 @@ const SHOWN = 'block';
1242
1329
  /** What each part holds — layout only; its LOOK is its element style. The trigger sizes to its words (a button in
1243
1330
  * the flow, not a full-width header row — `fit-content` keeps it from stretching) and the panel spans the root. The
1244
1331
  * trigger is `flex`, not `inline-flex`: it is a flex item of the root, which blockifies it on the site anyway. */
1245
- const PART_BASE = {
1332
+ const PART_BASE$1 = {
1246
1333
  [DisclosureRole.Disclosure]: { display: 'flex', flexDirection: 'column', width: '100%', minHeight: 'auto', height: 'auto' },
1247
1334
  // The toggle reads the text around it (the page's body type, through the root) — a LinkedContainer is born with
1248
1335
  // its own literal font, which an accordion's heading masks and a disclosure's words would show.
@@ -1252,17 +1339,17 @@ const PART_BASE = {
1252
1339
  },
1253
1340
  // A word is just its words: none of a Text's own box (its type's 10px padding, its full width) — the toggle's
1254
1341
  // padding is the click target.
1255
- [DisclosureRole.More]: { display: SHOWN, margin: ZERO_BOX$1, padding: ZERO_BOX$1, width: 'auto' },
1256
- [DisclosureRole.Less]: { display: HIDDEN, margin: ZERO_BOX$1, padding: ZERO_BOX$1, width: 'auto' },
1342
+ [DisclosureRole.More]: { display: SHOWN, margin: ZERO_BOX$2, padding: ZERO_BOX$2, width: 'auto' },
1343
+ [DisclosureRole.Less]: { display: HIDDEN, margin: ZERO_BOX$2, padding: ZERO_BOX$2, width: 'auto' },
1257
1344
  [DisclosureRole.Panel]: { display: 'block', width: '100%', minHeight: 'auto', height: 'auto' },
1258
1345
  };
1259
- const BODY_BASE = { margin: ZERO_BOX$1, padding: ZERO_BOX$1, minHeight: 'auto' };
1260
- function make$1(create, key, type, role, style, props, label) {
1261
- return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE[role] : undefined);
1346
+ const BODY_BASE$1 = { margin: ZERO_BOX$2, padding: ZERO_BOX$2, minHeight: 'auto' };
1347
+ function make$2(create, key, type, role, style, props, label) {
1348
+ return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE$1[role] : undefined);
1262
1349
  }
1263
1350
  /** One of the trigger's two words: shown while the disclosure is closed (`more`) or open (`less`). */
1264
1351
  function buildWord(create, role, words, rootId) {
1265
- const node = make$1(create, role === DisclosureRole.More ? 'more' : 'less', Type$1.Text, role, Style$1.Word, { content: words, tag: 'span' }, 'Text');
1352
+ const node = make$2(create, role === DisclosureRole.More ? 'more' : 'less', Type$3.Text, role, Style$3.Word, { content: words, tag: 'span' }, 'Text');
1266
1353
  node.stateStyles = { ...(node.stateStyles ?? {}), [ITEM_OPEN_STATE]: { display: role === DisclosureRole.More ? HIDDEN : SHOWN } };
1267
1354
  if (rootId)
1268
1355
  node.stateTriggers = { ...(node.stateTriggers ?? {}), [ITEM_OPEN_STATE]: rootId };
@@ -1271,19 +1358,19 @@ function buildWord(create, role, words, rootId) {
1271
1358
  /** A fresh Disclosure: "Read more" opens the text below and reads "Read less" while it is open; closed by default. */
1272
1359
  function buildDisclosureRecipe(create, options = {}) {
1273
1360
  const words = { ...DISCLOSURE_WORDS, ...(options.words ?? {}) };
1274
- const root = make$1(create, 'disclosure', Type$1.Container, DisclosureRole.Disclosure, Style$1.Root, {}, 'Disclosure');
1361
+ const root = make$2(create, 'disclosure', Type$3.Container, DisclosureRole.Disclosure, Style$3.Root, {}, 'Disclosure');
1275
1362
  root.itemKey = DISCLOSURE_ITEM_KEY;
1276
1363
  writeProp(root, CompositeBehaviourTrait.DefaultOn, options.open ? AccordionDefaultOn.First : AccordionDefaultOn.None);
1277
1364
  const icon = options.icon ?? AccordionIcon.Chevron;
1278
- const trigger = make$1(create, 'trigger', Type$1.LinkedContainer, DisclosureRole.Trigger, Style$1.Trigger, {}, 'Toggle');
1365
+ const trigger = make$2(create, 'trigger', Type$3.LinkedContainer, DisclosureRole.Trigger, Style$3.Trigger, {}, 'Toggle');
1279
1366
  trigger.children = [
1280
1367
  buildWord(create, DisclosureRole.More, words.more, root.id),
1281
1368
  buildWord(create, DisclosureRole.Less, words.less, root.id),
1282
1369
  ...(icon === AccordionIcon.None ? [] : [buildCompositeIcon(create, 'icon', icon, root.id, DisclosureRole.Icon)]),
1283
1370
  ];
1284
- const panel = make$1(create, 'panel', Type$1.Container, DisclosureRole.Panel, Style$1.Panel, {}, 'Panel');
1285
- const body = make$1(create, 'body', Type$1.Text, null, Style$1.Body, { content: words.body, tag: 'p' }, 'Text');
1286
- writeStyles(body, BODY_BASE);
1371
+ const panel = make$2(create, 'panel', Type$3.Container, DisclosureRole.Panel, Style$3.Panel, {}, 'Panel');
1372
+ const body = make$2(create, 'body', Type$3.Text, null, Style$3.Body, { content: words.body, tag: 'p' }, 'Text');
1373
+ writeStyles(body, BODY_BASE$1);
1287
1374
  panel.children = [body];
1288
1375
  root.children = [trigger, panel];
1289
1376
  return root;
@@ -1445,7 +1532,7 @@ const CARD_WORDS = {
1445
1532
  };
1446
1533
  /** The breakpoint whose layer stacks an "Image side" card (Breakpoint.Mobile, ≤767px). */
1447
1534
  const CARD_STACK_BREAKPOINT = 'mobile';
1448
- var Type;
1535
+ var Type$2;
1449
1536
  (function (Type) {
1450
1537
  Type["Container"] = "Container";
1451
1538
  Type["Image"] = "Image";
@@ -1453,8 +1540,8 @@ var Type;
1453
1540
  Type["Text"] = "Text";
1454
1541
  Type["Icon"] = "Icon";
1455
1542
  Type["ButtonBlock"] = "ButtonBlock";
1456
- })(Type || (Type = {}));
1457
- var Style;
1543
+ })(Type$2 || (Type$2 = {}));
1544
+ var Style$2;
1458
1545
  (function (Style) {
1459
1546
  Style["Card"] = "card";
1460
1547
  Style["Box"] = "box";
@@ -1467,7 +1554,7 @@ var Style;
1467
1554
  Style["Ghost"] = "button-ghost";
1468
1555
  Style["Icon"] = "icon";
1469
1556
  Style["Scrim"] = "media-scrim";
1470
- })(Style || (Style = {}));
1557
+ })(Style$2 || (Style$2 = {}));
1471
1558
  var Scheme;
1472
1559
  (function (Scheme) {
1473
1560
  Scheme["Default"] = "default";
@@ -1477,11 +1564,11 @@ const LINK_TYPE = 'linkType';
1477
1564
  const SAVE_ICON = 'ph ph-bookmark-simple';
1478
1565
  /** A value an element style writes — read from the contract, never retyped. */
1479
1566
  const styleValue = (style, trait) => String(elementStyleById(style)?.base[trait] ?? '');
1480
- const CARD_PAD = styleValue(Style.Card, 'padding');
1481
- const IMAGE_RADIUS = styleValue(Style.Media, 'borderRadius');
1482
- const TITLE_SIZE = styleValue(Style.Title, 'fontSize');
1567
+ const CARD_PAD = styleValue(Style$2.Card, 'padding');
1568
+ const IMAGE_RADIUS = styleValue(Style$2.Media, 'borderRadius');
1569
+ const TITLE_SIZE = styleValue(Style$2.Title, 'fontSize');
1483
1570
  const LEAD_SIZE = 'var(--iox-size-lead)';
1484
- const ZERO_BOX = '0px 0px 0px 0px';
1571
+ const ZERO_BOX$1 = '0px 0px 0px 0px';
1485
1572
  /** What every card part holds in every layout — the recipe's box. */
1486
1573
  const CARD_BASE = {
1487
1574
  // No height: a card is a grid or flex item, and the cell's stretch is what makes a row's cards equal.
@@ -1489,25 +1576,25 @@ const CARD_BASE = {
1489
1576
  [CardRole.Card]: { display: 'flex', gap: '14px', width: '100%', minWidth: '0px', overflow: 'hidden', position: 'relative' },
1490
1577
  [CardRole.Media]: { display: 'block', width: '100%', minHeight: 'auto', flexGrow: '0', flexShrink: '0' },
1491
1578
  [CardRole.Content]: { display: 'flex', flexDirection: 'column', gap: '8px', width: '100%', minWidth: '0px', minHeight: 'auto' },
1492
- [CardRole.Eyebrow]: { margin: ZERO_BOX, padding: ZERO_BOX, minHeight: 'auto' },
1493
- [CardRole.Title]: { margin: ZERO_BOX },
1494
- [CardRole.Text]: { margin: ZERO_BOX, padding: ZERO_BOX, minHeight: 'auto' },
1579
+ [CardRole.Eyebrow]: { margin: ZERO_BOX$1, padding: ZERO_BOX$1, minHeight: 'auto' },
1580
+ [CardRole.Title]: { margin: ZERO_BOX$1 },
1581
+ [CardRole.Text]: { margin: ZERO_BOX$1, padding: ZERO_BOX$1, minHeight: 'auto' },
1495
1582
  // Buttons pinned to the bottom (board 8 Q: equal heights + buttons pinned): the content column grows,
1496
1583
  // and the row's auto top margin takes the slack.
1497
1584
  [CardRole.Actions]: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '8px', margin: 'auto 0px 0px 0px', padding: '6px 0px 0px 0px', width: '100%', minHeight: 'auto' },
1498
1585
  };
1499
- const IMAGE_BASE = { display: 'block', width: '100%', maxWidth: '100%' };
1586
+ const IMAGE_BASE$2 = { display: 'block', width: '100%', maxWidth: '100%' };
1500
1587
  /** A button's label and a pill's words: inherit the type and ink of what holds them. */
1501
- const LABEL_BASE = { width: 'auto', minHeight: 'auto', padding: ZERO_BOX, margin: ZERO_BOX };
1588
+ const LABEL_BASE$1 = { width: 'auto', minHeight: 'auto', padding: ZERO_BOX$1, margin: ZERO_BOX$1 };
1502
1589
  const INLINE_ICON = { fontSize: '1em' };
1503
1590
  const IN_FLOW = { position: 'relative', top: 'auto', right: 'auto', bottom: 'auto', left: 'auto' };
1504
- const GROWING_CONTENT = { position: 'static', flexGrow: '1', flexShrink: '1', flexBasis: '0%', padding: ZERO_BOX };
1591
+ const GROWING_CONTENT = { position: 'static', flexGrow: '1', flexShrink: '1', flexBasis: '0%', padding: ZERO_BOX$1 };
1505
1592
  const LAYOUTS = {
1506
1593
  [CardLayout.Top]: {
1507
1594
  card: { flexDirection: 'column', alignItems: 'stretch', justifyContent: 'flex-start', minHeight: 'auto', padding: CARD_PAD },
1508
1595
  media: { ...IN_FLOW, flexBasis: 'auto' },
1509
1596
  image: { aspectRatio: '4 / 3', height: 'auto', minHeight: 'auto', borderRadius: IMAGE_RADIUS },
1510
- content: GROWING_CONTENT, contentStyle: Style.Box, contentScheme: Scheme.Default,
1597
+ content: GROWING_CONTENT, contentStyle: Style$2.Box, contentScheme: Scheme.Default,
1511
1598
  title: { fontSize: TITLE_SIZE },
1512
1599
  badgeOnMedia: true,
1513
1600
  },
@@ -1515,7 +1602,7 @@ const LAYOUTS = {
1515
1602
  card: { flexDirection: 'row', alignItems: 'stretch', justifyContent: 'flex-start', minHeight: 'auto', padding: CARD_PAD },
1516
1603
  media: { ...IN_FLOW, flexBasis: '42%' },
1517
1604
  image: { aspectRatio: 'auto', height: '100%', minHeight: '180px', borderRadius: IMAGE_RADIUS },
1518
- content: GROWING_CONTENT, contentStyle: Style.Box, contentScheme: Scheme.Default,
1605
+ content: GROWING_CONTENT, contentStyle: Style$2.Box, contentScheme: Scheme.Default,
1519
1606
  title: { fontSize: TITLE_SIZE },
1520
1607
  badgeOnMedia: true,
1521
1608
  stack: {
@@ -1525,11 +1612,11 @@ const LAYOUTS = {
1525
1612
  },
1526
1613
  },
1527
1614
  [CardLayout.Overlay]: {
1528
- card: { flexDirection: 'column', alignItems: 'stretch', justifyContent: 'flex-end', minHeight: '340px', padding: ZERO_BOX },
1615
+ card: { flexDirection: 'column', alignItems: 'stretch', justifyContent: 'flex-end', minHeight: '340px', padding: ZERO_BOX$1 },
1529
1616
  media: { position: 'absolute', top: '0px', right: '0px', bottom: '0px', left: '0px', flexBasis: 'auto' },
1530
1617
  image: { aspectRatio: 'auto', height: '100%', minHeight: 'auto', borderRadius: '0px' },
1531
1618
  content: { position: 'relative', flexGrow: '0', flexShrink: '0', flexBasis: 'auto', padding: `72px ${CARD_PAD} ${CARD_PAD} ${CARD_PAD}` },
1532
- contentStyle: Style.Scrim, contentScheme: Scheme.Inverse,
1619
+ contentStyle: Style$2.Scrim, contentScheme: Scheme.Inverse,
1533
1620
  title: { fontSize: TITLE_SIZE },
1534
1621
  badgeOnMedia: false,
1535
1622
  },
@@ -1537,7 +1624,7 @@ const LAYOUTS = {
1537
1624
  card: { flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', minHeight: 'auto', padding: CARD_PAD },
1538
1625
  media: { ...IN_FLOW, flexBasis: '72px' },
1539
1626
  image: { aspectRatio: '1 / 1', height: 'auto', minHeight: 'auto', borderRadius: IMAGE_RADIUS },
1540
- content: GROWING_CONTENT, contentStyle: Style.Box, contentScheme: Scheme.Default,
1627
+ content: GROWING_CONTENT, contentStyle: Style$2.Box, contentScheme: Scheme.Default,
1541
1628
  title: { fontSize: LEAD_SIZE },
1542
1629
  badgeOnMedia: false,
1543
1630
  partsOff: [CardPart.Text, CardPart.Actions],
@@ -1548,7 +1635,7 @@ const STACK_KEYS = {
1548
1635
  media: Object.keys(LAYOUTS[CardLayout.Side].stack.media),
1549
1636
  image: Object.keys(LAYOUTS[CardLayout.Side].stack.image),
1550
1637
  };
1551
- function make(create, key, type, role, elementStyle, props = {}, label) {
1638
+ function make$1(create, key, type, role, elementStyle, props = {}, label) {
1552
1639
  const node = create({ key, type, props, elementStyle });
1553
1640
  if (role)
1554
1641
  node.role = role;
@@ -1558,16 +1645,16 @@ function make(create, key, type, role, elementStyle, props = {}, label) {
1558
1645
  return node;
1559
1646
  }
1560
1647
  function buttonLabel(create, key, words) {
1561
- const label = make(create, key, Type.Text, null, Style.Label, { content: words, tag: 'span' });
1562
- writeStyles(label, LABEL_BASE);
1648
+ const label = make$1(create, key, Type$2.Text, null, Style$2.Label, { content: words, tag: 'span' });
1649
+ writeStyles(label, LABEL_BASE$1);
1563
1650
  return label;
1564
1651
  }
1565
1652
  /** Each optional part (and the locked title) as the recipe builds it. */
1566
1653
  const PART_BUILDERS = {
1567
1654
  [CardPart.Media]: (create) => {
1568
- const media = make(create, 'media', Type.Container, CardRole.Media, Style.Box, {}, 'Media');
1569
- const image = make(create, 'image', Type.Image, null, Style.Media, { src: '', alt: '' });
1570
- writeStyles(image, IMAGE_BASE);
1655
+ const media = make$1(create, 'media', Type$2.Container, CardRole.Media, Style$2.Box, {}, 'Media');
1656
+ const image = make$1(create, 'image', Type$2.Image, null, Style$2.Media, { src: '', alt: '' });
1657
+ writeStyles(image, IMAGE_BASE$2);
1571
1658
  media.children = [image];
1572
1659
  return media;
1573
1660
  },
@@ -1577,14 +1664,14 @@ const PART_BUILDERS = {
1577
1664
  badge.label = 'Badge';
1578
1665
  return badge;
1579
1666
  },
1580
- [CardPart.Eyebrow]: (create, words) => make(create, 'eyebrow', Type.Text, CardRole.Eyebrow, Style.Eyebrow, { content: words.eyebrow, tag: 'p' }, 'Eyebrow'),
1581
- [CardPart.Text]: (create, words) => make(create, 'text', Type.Text, CardRole.Text, Style.Body, { content: words.text, tag: 'p' }, 'Text'),
1667
+ [CardPart.Eyebrow]: (create, words) => make$1(create, 'eyebrow', Type$2.Text, CardRole.Eyebrow, Style$2.Eyebrow, { content: words.eyebrow, tag: 'p' }, 'Eyebrow'),
1668
+ [CardPart.Text]: (create, words) => make$1(create, 'text', Type$2.Text, CardRole.Text, Style$2.Body, { content: words.text, tag: 'p' }, 'Text'),
1582
1669
  [CardPart.Actions]: (create, words) => {
1583
- const actions = make(create, 'actions', Type.Container, CardRole.Actions, Style.Box, {}, 'Buttons');
1584
- const primary = make(create, 'primary', Type.ButtonBlock, null, Style.Primary);
1670
+ const actions = make$1(create, 'actions', Type$2.Container, CardRole.Actions, Style$2.Box, {}, 'Buttons');
1671
+ const primary = make$1(create, 'primary', Type$2.ButtonBlock, null, Style$2.Primary);
1585
1672
  primary.children = [buttonLabel(create, 'primary-label', words.primary)];
1586
- const ghost = make(create, 'secondary', Type.ButtonBlock, null, Style.Ghost);
1587
- const icon = make(create, 'secondary-icon', Type.Icon, null, Style.Icon, { iconClass: SAVE_ICON });
1673
+ const ghost = make$1(create, 'secondary', Type$2.ButtonBlock, null, Style$2.Ghost);
1674
+ const icon = make$1(create, 'secondary-icon', Type$2.Icon, null, Style$2.Icon, { iconClass: SAVE_ICON });
1588
1675
  writeStyles(icon, INLINE_ICON);
1589
1676
  ghost.children = [icon, buttonLabel(create, 'secondary-label', words.secondary)];
1590
1677
  actions.children = [primary, ghost];
@@ -1592,15 +1679,15 @@ const PART_BUILDERS = {
1592
1679
  },
1593
1680
  };
1594
1681
  function buildTitle(create, words) {
1595
- return make(create, 'title', Type.Heading, CardRole.Title, Style.Title, { content: words.title, level: 'h3', [LINK_TYPE]: LinkType.None }, 'Title');
1682
+ return make$1(create, 'title', Type$2.Heading, CardRole.Title, Style$2.Title, { content: words.title, level: 'h3', [LINK_TYPE]: LinkType.None }, 'Title');
1596
1683
  }
1597
1684
  /** A fresh Card v2: every part a node with its role and element style, laid out by `options.layout`. */
1598
1685
  function buildCardRecipe(create, options = {}) {
1599
1686
  const words = { ...CARD_WORDS, ...(options.words ?? {}) };
1600
1687
  const tone = options.badgeTone ?? BadgeTone.Accent;
1601
1688
  const on = (part) => options.parts?.[part] !== false;
1602
- const card = make(create, 'card', Type.Container, CardRole.Card, Style.Card, {}, 'Card');
1603
- const content = make(create, 'content', Type.Container, CardRole.Content, Style.Box, {}, 'Content');
1689
+ const card = make$1(create, 'card', Type$2.Container, CardRole.Card, Style$2.Card, {}, 'Card');
1690
+ const content = make$1(create, 'content', Type$2.Container, CardRole.Content, Style$2.Box, {}, 'Content');
1604
1691
  content.children = [
1605
1692
  ...(on(CardPart.Eyebrow) ? [PART_BUILDERS[CardPart.Eyebrow](create, words, tone)] : []),
1606
1693
  buildTitle(create, words),
@@ -1631,7 +1718,7 @@ function cardRootOf(node, path) {
1631
1718
  function cardPartsOf(card) {
1632
1719
  return Object.fromEntries(CARD_PARTS.map(part => [part, !!findByRole(card, CARD_PART_ROLE[part])]));
1633
1720
  }
1634
- const imageOf = (media) => media?.children?.find(c => c.type === Type.Image);
1721
+ const imageOf = (media) => media?.children?.find(c => c.type === Type$2.Image);
1635
1722
  /** Put the badge where the layout says: the Media's top corner, or the head of the content column
1636
1723
  * (Image behind text, Compact, or no Media at all). The corner's insets are logical, in iox-design.css
1637
1724
  * (mirrored in RTL); `position` is the node's own, because the editing canvas forces every node's
@@ -1663,7 +1750,7 @@ function applyPartLayout(card, spec, part, baseline) {
1663
1750
  setStack(part, spec.stack?.media, STACK_KEYS.media);
1664
1751
  const image = imageOf(part);
1665
1752
  if (image) {
1666
- writeStyles(image, { ...IMAGE_BASE, ...spec.image });
1753
+ writeStyles(image, { ...IMAGE_BASE$2, ...spec.image });
1667
1754
  setStack(image, spec.stack?.image, STACK_KEYS.image);
1668
1755
  }
1669
1756
  return;
@@ -1835,6 +1922,317 @@ function setCardLinked(card, on) {
1835
1922
  return writeProp(title, LINK_TYPE, next);
1836
1923
  }
1837
1924
 
1925
+ /**
1926
+ * THE ACCESSIBLE CAROUSEL (CC3 — complex-components plan §7c, board 6; board 9 Q13). The Slider's controls are no
1927
+ * longer drawn by Swiper: Previous, Next, Pause and Pagination are REAL nodes with roles — selectable on the canvas,
1928
+ * in the layer tree and the breadcrumb, and styled on their own (R2) — handed to Swiper as its navigation and
1929
+ * pagination elements.
1930
+ *
1931
+ * SliderContainer (.swiper)
1932
+ * ├─ ButtonBlock `button-secondary` role slider.pause FIRST focus stop; present while Autoplay is on, locked
1933
+ * ├─ ButtonBlock `button-secondary` role slider.prev › Icon caret (points the reading way in RTL)
1934
+ * ├─ ButtonBlock `button-secondary` role slider.next › Icon caret
1935
+ * ├─ (the slides — `.swiper-wrapper` › SliderSlide …)
1936
+ * └─ Container role slider.pagination Swiper draws the dots (or "3 / 6") inside it
1937
+ *
1938
+ * The parts render INSIDE the `.swiper` element, never inside `.swiper-wrapper` (slider.md: a `.swiper-slide` must be
1939
+ * the wrapper's direct child): the parts that come before the first slide in the tree render before the wrapper, the
1940
+ * rest after it. By default they sit over the slides like Swiper's own controls (the shared CSS, iox-design.css, at
1941
+ * zero specificity — a part's own styles win), so a saved slider migrated from Swiper-drawn controls keeps its look.
1942
+ *
1943
+ * The Bindings switches stay the source of truth: Navigation arrows ⇔ Previous + Next, Pagination ⇔ Pagination,
1944
+ * Autoplay ⇔ Pause (WCAG 2.2.2 — "added while Autoplay is on, cannot be removed"). `syncSliderParts` is the one edit.
1945
+ */
1946
+ var SliderRole;
1947
+ (function (SliderRole) {
1948
+ SliderRole["Pause"] = "slider.pause";
1949
+ SliderRole["Prev"] = "slider.prev";
1950
+ SliderRole["Next"] = "slider.next";
1951
+ SliderRole["Pagination"] = "slider.pagination";
1952
+ })(SliderRole || (SliderRole = {}));
1953
+ /** Parts in the order they are stamped: the pause control first (APG: the first focus stop), the dots last. */
1954
+ const SLIDER_PART_ROLES = [SliderRole.Pause, SliderRole.Prev, SliderRole.Next, SliderRole.Pagination];
1955
+ const SLIDER_TYPE = 'SliderContainer';
1956
+ const SLIDE_TYPE = 'SliderSlide';
1957
+ /** The Bindings switches that own the parts. */
1958
+ var SliderFlag;
1959
+ (function (SliderFlag) {
1960
+ SliderFlag["Navigation"] = "enableNavigation";
1961
+ SliderFlag["Pagination"] = "enablePagination";
1962
+ SliderFlag["Autoplay"] = "enableAutoplay";
1963
+ })(SliderFlag || (SliderFlag = {}));
1964
+ const FLAG_PARTS = [
1965
+ [SliderFlag.Autoplay, [SliderRole.Pause]],
1966
+ [SliderFlag.Navigation, [SliderRole.Prev, SliderRole.Next]],
1967
+ [SliderFlag.Pagination, [SliderRole.Pagination]],
1968
+ ];
1969
+ /** The trait that names the carousel (Slider ▸ Accessibility ▸ Carousel name) — the A11y trait every named part uses. */
1970
+ const SLIDER_NAME_TRAIT = 'ariaLabel';
1971
+ function isSliderPart(node) {
1972
+ return !!node?.role && SLIDER_PART_ROLES.includes(node.role);
1973
+ }
1974
+ function isSlider(node) {
1975
+ return node?.type === SLIDER_TYPE;
1976
+ }
1977
+ /** The slider's own part playing `role` (a direct child), or undefined. */
1978
+ function sliderPart(slider, role) {
1979
+ return (slider.children ?? []).find(c => c?.role === role);
1980
+ }
1981
+ /** True when the slider's controls are parts (it was created or migrated in CC3) — Swiper then draws none of its own. */
1982
+ function hasSliderParts(slider) {
1983
+ return (slider.children ?? []).some(isSliderPart);
1984
+ }
1985
+ /** A slider's children split as they render: parts before the first slide, the slides (into `.swiper-wrapper`), and
1986
+ * the parts after it. */
1987
+ function splitSliderChildren(children) {
1988
+ const lead = [];
1989
+ const slides = [];
1990
+ const trail = [];
1991
+ for (const child of children ?? []) {
1992
+ if (!isSliderPart(child))
1993
+ slides.push(child);
1994
+ else
1995
+ (slides.length ? trail : lead).push(child);
1996
+ }
1997
+ return { lead, slides, trail };
1998
+ }
1999
+ /** The slides only (the template slide of a bound slider is `sliderSlides(children)[0]`). */
2000
+ function sliderSlides(children) {
2001
+ return (children ?? []).filter(c => !isSliderPart(c));
2002
+ }
2003
+ // ── the parts' recipe ───────────────────────────────────────────────────────────────────────────────
2004
+ var Type$1;
2005
+ (function (Type) {
2006
+ Type["ButtonBlock"] = "ButtonBlock";
2007
+ Type["Icon"] = "Icon";
2008
+ Type["Container"] = "Container";
2009
+ })(Type$1 || (Type$1 = {}));
2010
+ var Style$1;
2011
+ (function (Style) {
2012
+ Style["Control"] = "button-secondary";
2013
+ Style["Icon"] = "icon";
2014
+ })(Style$1 || (Style$1 = {}));
2015
+ /** Each part's glyph (Phosphor) and its default accessible name. */
2016
+ const SLIDER_PART_GLYPH = {
2017
+ [SliderRole.Pause]: 'ph ph-pause',
2018
+ [SliderRole.Prev]: 'ph ph-caret-left',
2019
+ [SliderRole.Next]: 'ph ph-caret-right',
2020
+ };
2021
+ const SLIDER_PLAY_GLYPH = 'ph-fill ph-play';
2022
+ const PART_LABEL = {
2023
+ [SliderRole.Pause]: 'Pause',
2024
+ [SliderRole.Prev]: 'Previous',
2025
+ [SliderRole.Next]: 'Next',
2026
+ [SliderRole.Pagination]: 'Pagination',
2027
+ };
2028
+ const ZERO$1 = '0px 0px 0px 0px';
2029
+ /** A round 44px control (board 6 `.st-ctl`), over the slides (z above `.swiper-wrapper`). The PLACE (which edge) is the
2030
+ * shared CSS's, in logical insets, so a carousel reads the same way in RTL. */
2031
+ const CONTROL_BASE = {
2032
+ position: 'absolute', zIndex: '10', display: 'flex', alignItems: 'center', justifyContent: 'center',
2033
+ width: '44px', height: '44px', minWidth: '44px', padding: ZERO$1, margin: ZERO$1, borderRadius: '999px',
2034
+ };
2035
+ const ICON_BASE = { fontSize: '20px', color: 'inherit' };
2036
+ /** The arrows' vertical middle over the slides: half the 44px control above the centre line. */
2037
+ const ARROW_TOP = 'calc(50% - 22px)';
2038
+ const PAGINATION_BASE = {
2039
+ position: 'absolute', zIndex: '10', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px',
2040
+ width: 'auto', minHeight: 'auto', height: 'auto', padding: ZERO$1, margin: ZERO$1, color: 'var(--iox-accent)',
2041
+ };
2042
+ const SWIPER_THEME_COLOR = '#007aff';
2043
+ function legacyLookOf(slider) {
2044
+ const color = readProp(slider, 'navigationColor');
2045
+ const size = Number(readProp(slider, 'navigationSize'));
2046
+ const dots = readProp(slider, 'paginationColor');
2047
+ return {
2048
+ navigationColor: typeof color === 'string' && color ? color : SWIPER_THEME_COLOR,
2049
+ navigationSize: Number.isFinite(size) && size > 0 ? size : 44,
2050
+ paginationColor: typeof dots === 'string' && dots ? dots : SWIPER_THEME_COLOR,
2051
+ };
2052
+ }
2053
+ /** One part, from the recipe (board 6: `button-secondary` controls, token dots) or in a migrated slider's legacy look. */
2054
+ function buildSliderPart(create, role, legacy) {
2055
+ if (role === SliderRole.Pagination) {
2056
+ const dots = makeCompositePart(create, { key: 'pagination', type: Type$1.Container, props: {}, elementStyle: NO_ELEMENT_STYLE }, role, PART_LABEL[role], legacy?.paginationColor ? { ...PAGINATION_BASE, color: legacy.paginationColor } : PAGINATION_BASE);
2057
+ dots.children = [];
2058
+ return dots;
2059
+ }
2060
+ const key = role.split('.')[1];
2061
+ // Previous / Next sit at the vertical middle (their own `top`, so a part set back in flow drops it); the inline edge
2062
+ // is the shared CSS's, logical, so the arrows swap sides in RTL.
2063
+ const placed = role === SliderRole.Pause ? CONTROL_BASE : { ...CONTROL_BASE, top: ARROW_TOP };
2064
+ const base = legacy
2065
+ ? { ...placed, backgroundColor: 'transparent', borderWidth: ZERO$1, color: legacy.navigationColor ?? SWIPER_THEME_COLOR, boxShadow: 'none' }
2066
+ : placed;
2067
+ // Previous / Next carry their accessible name (Swiper's A11y module reads it); the pause control's name is its
2068
+ // runtime's — it says what pressing it will do.
2069
+ const props = { tag: 'button', type: 'button' };
2070
+ if (role !== SliderRole.Pause)
2071
+ props[SLIDER_NAME_TRAIT] = role === SliderRole.Prev ? SLIDER_WORDS.en.prev : SLIDER_WORDS.en.next;
2072
+ const button = makeCompositePart(create, { key, type: Type$1.ButtonBlock, props, elementStyle: legacy ? NO_ELEMENT_STYLE : Style$1.Control }, role, PART_LABEL[role], base);
2073
+ const iconBase = legacy ? { ...ICON_BASE, fontSize: `${Math.round((legacy.navigationSize ?? 44) * 0.6)}px` } : ICON_BASE;
2074
+ const icon = makeCompositePart(create, { key: `${key}-icon`, type: Type$1.Icon, props: { iconClass: SLIDER_PART_GLYPH[role] }, elementStyle: legacy ? NO_ELEMENT_STYLE : Style$1.Icon }, null, 'Icon', iconBase);
2075
+ button.children = [icon];
2076
+ return button;
2077
+ }
2078
+ const truthy = (v) => v === true || v === 'true';
2079
+ /** The parts the slider's switches call for, in stamping order. */
2080
+ function wantedSliderParts(slider) {
2081
+ return FLAG_PARTS.filter(([flag]) => truthy(readProp(slider, flag))).flatMap(([, roles]) => roles);
2082
+ }
2083
+ /**
2084
+ * Make the slider's parts match its switches — ONE edit (a palette switch, the migration, the composer). Missing
2085
+ * parts are stamped from the recipe (or, `legacy`, in the migrated look) at their place: the pause control, Previous
2086
+ * and Next before the first slide, the pagination after the last; parts whose switch is off are removed. Everything
2087
+ * else (the slides, the author's edits to an existing part) is kept.
2088
+ */
2089
+ function syncSliderParts(slider, create, legacy) {
2090
+ const wanted = new Set(wantedSliderParts(slider));
2091
+ const children = [...(slider.children ?? [])];
2092
+ const removed = children.filter(c => isSliderPart(c) && !wanted.has(c.role));
2093
+ const kept = children.filter(c => !removed.includes(c));
2094
+ const added = [];
2095
+ for (const role of SLIDER_PART_ROLES) {
2096
+ if (!wanted.has(role) || kept.some(c => c.role === role))
2097
+ continue;
2098
+ const part = buildSliderPart(create, role, legacy);
2099
+ added.push(part);
2100
+ if (role === SliderRole.Pagination) {
2101
+ kept.push(part);
2102
+ }
2103
+ else {
2104
+ // Before the first slide, after the parts that come before it in stamping order.
2105
+ const order = SLIDER_PART_ROLES.indexOf(role);
2106
+ const firstSlide = kept.findIndex(c => !isSliderPart(c));
2107
+ let at = firstSlide < 0 ? kept.length : firstSlide;
2108
+ for (let i = 0; i < kept.length; i++) {
2109
+ const r = kept[i].role;
2110
+ if (isSliderPart(kept[i]) && SLIDER_PART_ROLES.indexOf(r) > order && i < at) {
2111
+ at = i;
2112
+ break;
2113
+ }
2114
+ }
2115
+ kept.splice(at, 0, part);
2116
+ }
2117
+ }
2118
+ if (added.length || removed.length)
2119
+ slider.children = kept;
2120
+ return { added, removed };
2121
+ }
2122
+ /** True when a saved slider still relies on Swiper-drawn controls: a switch is on and no part exists yet. */
2123
+ function needsSliderMigration(slider) {
2124
+ return !!slider && isSlider(slider) && !hasSliderParts(slider) && wantedSliderParts(slider).length > 0;
2125
+ }
2126
+ /**
2127
+ * The migration of a saved slider (board 9 Q13): its Swiper-drawn arrows, dots — and, when Autoplay is on, the pause
2128
+ * control WCAG 2.2.2 requires — become parts, in the place and the colours Swiper drew them. Idempotent: a slider
2129
+ * that already has parts is left alone. True when it changed.
2130
+ */
2131
+ function migrateSliderControls(slider, create) {
2132
+ if (!needsSliderMigration(slider))
2133
+ return false;
2134
+ const change = syncSliderParts(slider, create, legacyLookOf(slider));
2135
+ return change.added.length > 0;
2136
+ }
2137
+ /**
2138
+ * A switch flipped in the panel: on a slider still on Swiper-drawn controls, migrate it first (so its other controls
2139
+ * keep their look), then stamp / remove the parts the new switch calls for. ONE edit.
2140
+ */
2141
+ function setSliderFlag(slider, flag, on, create) {
2142
+ const wasLegacy = !hasSliderParts(slider);
2143
+ const before = wantedSliderParts(slider);
2144
+ const migrated = [];
2145
+ if (wasLegacy && before.length) {
2146
+ syncSliderParts(slider, create, legacyLookOf(slider)).added.forEach(p => migrated.push(p));
2147
+ }
2148
+ writeProp(slider, flag, on);
2149
+ const change = syncSliderParts(slider, create);
2150
+ return { added: [...migrated, ...change.added], removed: change.removed };
2151
+ }
2152
+ const SLIDER_WORDS = {
2153
+ en: {
2154
+ stop: 'Stop automatic slide show', start: 'Start automatic slide show', prev: 'Previous slide', next: 'Next slide',
2155
+ carousel: 'carousel', slide: 'slide', slideLabel: '{{index}} of {{slidesLength}}', goTo: 'Go to slide {{index}}',
2156
+ },
2157
+ he: {
2158
+ stop: 'עצירת המצגת האוטומטית', start: 'הפעלת המצגת האוטומטית', prev: 'השקופית הקודמת', next: 'השקופית הבאה',
2159
+ carousel: 'קרוסלה', slide: 'שקופית', slideLabel: '{{index}} מתוך {{slidesLength}}', goTo: 'מעבר לשקופית {{index}}',
2160
+ },
2161
+ };
2162
+ function sliderWords(lang) {
2163
+ return (lang ?? '').toLowerCase().startsWith('he') ? SLIDER_WORDS.he : SLIDER_WORDS.en;
2164
+ }
2165
+ /** The wrapper's live region (APG): silent while the slides move on their own, polite otherwise. */
2166
+ function sliderLiveRegion(rotating) {
2167
+ return rotating ? 'off' : 'polite';
2168
+ }
2169
+ /**
2170
+ * The pause control's state (APG carousel): its NAME says what it will do ("Stop automatic slide show" while it
2171
+ * rotates, "Start automatic slide show" when stopped). An author who named the control keeps the name, and the
2172
+ * state is `aria-pressed` instead (pressed = stopped).
2173
+ */
2174
+ function sliderPauseAttributes(rotating, words = SLIDER_WORDS.en, authorName) {
2175
+ const name = (authorName ?? '').trim();
2176
+ return name
2177
+ ? { 'aria-label': name, 'aria-pressed': rotating ? 'false' : 'true' }
2178
+ : { 'aria-label': rotating ? words.stop : words.start };
2179
+ }
2180
+ /** The pause control's glyph: pause while it rotates, play when stopped. */
2181
+ function sliderPauseGlyph(rotating) {
2182
+ return rotating ? SLIDER_PART_GLYPH[SliderRole.Pause] : SLIDER_PLAY_GLYPH;
2183
+ }
2184
+ /** Board 6's sample quotes — PLACEHOLDERS: testimonials are facts the studio gives, never invented. */
2185
+ const CAROUSEL_QUOTES = [
2186
+ { quote: 'A sample quote, two lines long, in the client’s own words.', who: '[Client name] · [Project, city]' },
2187
+ { quote: 'Testimonials are facts: replace these with words a client actually said.', who: '[Client name] · [Project, city]' },
2188
+ { quote: 'A third sample quote so the rotation has somewhere to go.', who: '[Client name] · [Project, city]' },
2189
+ ];
2190
+ /** A quote card. The carousel's controls sit in a ROW above the slides and the dots below them (board 6), so the
2191
+ * card needs no room kept for an overlay. */
2192
+ const QUOTE_SLIDE_BASE = {
2193
+ width: '100%', aspectRatio: 'auto', minHeight: 'auto', height: 'auto', display: 'flex', flexDirection: 'column', gap: '12px', padding: '32px 32px 32px 32px',
2194
+ };
2195
+ /** Board 6's carousel: the pause control, Previous and Next in one row above the slides (in flow — they read in the
2196
+ * page's direction), the dots under them. */
2197
+ const ROW_CONTROL_BASE = { position: 'static', display: 'inline-flex', margin: '0px 4px 12px 4px' };
2198
+ const ROW_DOTS_BASE = { position: 'static', margin: '12px 0px 0px 0px' };
2199
+ const QUOTE_TEXT_BASE = { margin: ZERO$1, padding: ZERO$1, minHeight: 'auto', width: 'auto' };
2200
+ /**
2201
+ * The testimonial carousel (board 6): a named Slider, one quote per slide (each a `card`), rotating every 5s with its
2202
+ * control parts — the pause control first, Previous · Next, the dots. ONE recipe for the composer and the builder.
2203
+ */
2204
+ function buildCarouselRecipe(create, options = {}) {
2205
+ const slider = makeCompositePart(create, { key: 'carousel', type: SLIDER_TYPE, props: {}, elementStyle: NO_ELEMENT_STYLE }, null, 'Carousel', { width: '100%' });
2206
+ for (const [name, value] of Object.entries({
2207
+ // Rewind: Previous and Next are always there to press (board 6) — neither greys out at an end.
2208
+ slidesPerView: 1, spaceBetween: 0, loop: 'rewind', enableNavigation: true, enablePagination: true, enableAutoplay: true, autoplayDelay: 5000,
2209
+ autoplayPauseOnMouseEnter: true, autoHeight: true, [SLIDER_NAME_TRAIT]: options.name ?? 'What clients say',
2210
+ }))
2211
+ writeProp(slider, name, value);
2212
+ slider.children = (options.quotes ?? CAROUSEL_QUOTES).map((q, i) => {
2213
+ const slide = makeCompositePart(create, { key: `slide-${i + 1}`, type: SLIDE_TYPE, props: {}, elementStyle: 'card' }, null, 'Slide', QUOTE_SLIDE_BASE);
2214
+ slide.children = [
2215
+ makeCompositePart(create, { key: `quote-${i + 1}`, type: 'Text', props: { content: q.quote, tag: 'p' }, elementStyle: 'lead' }, null, 'Quote', QUOTE_TEXT_BASE),
2216
+ makeCompositePart(create, { key: `who-${i + 1}`, type: 'Text', props: { content: q.who, tag: 'p' }, elementStyle: 'small' }, null, 'Client', QUOTE_TEXT_BASE),
2217
+ ];
2218
+ return slide;
2219
+ });
2220
+ syncSliderParts(slider, create);
2221
+ for (const part of slider.children ?? []) {
2222
+ if (part.role === SliderRole.Pagination)
2223
+ writeStyles(part, ROW_DOTS_BASE);
2224
+ else if (isSliderPart(part))
2225
+ writeStyles(part, ROW_CONTROL_BASE);
2226
+ }
2227
+ return slider;
2228
+ }
2229
+ /** The slide a lightbox opens at for the clicked row (clamped to the slides there are). */
2230
+ function slideIndexForRow(rowIndex, slideCount) {
2231
+ if (!Number.isFinite(rowIndex) || rowIndex < 0 || slideCount <= 0)
2232
+ return 0;
2233
+ return Math.min(Math.floor(rowIndex), slideCount - 1);
2234
+ }
2235
+
1838
2236
  const COMPOSITE_ROLE_SPECS = {
1839
2237
  [CardRole.Card]: { parent: null },
1840
2238
  [CardRole.Media]: { parent: CardRole.Card, part: CardPart.Media },
@@ -1857,6 +2255,21 @@ const COMPOSITE_ROLE_SPECS = {
1857
2255
  [DisclosureRole.More]: { parent: DisclosureRole.Trigger },
1858
2256
  [DisclosureRole.Less]: { parent: DisclosureRole.Trigger },
1859
2257
  [DisclosureRole.Icon]: { parent: DisclosureRole.Trigger },
2258
+ // CC4a — a Tabs always keeps its tab list and its panels holder; a tab lives in the list and a panel in the
2259
+ // holder (each may be deleted — with its pair — but never moved out); the list takes only tabs, the holder only
2260
+ // panels, and a panel takes anything.
2261
+ [TabsRole.Tabs]: { parent: null },
2262
+ [TabsRole.List]: { parent: TabsRole.Tabs, locked: true, accepts: [TabsRole.Trigger] },
2263
+ [TabsRole.Panels]: { parent: TabsRole.Tabs, locked: true, accepts: [TabsRole.Panel] },
2264
+ [TabsRole.Trigger]: { parent: TabsRole.List, pinned: true },
2265
+ [TabsRole.Panel]: { parent: TabsRole.Panels, pinned: true },
2266
+ // CC3 — a Slider's controls are its own direct children (Swiper is handed them). The pause control stays while
2267
+ // Autoplay is on (WCAG 2.2.2): the Autoplay switch removes it, never Delete. The arrows and the dots follow
2268
+ // their switches and may be deleted.
2269
+ [SliderRole.Pause]: { parent: null, parentType: SLIDER_TYPE, locked: true },
2270
+ [SliderRole.Prev]: { parent: null, parentType: SLIDER_TYPE, pinned: true },
2271
+ [SliderRole.Next]: { parent: null, parentType: SLIDER_TYPE, pinned: true },
2272
+ [SliderRole.Pagination]: { parent: null, parentType: SLIDER_TYPE, pinned: true },
1860
2273
  };
1861
2274
  var StructureEdit;
1862
2275
  (function (StructureEdit) {
@@ -1870,6 +2283,8 @@ var StructureRefusal;
1870
2283
  StructureRefusal["LockedPart"] = "locked-part";
1871
2284
  /** A locked part can only live directly inside its own parent role. */
1872
2285
  StructureRefusal["LockedPlacement"] = "locked-placement";
2286
+ /** The new parent takes only certain roles (a tab list takes only tabs, CC4a). */
2287
+ StructureRefusal["NotAccepted"] = "not-accepted";
1873
2288
  })(StructureRefusal || (StructureRefusal = {}));
1874
2289
  /**
1875
2290
  * Why the structure guard refuses `edit` on `node`, or null when it is allowed. `ancestors` (Move only) =
@@ -1877,26 +2292,1229 @@ var StructureRefusal;
1877
2292
  */
1878
2293
  function structureRefusal(node, edit, ancestors = []) {
1879
2294
  const spec = node?.role ? COMPOSITE_ROLE_SPECS[node.role] : undefined;
1880
- if (!spec?.locked)
2295
+ const parent = ancestors[ancestors.length - 1];
2296
+ if (edit === StructureEdit.Move && acceptsRefusal(node, parent))
2297
+ return StructureRefusal.NotAccepted;
2298
+ if (!spec?.locked && !spec?.pinned)
1881
2299
  return null;
1882
2300
  if (edit === StructureEdit.Delete)
1883
- return StructureRefusal.LockedPart;
1884
- const parent = ancestors[ancestors.length - 1];
1885
- return parent?.role === spec.parent ? null : StructureRefusal.LockedPlacement;
2301
+ return spec.locked ? StructureRefusal.LockedPart : null;
2302
+ const placed = spec.parentType ? parent?.type === spec.parentType : parent?.role === spec.parent;
2303
+ return placed ? null : StructureRefusal.LockedPlacement;
1886
2304
  }
1887
2305
  /** True when `node` is a locked part (shown with a lock and no delete in the layer tree). Deleting a whole
1888
2306
  * composite is always allowed — only its locked parts are refused. */
1889
2307
  function isLockedPart(node) {
1890
2308
  return !!(node?.role && COMPOSITE_ROLE_SPECS[node.role]?.locked);
1891
2309
  }
2310
+ /** Why `parent` refuses `node` as a child (it takes only certain roles), or null. */
2311
+ function acceptsRefusal(node, parent) {
2312
+ const accepts = parent?.role ? COMPOSITE_ROLE_SPECS[parent.role]?.accepts : undefined;
2313
+ if (!accepts)
2314
+ return null;
2315
+ return node?.role && accepts.includes(node.role) ? null : StructureRefusal.NotAccepted;
2316
+ }
2317
+
2318
+ const SLIDE_VISIBLE = 'swiper-slide-visible';
2319
+ const RUNTIME_KEY = '__ioxCarousel';
2320
+ const ROTATION_EVENTS = 'autoplayStart autoplayStop autoplayPause autoplayResume';
2321
+ const SLIDE_EVENTS = 'slideChange transitionEnd resize slidesLengthChange';
2322
+ const AUTHOR_NAME_KEY = '__ioxAuthorName';
2323
+ /** The name the AUTHOR gave the pause control (its rendered `aria-label` before this runtime first relabelled it). */
2324
+ function authorNameOf(el) {
2325
+ if (!el)
2326
+ return null;
2327
+ const host = el;
2328
+ if (host[AUTHOR_NAME_KEY] === undefined)
2329
+ host[AUTHOR_NAME_KEY] = el.getAttribute('aria-label') ?? '';
2330
+ return host[AUTHOR_NAME_KEY] || null;
2331
+ }
2332
+ /** The part of `role` that belongs to THIS slider (never one of a slider nested in a slide). */
2333
+ function sliderPartElement(sliderEl, role) {
2334
+ const cls = `iox-role-${role.replace('.', '-')}`;
2335
+ for (const el of Array.from(sliderEl.querySelectorAll(`.${cls}`))) {
2336
+ if (el.closest('.swiper') === sliderEl && !el.closest('.swiper-wrapper'))
2337
+ return el;
2338
+ }
2339
+ return null;
2340
+ }
2341
+ class CarouselA11y {
2342
+ constructor(swiper, options = {}) {
2343
+ this.swiper = swiper;
2344
+ /** The visitor pressed pause: rotation stays stopped until they press it again. */
2345
+ this.stoppedByVisitor = false;
2346
+ this.focusInside = false;
2347
+ /** The pointer is over the carousel and Swiper holds rotation for it (`pauseOnMouseEnter`). */
2348
+ this.hovering = false;
2349
+ this.cleanups = [];
2350
+ const el = swiper.el;
2351
+ this.words = options.words ?? sliderWords(el.ownerDocument?.documentElement?.getAttribute('lang'));
2352
+ this.pause = sliderPartElement(el, SliderRole.Pause);
2353
+ this.authorName = authorNameOf(this.pause);
2354
+ this.listen(el, 'focusin', () => this.onFocusIn());
2355
+ this.listen(el, 'pointerenter', () => { this.hovering = this.hoverPauses; this.sync(); });
2356
+ this.listen(el, 'pointerleave', () => { this.hovering = false; this.sync(); });
2357
+ this.listen(el, 'focusout', (e) => this.onFocusOut(e));
2358
+ if (this.pause)
2359
+ this.listen(this.pause, 'click', (e) => { e.preventDefault(); this.togglePause(); });
2360
+ const sync = () => this.sync();
2361
+ swiper.on(ROTATION_EVENTS, sync);
2362
+ swiper.on(SLIDE_EVENTS, sync);
2363
+ this.cleanups.push(() => { swiper.off(ROTATION_EVENTS, sync); swiper.off(SLIDE_EVENTS, sync); });
2364
+ el[RUNTIME_KEY] = this;
2365
+ this.sync();
2366
+ }
2367
+ /** True while the slides move on their own: autoplay runs and nothing holds it — not focus inside, not the
2368
+ * pointer over it. (Swiper's own `paused` also flips during every transition, so it is not read here.) */
2369
+ get rotating() {
2370
+ return !!this.swiper.autoplay?.running && !this.focusInside && !this.hovering;
2371
+ }
2372
+ get hoverPauses() {
2373
+ const autoplay = this.swiper.params?.autoplay;
2374
+ return typeof autoplay === 'object' && !!autoplay?.pauseOnMouseEnter;
2375
+ }
2376
+ /** The pause control: stop rotation for good, or start it again. */
2377
+ togglePause() {
2378
+ const a = this.swiper.autoplay;
2379
+ if (!a)
2380
+ return;
2381
+ if (a.running) {
2382
+ this.stoppedByVisitor = true;
2383
+ a.stop();
2384
+ }
2385
+ else {
2386
+ this.stoppedByVisitor = false;
2387
+ a.start();
2388
+ // Focus is on the pause control itself — inside the carousel: the rotation it just started waits until
2389
+ // focus leaves (APG), but the control already reads "stop".
2390
+ if (this.focusInside)
2391
+ a.pause();
2392
+ }
2393
+ this.sync();
2394
+ }
2395
+ destroy() {
2396
+ this.cleanups.splice(0).forEach(fn => fn());
2397
+ delete this.swiper.el[RUNTIME_KEY];
2398
+ }
2399
+ listen(target, type, fn) {
2400
+ target.addEventListener(type, fn);
2401
+ this.cleanups.push(() => target.removeEventListener(type, fn));
2402
+ }
2403
+ onFocusIn() {
2404
+ this.focusInside = true;
2405
+ if (this.swiper.autoplay?.running && !this.swiper.autoplay.paused)
2406
+ this.swiper.autoplay.pause();
2407
+ this.sync();
2408
+ }
2409
+ onFocusOut(e) {
2410
+ const next = e.relatedTarget;
2411
+ if (next && this.swiper.el.contains(next))
2412
+ return;
2413
+ this.focusInside = false;
2414
+ if (!this.stoppedByVisitor && this.swiper.autoplay?.running && this.swiper.autoplay.paused)
2415
+ this.swiper.autoplay.resume();
2416
+ this.sync();
2417
+ }
2418
+ /** Re-state the carousel: the live region, the pause control, the slides out of view. */
2419
+ sync() {
2420
+ if (this.swiper.destroyed)
2421
+ return;
2422
+ const rotating = this.rotating;
2423
+ this.swiper.wrapperEl?.setAttribute('aria-live', sliderLiveRegion(rotating));
2424
+ if (this.pause) {
2425
+ const running = !!this.swiper.autoplay?.running;
2426
+ const attrs = sliderPauseAttributes(running, this.words, this.authorName);
2427
+ for (const [name, value] of Object.entries(attrs))
2428
+ this.pause.setAttribute(name, value);
2429
+ if (!('aria-pressed' in attrs))
2430
+ this.pause.removeAttribute('aria-pressed');
2431
+ const icon = this.pause.querySelector('i');
2432
+ if (icon)
2433
+ icon.className = `${sliderPauseGlyph(running)} ${Array.from(icon.classList).filter(c => !c.startsWith('ph')).join(' ')}`.trim();
2434
+ }
2435
+ this.syncHiddenSlides();
2436
+ }
2437
+ syncHiddenSlides() {
2438
+ const slides = this.swiper.slides ?? [];
2439
+ const anyVisibleMarked = slides.some(s => s.classList?.contains(SLIDE_VISIBLE));
2440
+ slides.forEach((slide, i) => {
2441
+ const visible = anyVisibleMarked ? slide.classList.contains(SLIDE_VISIBLE) : i === this.swiper.activeIndex;
2442
+ if (visible) {
2443
+ slide.removeAttribute('inert');
2444
+ slide.removeAttribute('aria-hidden');
2445
+ }
2446
+ else {
2447
+ slide.setAttribute('inert', '');
2448
+ slide.setAttribute('aria-hidden', 'true');
2449
+ }
2450
+ });
2451
+ }
2452
+ }
2453
+ /** The carousel runtime attached to a slider element, if any. */
2454
+ function carouselOf(el) {
2455
+ return el?.[RUNTIME_KEY] ?? null;
2456
+ }
2457
+ /** Attach once per Swiper (a re-init gets a fresh runtime). */
2458
+ function attachCarouselA11y(swiper, options = {}) {
2459
+ carouselOf(swiper.el)?.destroy();
2460
+ return new CarouselA11y(swiper, options);
2461
+ }
2462
+
2463
+ /**
2464
+ * THE LIGHTBOX (CC3 — complex-components plan §7d, board 6). Not a component: a Dialog holding a Slider bound to the
2465
+ * SAME source as the grid of thumbnails. The one new thing is `openDialog`'s "Open at the clicked item"
2466
+ * (`slideTo: 'row'`): a thumbnail in a repeater row opens the lightbox at ITS slide.
2467
+ *
2468
+ * Dialog `dialog` · centre · full viewport · Inverse scheme "Lightbox" (a free-standing Dialog: board A4)
2469
+ * ├─ ButtonBlock `button-ghost` "Close" › Icon × click → closeDialog
2470
+ * └─ SliderContainer one slide in view · keyboard · the source's rows
2471
+ * ├─ Previous · Next (slider.prev / slider.next, button-secondary) hidden on a phone (swipe)
2472
+ * ├─ SliderSlide › Image `media` (contain, keeps its alt) + Text `small` caption
2473
+ * └─ Pagination (slider.pagination) — the counter "3 / 6"
2474
+ *
2475
+ * Keyboard ← → are the Slider's Keyboard module; Esc and the focus trap are the `<dialog>`'s; focus returns to the
2476
+ * thumbnail that opened it (the dialog opener's own rule).
2477
+ */
2478
+ const SLIDE_TO_ROW = 'row';
2479
+ /** The attribute a renderer writes on every repeated row (the context-state provenance, both engine paths, the canvas). */
2480
+ const ROW_INDEX_ATTRIBUTE = 'data-iox-row-idx';
2481
+ const LIGHTBOX_LABEL = 'Lightbox';
2482
+ const DIALOG = 'Dialog';
2483
+ const REPEATER = 'Repeater';
2484
+ const SOURCE = 'source';
2485
+ const SOURCE_PATH = 'sourcePath';
2486
+ const ZERO = '0px 0px 0px 0px';
2487
+ const DIALOG_BASE = {
2488
+ width: '100vw', maxWidth: '100vw', height: '100dvh', maxHeight: '100dvh', borderRadius: '0px', padding: '18px 18px 18px 18px',
2489
+ display: 'flex', flexDirection: 'column', gap: '0px',
2490
+ };
2491
+ /** The top row (board 6): the counter at its start, Close at its end — Close sits above the slider, which rises into
2492
+ * the row so its counter shares it. */
2493
+ const CLOSE_BASE = {
2494
+ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '44px', height: '44px', minWidth: '44px',
2495
+ padding: ZERO, margin: ZERO, borderRadius: '999px', alignSelf: 'flex-end', flexShrink: '0', position: 'relative', zIndex: '20',
2496
+ };
2497
+ const SLIDER_BASE = { width: '100%', height: '100%', minHeight: '0px', flexGrow: '1', margin: '-44px 0px 0px 0px' };
2498
+ const SLIDE_BASE = {
2499
+ width: '100%', height: '100%', aspectRatio: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'center',
2500
+ justifyContent: 'center', gap: '12px', padding: '56px 64px 16px 64px',
2501
+ };
2502
+ const IMAGE_BASE$1 = { width: '100%', height: 'auto', maxHeight: '72dvh', minHeight: '0px', objectFit: 'contain', aspectRatio: '4 / 3', flexShrink: '1' };
2503
+ const CAPTION_BASE = { textAlign: 'center', margin: ZERO, padding: ZERO, minHeight: 'auto', width: 'auto' };
2504
+ /** The counter "3 / 6" sits at the START of the top row, Close at its end (board 6). */
2505
+ const COUNTER_BASE = { color: 'var(--iox-text-muted)', top: '12px', alignItems: 'flex-start', justifyContent: 'flex-start' };
2506
+ /** On a phone the arrows give way to a swipe (board 6, "the arrows hide on touch-only widths"). */
2507
+ const LIGHTBOX_ARROWS_HIDE_AT = 'mobile';
2508
+ /** A click on `node` runs one instant action on `target`. */
2509
+ function onClick(node, type, target, params) {
2510
+ node.interactions = [{
2511
+ id: `${node.id ?? 'node'}-click`, trigger: 'click',
2512
+ actions: [{ type, target, duration: 0, delay: 0, easing: 'ease', ...(params ? { params } : {}) }],
2513
+ }];
2514
+ }
2515
+ /** A binding of `prop` to the row field `field` of `source` (`item` = the row itself). */
2516
+ function bindRow(node, prop, source, field) {
2517
+ const bindings = (node.bindings ?? []).filter((b) => b.prop !== prop);
2518
+ bindings.push({ prop, source, path: field === 'item' ? '' : field });
2519
+ node.bindings = bindings;
2520
+ }
2521
+ function buildSlide(create, key, options, rowAlias) {
2522
+ const slide = makeCompositePart(create, { key, type: SLIDE_TYPE, props: {}, elementStyle: NO_ELEMENT_STYLE }, null, 'Slide', SLIDE_BASE);
2523
+ const image = makeCompositePart(create, { key: `${key}-image`, type: 'Image', props: { src: '', alt: '' }, elementStyle: 'media' }, null, 'Image', IMAGE_BASE$1);
2524
+ const caption = makeCompositePart(create, { key: `${key}-caption`, type: 'Text', props: { content: 'Caption', tag: 'p' }, elementStyle: 'small' }, null, 'Caption', CAPTION_BASE);
2525
+ if (rowAlias) {
2526
+ bindRow(image, 'src', rowAlias, options.imageField ?? 'item');
2527
+ if (options.altField)
2528
+ bindRow(image, 'alt', rowAlias, options.altField);
2529
+ if (options.captionField)
2530
+ bindRow(caption, 'content', rowAlias, options.captionField);
2531
+ }
2532
+ slide.children = options.captionField || !rowAlias ? [image, caption] : [image];
2533
+ return slide;
2534
+ }
2535
+ /** A fresh lightbox: the Dialog, its Close part and the Slider (bound to `source`, or three manual slides). */
2536
+ function buildLightboxRecipe(create, options = {}) {
2537
+ const dialog = makeCompositePart(create, { key: 'lightbox', type: DIALOG, props: {}, elementStyle: 'dialog' }, null, LIGHTBOX_LABEL, DIALOG_BASE);
2538
+ writeProp(dialog, 'placement', 'center');
2539
+ writeProp(dialog, 'closeOnBackdrop', true);
2540
+ writeProp(dialog, 'showCloseButton', false);
2541
+ writeProp(dialog, 'ariaLabel', 'Gallery');
2542
+ setColorScheme(dialog, 'inverse');
2543
+ const close = makeCompositePart(create, { key: 'close', type: 'ButtonBlock', props: { tag: 'button', type: 'button', ariaLabel: 'Close' }, elementStyle: 'button-ghost' }, null, 'Close', CLOSE_BASE);
2544
+ close.children = [makeCompositePart(create, { key: 'close-icon', type: 'Icon', props: { iconClass: 'ph ph-x' }, elementStyle: 'icon' }, null, 'Icon', { fontSize: '22px', color: 'inherit' })];
2545
+ onClick(close, 'closeDialog', dialog.id ?? '');
2546
+ const slider = makeCompositePart(create, { key: 'slider', type: SLIDER_TYPE, props: {}, elementStyle: NO_ELEMENT_STYLE }, null, 'Slider', SLIDER_BASE);
2547
+ for (const [name, value] of Object.entries({
2548
+ slidesPerView: 1, spaceBetween: 0, loop: 'off', enableNavigation: true, enablePagination: true, paginationType: 'fraction',
2549
+ enableKeyboard: true, ariaLabel: 'Gallery', limit: 50,
2550
+ }))
2551
+ writeProp(slider, name, value);
2552
+ const rowAlias = options.source ? lastSegment(options.sourcePath) || options.source : null;
2553
+ if (options.source) {
2554
+ writeProp(slider, SOURCE, options.source);
2555
+ if (options.sourcePath)
2556
+ writeProp(slider, SOURCE_PATH, options.sourcePath);
2557
+ }
2558
+ const prev = buildSliderPart(create, SliderRole.Prev);
2559
+ const next = buildSliderPart(create, SliderRole.Next);
2560
+ for (const arrow of [prev, next]) {
2561
+ const layers = { ...(arrow.responsive ?? {}) };
2562
+ layers[LIGHTBOX_ARROWS_HIDE_AT] = { styleProps: { display: 'none' } };
2563
+ arrow.responsive = layers;
2564
+ }
2565
+ const counter = buildSliderPart(create, SliderRole.Pagination);
2566
+ counter.label = 'Counter';
2567
+ writeStyles(counter, COUNTER_BASE);
2568
+ const slides = rowAlias ? [buildSlide(create, 'slide', options, rowAlias)] : [1, 2, 3].map(n => buildSlide(create, `slide-${n}`, options, null));
2569
+ slider.children = [prev, next, ...slides, counter];
2570
+ dialog.children = [close, slider];
2571
+ return dialog;
2572
+ }
2573
+ const lastSegment = (path) => (path ? String(path).split('.').filter(Boolean).pop() ?? '' : '');
2574
+ /** What a Repeater (its `source` binding) or a bound Slider (its `source` / `sourcePath` traits) repeats over. */
2575
+ function repeatSourceOf(node) {
2576
+ if (!node || (node.type !== REPEATER && node.type !== SLIDER_TYPE))
2577
+ return null;
2578
+ const binding = (node.bindings ?? []).find(b => b?.prop === SOURCE);
2579
+ if (binding?.source)
2580
+ return { source: binding.source, path: binding.path ?? '' };
2581
+ const source = readProp(node, SOURCE);
2582
+ if (typeof source !== 'string' || !source)
2583
+ return null;
2584
+ const path = readProp(node, SOURCE_PATH);
2585
+ return { source, path: typeof path === 'string' ? path : '' };
2586
+ }
2587
+ function sameRepeatSource(a, b) {
2588
+ return !!a && !!b && a.source === b.source && (a.path ?? '') === (b.path ?? '');
2589
+ }
2590
+ /** The Slider a lightbox Dialog holds (the first one, depth first), or null. */
2591
+ function lightboxSliderOf(dialog) {
2592
+ for (const child of dialog?.children ?? []) {
2593
+ if (child.type === SLIDER_TYPE)
2594
+ return child;
2595
+ const below = lightboxSliderOf(child);
2596
+ if (below)
2597
+ return below;
2598
+ }
2599
+ return null;
2600
+ }
2601
+ /** The repeated rows the element is in: the nearest Repeater or bound Slider among its ancestors (`path` = root → the
2602
+ * element, inclusive). */
2603
+ function rowSourceOf(path) {
2604
+ for (let i = path.length - 2; i >= 0; i--) {
2605
+ const source = repeatSourceOf(path[i]);
2606
+ if (source)
2607
+ return source;
2608
+ }
2609
+ return null;
2610
+ }
2611
+ /**
2612
+ * Interactions ▸ openDialog ▸ "Open at the clicked item" is offered only when it can work (board 9): the clicked
2613
+ * element sits in a repeater row AND the target Dialog holds a Slider bound to the same source.
2614
+ */
2615
+ function canOpenAtClickedItem(ownerPath, dialog) {
2616
+ if (!dialog || dialog.type !== DIALOG)
2617
+ return false;
2618
+ return sameRepeatSource(rowSourceOf(ownerPath), repeatSourceOf(lightboxSliderOf(dialog)));
2619
+ }
2620
+ /** The repeated row an element sits in (its row index), 0 when it is in none (the canvas' template row). */
2621
+ function rowIndexOf(el) {
2622
+ const row = el?.closest?.(`[${ROW_INDEX_ATTRIBUTE}]`);
2623
+ const index = Number(row?.getAttribute(ROW_INDEX_ATTRIBUTE));
2624
+ return Number.isInteger(index) && index >= 0 ? index : 0;
2625
+ }
2626
+
2627
+ /**
2628
+ * TABS (CC4a — composite K2 T17–T20; complex-components boards 3 and 9, APPROVED 2026-10-20). Not a component: a
2629
+ * recipe of ordinary nodes, each part carrying a role, so every tab and every panel is selectable on the canvas, in
2630
+ * the layer tree and the breadcrumb, and wears its own element style (R2).
2631
+ *
2632
+ * Container `box` role tabs traits activateOn · orientation · ariaLabel (the list's name)
2633
+ * ├─ Container `tab-list` role tabs.list → role=tablist (locked) — or a Repeater (From content)
2634
+ * │ └─ ButtonBlock `tab` ×3 role tabs.trigger itemKey k1… → <button role=tab> › Text `button-label`
2635
+ * └─ Container `box` role tabs.panels (locked) — or a Repeater (From content)
2636
+ * └─ Container `box` ×3 role tabs.panel the SAME itemKey as its tab → role=tabpanel, hidden while off
2637
+ * ├─ Image `media` 4 / 3
2638
+ * └─ Text `body`
2639
+ *
2640
+ * A tab and its panel are a PAIR by `itemKey` — never by index, so a tab moved to the end still shows its own panel.
2641
+ * The behaviour is BUILT IN (composite plan D1 = B): exactly one item is on (`exclusive`), the APG tabs pattern with
2642
+ * AUTOMATIC activation (arrows move focus and activate, roving tabindex) — one runtime (`exclusive-runtime.ts`) on the
2643
+ * builder canvas and both engine paths, from the attribute planners below.
2644
+ */
2645
+ var TabsBehaviourTrait;
2646
+ (function (TabsBehaviourTrait) {
2647
+ /** Which pointer gesture turns a tab on: a click (always) or also a hover. */
2648
+ TabsBehaviourTrait["ActivateOn"] = "activateOn";
2649
+ /** The tab list across the top, or beside the panels. */
2650
+ TabsBehaviourTrait["Orientation"] = "orientation";
2651
+ /** The tab list's accessible name (`aria-label` on the role=tablist element) — the root's, edited in Accessibility. */
2652
+ TabsBehaviourTrait["Label"] = "ariaLabel";
2653
+ })(TabsBehaviourTrait || (TabsBehaviourTrait = {}));
2654
+ var TabsActivateOn;
2655
+ (function (TabsActivateOn) {
2656
+ TabsActivateOn["Click"] = "click";
2657
+ TabsActivateOn["Hover"] = "hover";
2658
+ })(TabsActivateOn || (TabsActivateOn = {}));
2659
+ var TabsOrientation;
2660
+ (function (TabsOrientation) {
2661
+ TabsOrientation["Horizontal"] = "horizontal";
2662
+ TabsOrientation["Vertical"] = "vertical";
2663
+ })(TabsOrientation || (TabsOrientation = {}));
2664
+ const TABS_ACTIVATE_ONS = [TabsActivateOn.Click, TabsActivateOn.Hover];
2665
+ const TABS_ORIENTATIONS = [TabsOrientation.Horizontal, TabsOrientation.Vertical];
2666
+ /** Tabs from content default to six rows: more tabs do not fit a row (board 9 Q13). */
2667
+ const TABS_CONTENT_LIMIT = 6;
2668
+ /** The breakpoint whose layer stacks a VERTICAL tabs (the list above the panels) and a panel's picture above its words. */
2669
+ const TABS_STACK_BREAKPOINT = CARD_STACK_BREAKPOINT;
2670
+ /** The words a fresh Tabs starts with — generic, the author's to replace (Card v2.1: never invented facts). */
2671
+ const TABS_WORDS = [
2672
+ { label: 'First tab', text: 'Say what this tab is about in a sentence or two. Its picture sits beside it.' },
2673
+ { label: 'Second tab', text: 'Say what this tab is about in a sentence or two. Its picture sits beside it.' },
2674
+ { label: 'Third tab', text: 'Say what this tab is about in a sentence or two. Its picture sits beside it.' },
2675
+ ];
2676
+ const TABS_NEW_TAB_WORDS = { label: 'New tab', text: 'Say what this tab is about in a sentence or two. Its picture sits beside it.' };
2677
+ var Type;
2678
+ (function (Type) {
2679
+ Type["Container"] = "Container";
2680
+ Type["ButtonBlock"] = "ButtonBlock";
2681
+ Type["Text"] = "Text";
2682
+ Type["Image"] = "Image";
2683
+ Type["Repeater"] = "Repeater";
2684
+ })(Type || (Type = {}));
2685
+ var Style;
2686
+ (function (Style) {
2687
+ Style["Root"] = "box";
2688
+ Style["List"] = "tab-list";
2689
+ Style["Tab"] = "tab";
2690
+ Style["Label"] = "button-label";
2691
+ Style["Panels"] = "box";
2692
+ Style["Panel"] = "box";
2693
+ Style["Media"] = "media";
2694
+ Style["Body"] = "body";
2695
+ })(Style || (Style = {}));
2696
+ const ZERO_BOX = '0px 0px 0px 0px';
2697
+ /** The list's hairline, as its element style writes it — read from the contract, never retyped. */
2698
+ const LIST_EDGE = String(elementStyleById(Style.List)?.base['borderWidth'] ?? ZERO_BOX);
2699
+ /** What each part holds — layout only; its LOOK is its element style. The list WRAPS at narrow widths (board 3). */
2700
+ const PART_BASE = {
2701
+ [TabsRole.Tabs]: { display: 'flex', width: '100%', minHeight: 'auto', height: 'auto' },
2702
+ [TabsRole.List]: { display: 'flex', gap: '4px', minHeight: 'auto', height: 'auto' },
2703
+ [TabsRole.Trigger]: { width: 'auto', minHeight: 'auto', height: 'auto', cursor: 'pointer' },
2704
+ [TabsRole.Panels]: { display: 'block', width: '100%', minWidth: '0px', minHeight: 'auto', height: 'auto' },
2705
+ // Board 3: a panel is any nodes at all — the recipe's is a picture beside its words.
2706
+ [TabsRole.Panel]: { display: 'grid', gridTemplateColumns: '1fr 1.2fr', gap: '24px', alignItems: 'center', width: '100%', minHeight: 'auto', height: 'auto' },
2707
+ };
2708
+ const LABEL_BASE = { width: 'auto', minHeight: 'auto', padding: ZERO_BOX, margin: ZERO_BOX };
2709
+ const IMAGE_BASE = { display: 'block', width: '100%', maxWidth: '100%', aspectRatio: '4 / 3', height: 'auto', minHeight: 'auto' };
2710
+ const BODY_BASE = { margin: ZERO_BOX, padding: ZERO_BOX, minHeight: 'auto' };
2711
+ /** On a phone a panel's picture sits above its words. */
2712
+ const PANEL_STACK = { gridTemplateColumns: '1fr' };
2713
+ const ORIENTATION_LAYOUT = {
2714
+ [TabsOrientation.Horizontal]: {
2715
+ root: { flexDirection: 'column', alignItems: 'stretch', gap: '20px' },
2716
+ list: { flexDirection: 'row', flexWrap: 'wrap', width: '100%', flexShrink: '1', borderWidth: LIST_EDGE },
2717
+ },
2718
+ [TabsOrientation.Vertical]: {
2719
+ root: { flexDirection: 'row', alignItems: 'flex-start', gap: '24px' },
2720
+ list: { flexDirection: 'column', flexWrap: 'nowrap', width: 'auto', flexShrink: '0', borderWidth: ZERO_BOX },
2721
+ rootStack: { flexDirection: 'column', alignItems: 'stretch', gap: '20px' },
2722
+ listStack: { width: '100%' },
2723
+ },
2724
+ };
2725
+ const ROOT_STACK_KEYS = ['flexDirection', 'alignItems', 'gap'];
2726
+ const LIST_STACK_KEYS = ['width'];
2727
+ function make(create, key, type, role, style, props = {}, label) {
2728
+ return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE[role] : undefined);
2729
+ }
2730
+ /** One tab and its panel, sharing `itemKey`. */
2731
+ function buildTabPair(create, words, itemKey) {
2732
+ const tab = make(create, `${itemKey}-tab`, Type.ButtonBlock, TabsRole.Trigger, Style.Tab, {}, 'Tab');
2733
+ tab.itemKey = itemKey;
2734
+ const label = make(create, `${itemKey}-label`, Type.Text, null, Style.Label, { content: words.label, tag: 'span' }, 'Text');
2735
+ writeStyles(label, LABEL_BASE);
2736
+ tab.children = [label];
2737
+ const panel = make(create, `${itemKey}-panel`, Type.Container, TabsRole.Panel, Style.Panel, {}, 'Panel');
2738
+ panel.itemKey = itemKey;
2739
+ writeLayerStyles(panel, TABS_STACK_BREAKPOINT, PANEL_STACK);
2740
+ const image = make(create, `${itemKey}-image`, Type.Image, null, Style.Media, { src: '', alt: '' }, 'Image');
2741
+ writeStyles(image, IMAGE_BASE);
2742
+ const text = make(create, `${itemKey}-text`, Type.Text, null, Style.Body, { content: words.text, tag: 'p' }, 'Text');
2743
+ writeStyles(text, BODY_BASE);
2744
+ panel.children = [image, text];
2745
+ return { tab, panel };
2746
+ }
2747
+ /** A fresh Tabs: three tabs, the first on, activated by a click, the list across the top (board 3). */
2748
+ function buildTabsRecipe(create, options = {}) {
2749
+ const root = make(create, 'tabs', Type.Container, TabsRole.Tabs, Style.Root, {}, 'Tabs');
2750
+ writeProp(root, TabsBehaviourTrait.ActivateOn, options.activateOn ?? TabsActivateOn.Click);
2751
+ writeProp(root, TabsBehaviourTrait.Orientation, options.orientation ?? TabsOrientation.Horizontal);
2752
+ if (options.label)
2753
+ writeProp(root, TabsBehaviourTrait.Label, options.label);
2754
+ const list = make(create, 'list', Type.Container, TabsRole.List, Style.List, {}, 'Tab list');
2755
+ const panels = make(create, 'panels', Type.Container, TabsRole.Panels, Style.Panels, {}, 'Panels');
2756
+ const pairs = (options.tabs ?? TABS_WORDS).map((w, i) => buildTabPair(create, w, `k${i + 1}`));
2757
+ list.children = pairs.map(p => p.tab);
2758
+ panels.children = pairs.map(p => p.panel);
2759
+ root.children = [list, panels];
2760
+ applyOrientation(root, options.orientation ?? TabsOrientation.Horizontal);
2761
+ return root;
2762
+ }
2763
+ // ── reading a tree ──────────────────────────────────────────────────────────────────────────────
2764
+ function isTabsRoot(node) {
2765
+ return node?.role === TabsRole.Tabs;
2766
+ }
2767
+ /** The tab list (the holder of the triggers). */
2768
+ function tabsListOf(root) {
2769
+ return (root.children ?? []).find(c => c.role === TabsRole.List);
2770
+ }
2771
+ /** The holder of the panels. */
2772
+ function tabsPanelsOf(root) {
2773
+ return (root.children ?? []).find(c => c.role === TabsRole.Panels);
2774
+ }
2775
+ /** The tabs in order — every tab (Manual), or the one template tab (From content). */
2776
+ function tabsTriggers(root) {
2777
+ return (tabsListOf(root)?.children ?? []).filter(c => c.role === TabsRole.Trigger);
2778
+ }
2779
+ /** The panels in their holder's order (their order does not pair them — their key does). */
2780
+ function tabsPanels(root) {
2781
+ return (tabsPanelsOf(root)?.children ?? []).filter(c => c.role === TabsRole.Panel);
2782
+ }
2783
+ /** True when the Tabs takes its tabs from content (its list is a Repeater, R3). */
2784
+ function isTabsFromContent(root) {
2785
+ return !!root && tabsListOf(root)?.type === Type.Repeater;
2786
+ }
2787
+ /** The panel paired with `tab` (the same key), or undefined. From content the one template panel pairs the template tab. */
2788
+ function tabPanelOf(root, tab) {
2789
+ if (isTabsFromContent(root))
2790
+ return tabsPanels(root)[0];
2791
+ return tab.itemKey ? tabsPanels(root).find(p => p.itemKey === tab.itemKey) : undefined;
2792
+ }
2793
+ /** The tab paired with `panel` (the same key), or undefined. */
2794
+ function tabTriggerOf(root, panel) {
2795
+ if (isTabsFromContent(root))
2796
+ return tabsTriggers(root)[0];
2797
+ return panel.itemKey ? tabsTriggers(root).find(t => t.itemKey === panel.itemKey) : undefined;
2798
+ }
2799
+ /** The other half of a tab ⇄ panel pair, or undefined for any other node. */
2800
+ function tabsPartnerOf(root, node) {
2801
+ if (node.role === TabsRole.Trigger)
2802
+ return tabPanelOf(root, node);
2803
+ if (node.role === TabsRole.Panel)
2804
+ return tabTriggerOf(root, node);
2805
+ return undefined;
2806
+ }
2807
+ /** The words a tab shows: its first Text's. */
2808
+ function tabLabelNodeOf(tab) {
2809
+ let hit;
2810
+ walkComposite(tab, n => { if (!hit && n !== tab && n.type === Type.Text)
2811
+ hit = n; });
2812
+ return hit;
2813
+ }
2814
+ const stripTags = (v) => String(v ?? '').replace(/<[^>]*>/g, '').trim();
2815
+ /** A tab's name in the Items list, the layer tree and the item-action picker: its words. */
2816
+ function tabLabel(tab) {
2817
+ const label = tabLabelNodeOf(tab);
2818
+ return label ? stripTags(readProp(label, 'content')) : '';
2819
+ }
2820
+ /** Items ▸ rename: the tab's words. */
2821
+ function renameTab(tab, words) {
2822
+ const label = tabLabelNodeOf(tab);
2823
+ return !!label && writeProp(label, 'content', words);
2824
+ }
2825
+ /** The first Text inside a panel (the words From content binds to). */
2826
+ function tabPanelTextOf(panel) {
2827
+ let hit;
2828
+ walkComposite(panel, n => { if (!hit && n !== panel && n.type === Type.Text)
2829
+ hit = n; });
2830
+ return hit;
2831
+ }
2832
+ /** The first Image inside a panel. */
2833
+ function tabPanelImageOf(panel) {
2834
+ let hit;
2835
+ walkComposite(panel, n => { if (!hit && n.type === Type.Image)
2836
+ hit = n; });
2837
+ return hit;
2838
+ }
2839
+ /** The root's behaviour (an unset trait reads its default: a click, the list across the top). */
2840
+ function tabsBehaviour(root) {
2841
+ const on = root ? readProp(root, TabsBehaviourTrait.ActivateOn) : undefined;
2842
+ const orientation = root ? readProp(root, TabsBehaviourTrait.Orientation) : undefined;
2843
+ return {
2844
+ activateOn: TABS_ACTIVATE_ONS.includes(on) ? on : TabsActivateOn.Click,
2845
+ orientation: TABS_ORIENTATIONS.includes(orientation) ? orientation : TabsOrientation.Horizontal,
2846
+ };
2847
+ }
2848
+ /** The tab list's accessible name (empty when unnamed). */
2849
+ function tabsLabel(root) {
2850
+ return root ? String(readProp(root, TabsBehaviourTrait.Label) ?? '').trim() : '';
2851
+ }
2852
+ /** Write the orientation preset into the root's and the list's layout (both orientations write the same keys). */
2853
+ function applyOrientation(root, orientation) {
2854
+ const spec = ORIENTATION_LAYOUT[orientation];
2855
+ const list = tabsListOf(root);
2856
+ writeStyles(root, spec.root);
2857
+ if (list)
2858
+ writeStyles(list, spec.list);
2859
+ if (spec.rootStack)
2860
+ writeLayerStyles(root, TABS_STACK_BREAKPOINT, spec.rootStack);
2861
+ else
2862
+ clearLayerStyles(root, TABS_STACK_BREAKPOINT, ROOT_STACK_KEYS);
2863
+ if (list) {
2864
+ if (spec.listStack)
2865
+ writeLayerStyles(list, TABS_STACK_BREAKPOINT, spec.listStack);
2866
+ else
2867
+ clearLayerStyles(list, TABS_STACK_BREAKPOINT, LIST_STACK_KEYS);
2868
+ }
2869
+ }
2870
+ /** Behaviour ▸ Activate on / Orientation (and Accessibility ▸ the list's name). False when the root has no such trait. */
2871
+ function setTabsBehaviour(root, patch) {
2872
+ let ok = true;
2873
+ if (patch.activateOn !== undefined)
2874
+ ok = writeProp(root, TabsBehaviourTrait.ActivateOn, patch.activateOn) && ok;
2875
+ if (patch.orientation !== undefined) {
2876
+ ok = writeProp(root, TabsBehaviourTrait.Orientation, patch.orientation) && ok;
2877
+ applyOrientation(root, patch.orientation);
2878
+ }
2879
+ if (patch.label !== undefined)
2880
+ ok = writeProp(root, TabsBehaviourTrait.Label, patch.label) && ok;
2881
+ return ok;
2882
+ }
2883
+ // ── editing (Items list, selection toolbar ▸ Add tab, the structure guard) ──────────────────────
2884
+ /** A key no tab of `root` holds yet (`k4`…). */
2885
+ function nextTabKey(root) {
2886
+ const taken = new Set([...tabsTriggers(root), ...tabsPanels(root)].map(n => n.itemKey).filter(Boolean));
2887
+ let n = tabsTriggers(root).length + 1;
2888
+ while (taken.has(`k${n}`))
2889
+ n++;
2890
+ return `k${n}`;
2891
+ }
2892
+ /** Items ▸ Add tab (and the toolbar's "Add tab"): a fresh tab after `after` (or at the end) AND its panel. */
2893
+ function addTabPair(root, create, after, words = TABS_NEW_TAB_WORDS) {
2894
+ const list = tabsListOf(root);
2895
+ const panels = tabsPanelsOf(root);
2896
+ if (!list || !panels || isTabsFromContent(root))
2897
+ return null;
2898
+ const pair = buildTabPair(create, words, nextTabKey(root));
2899
+ const tabs = [...(list.children ?? [])];
2900
+ const afterTab = after ? (after.role === TabsRole.Panel ? tabTriggerOf(root, after) : after) : null;
2901
+ const at = afterTab ? tabs.indexOf(afterTab) : -1;
2902
+ tabs.splice(at < 0 ? tabs.length : at + 1, 0, pair.tab);
2903
+ list.children = tabs;
2904
+ const afterPanel = afterTab ? tabPanelOf(root, afterTab) : null;
2905
+ const held = [...(panels.children ?? [])];
2906
+ const pAt = afterPanel ? held.indexOf(afterPanel) : -1;
2907
+ held.splice(pAt < 0 ? held.length : pAt + 1, 0, pair.panel);
2908
+ panels.children = held;
2909
+ return pair;
2910
+ }
2911
+ function moveIn(holder, node, toIndex, role) {
2912
+ const children = [...(holder.children ?? [])];
2913
+ const from = children.indexOf(node);
2914
+ if (from < 0)
2915
+ return;
2916
+ children.splice(from, 1);
2917
+ const same = children.filter(c => c.role === role);
2918
+ const anchor = same[Math.max(0, Math.min(toIndex, same.length))];
2919
+ children.splice(anchor ? children.indexOf(anchor) : children.length, 0, node);
2920
+ holder.children = children;
2921
+ }
2922
+ /** Items ▸ drag to reorder: move a tab — and its panel, so the layer tree reads in the same order — to `toIndex`.
2923
+ * The pairing never depended on the order. False when `tab` is not one of the root's tabs. */
2924
+ function moveTabPair(root, tab, toIndex) {
2925
+ const list = tabsListOf(root);
2926
+ const panels = tabsPanelsOf(root);
2927
+ if (!list || tab.role !== TabsRole.Trigger || !(list.children ?? []).includes(tab))
2928
+ return false;
2929
+ const panel = tabPanelOf(root, tab);
2930
+ moveIn(list, tab, toIndex, TabsRole.Trigger);
2931
+ if (panel && panels)
2932
+ moveIn(panels, panel, toIndex, TabsRole.Panel);
2933
+ return true;
2934
+ }
2935
+ /** Remove a tab and its panel (the pair) — what "Delete tab and its panel" does. True when the tab was removed. */
2936
+ function removeTabPair(root, node) {
2937
+ const tab = node.role === TabsRole.Panel ? tabTriggerOf(root, node) : node;
2938
+ const list = tabsListOf(root);
2939
+ if (!tab || !list || tab.role !== TabsRole.Trigger)
2940
+ return false;
2941
+ const panel = tabPanelOf(root, tab);
2942
+ list.children = (list.children ?? []).filter(c => c !== tab);
2943
+ const panels = tabsPanelsOf(root);
2944
+ if (panel && panels)
2945
+ panels.children = (panels.children ?? []).filter(c => c !== panel);
2946
+ return true;
2947
+ }
2948
+ var TabsEditRefusal;
2949
+ (function (TabsEditRefusal) {
2950
+ /** A Tabs keeps at least one tab (composite plan T18). */
2951
+ TabsEditRefusal["LastTab"] = "last-tab";
2952
+ })(TabsEditRefusal || (TabsEditRefusal = {}));
2953
+ /** Why removing `node` (a tab or a panel) from `root` is refused, or null. */
2954
+ function tabsRemovalRefusal(root, node) {
2955
+ if (!root || (node?.role !== TabsRole.Trigger && node?.role !== TabsRole.Panel))
2956
+ return null;
2957
+ return tabsTriggers(root).length <= 1 ? TabsEditRefusal.LastTab : null;
2958
+ }
2959
+ /** Every "Item open" (`trigger-on`) state inside a tab or a panel points at ITS tab or panel — the part that carries
2960
+ * `data-iox-on` while its item is on. */
2961
+ function tabsItemTriggerIds(root) {
2962
+ for (const part of [...tabsTriggers(root), ...tabsPanels(root)]) {
2963
+ if (!part.id)
2964
+ continue;
2965
+ walkComposite(part, n => {
2966
+ if (n === part || !n.stateStyles?.[ITEM_OPEN_STATE])
2967
+ return;
2968
+ n.stateTriggers = { ...(n.stateTriggers ?? {}), [ITEM_OPEN_STATE]: part.id };
2969
+ });
2970
+ }
2971
+ }
2972
+ function tabsScope(rootId, behaviour, keys, label = '') {
2973
+ return { rootId, behaviour, keys, active: keys[0] ?? null, label };
2974
+ }
2975
+ /** The item keys a manual Tabs pairs by — its tabs' own keys, in order (a tab with none by its index). */
2976
+ function manualTabKeys(root) {
2977
+ return tabsTriggers(root).map((t, i) => t.itemKey || `item-${i}`);
2978
+ }
2979
+ /** One item's render state: its key, whether it is on, and its ARIA ids (`iox-tab-<scope>-<key>-h` / `-p`). */
2980
+ function tabsItemState(scope, key) {
2981
+ return { key, on: scope.active === key, ids: accordionDomIds(scope.rootId, key, CompositeKind.Tabs) };
2982
+ }
2983
+ const ACTIVATE_ON_ATTRIBUTE = 'data-iox-activate-on';
2984
+ const ORIENTATION_ATTRIBUTE = 'data-iox-orientation';
2985
+ function tabsRootAttributes(behaviour, scopeId) {
2986
+ return {
2987
+ [COMPOSITE_ATTRIBUTE]: CompositeKind.Tabs,
2988
+ [ACTIVATE_ON_ATTRIBUTE]: behaviour.activateOn,
2989
+ [ORIENTATION_ATTRIBUTE]: behaviour.orientation,
2990
+ ...(scopeId ? { [SCOPE_ATTRIBUTE]: scopeId } : {}),
2991
+ };
2992
+ }
2993
+ /** The tab list: `role=tablist`, vertical said aloud, named by the root's label. */
2994
+ function tabListAttributes(behaviour, label = '') {
2995
+ return {
2996
+ role: 'tablist',
2997
+ ...(behaviour.orientation === TabsOrientation.Vertical ? { 'aria-orientation': 'vertical' } : {}),
2998
+ ...(label ? { 'aria-label': label } : {}),
2999
+ };
3000
+ }
3001
+ /** A tab: a real button that selects its panel — selected = the one tab in the Tab order (roving tabindex). */
3002
+ function tabTriggerAttributes(state) {
3003
+ return {
3004
+ type: 'button',
3005
+ role: 'tab',
3006
+ id: state.ids.trigger,
3007
+ 'aria-selected': state.on ? 'true' : 'false',
3008
+ 'aria-controls': state.ids.panel,
3009
+ tabindex: state.on ? '0' : '-1',
3010
+ [ITEM_KEY_ATTRIBUTE]: state.key,
3011
+ ...(state.on ? { [ON_ATTRIBUTE]: '' } : {}),
3012
+ };
3013
+ }
3014
+ /** A panel: `role=tabpanel`, named by its tab, a Tab stop of its own, and `hidden` (display:none) while off. */
3015
+ function tabPanelAttributes(state) {
3016
+ return {
3017
+ role: 'tabpanel',
3018
+ id: state.ids.panel,
3019
+ 'aria-labelledby': state.ids.trigger,
3020
+ tabindex: '0',
3021
+ [ITEM_KEY_ATTRIBUTE]: state.key,
3022
+ ...(state.on ? { [ON_ATTRIBUTE]: '' } : { hidden: '' }),
3023
+ };
3024
+ }
3025
+ /**
3026
+ * The render scope of one rendered Tabs — ONE decision for both engine paths (and the composer's checks): its scope id
3027
+ * (its node id + the rows around it, vectoriox/iox-ui#20), its behaviour, its list's name and every item key in order —
3028
+ * a manual tab's own `itemKey`, or, From content, each ROW's own id (R3: never its index — a reorder in the CMS keeps
3029
+ * every tab with its own panel). `rows` = the rows the tab list's Repeater resolved (null for a manual Tabs).
3030
+ */
3031
+ function tabsScopeFor(root, rootCssId, rows, rowKeys = []) {
3032
+ const keys = rows ? rows.map((row, i) => rowItemKey(row, i)) : manualTabKeys(root);
3033
+ return tabsScope(compositeScopeId(rootCssId, rowKeys), tabsBehaviour(root), keys, tabsLabel(root));
3034
+ }
3035
+ /** The key a manual tab or panel renders with: its own key, else its index among its kind. */
3036
+ function tabPartKey(node, index) {
3037
+ return node.itemKey || `item-${index}`;
3038
+ }
3039
+ /**
3040
+ * The attributes a part of a Tabs writes on its own element (both engine paths, SSR included): the root's data
3041
+ * attributes (its scope id when it sits in repeated rows), the list's role and name, a tab's and a panel's state by
3042
+ * `key` (its own key, or — From content — its row's). Anything else writes nothing.
3043
+ */
3044
+ function tabsPartAttributes(node, scope, key, rowScoped = false) {
3045
+ switch (node.role) {
3046
+ case TabsRole.Tabs: return tabsRootAttributes(scope.behaviour, rowScoped ? scope.rootId : undefined);
3047
+ case TabsRole.List: return tabListAttributes(scope.behaviour, scope.label);
3048
+ case TabsRole.Trigger: return key ? tabTriggerAttributes(tabsItemState(scope, key)) : {};
3049
+ case TabsRole.Panel: return key ? tabPanelAttributes(tabsItemState(scope, key)) : {};
3050
+ default: return {};
3051
+ }
3052
+ }
3053
+ /**
3054
+ * The APG tabs keys on a tab (automatic activation): → / ← move along a horizontal list — mirrored under RTL — and ↓ /
3055
+ * ↑ along a vertical one; Home / End jump. Every move wraps. Enter / Space are the button's own.
3056
+ */
3057
+ function tabsKeyAction(key, orientation, rtl = false) {
3058
+ if (key === 'Home')
3059
+ return AccordionKeyAction.First;
3060
+ if (key === 'End')
3061
+ return AccordionKeyAction.Last;
3062
+ if (orientation === TabsOrientation.Vertical) {
3063
+ if (key === 'ArrowDown')
3064
+ return AccordionKeyAction.Next;
3065
+ if (key === 'ArrowUp')
3066
+ return AccordionKeyAction.Previous;
3067
+ return null;
3068
+ }
3069
+ if (key === 'ArrowRight')
3070
+ return rtl ? AccordionKeyAction.Previous : AccordionKeyAction.Next;
3071
+ if (key === 'ArrowLeft')
3072
+ return rtl ? AccordionKeyAction.Next : AccordionKeyAction.Previous;
3073
+ return null;
3074
+ }
3075
+ // ── Items from content (R3, composite T39) ──────────────────────────────────────────────────────
3076
+ const SHORT_TEXT = new Set(['text']);
3077
+ const LONG_TEXT = new Set(['textarea', 'richText', 'editor']);
3078
+ const IMAGE_CONTROLLERS = new Set(['image', 'media', 'file']);
3079
+ /** R3 (board 3 "Tabs from content"): switching to From content binds the tab's words to the first short-text field,
3080
+ * the panel's words to the first long-text field and its picture to the first image field. ONE rule for the builder's
3081
+ * switch and the composer. */
3082
+ function pickTabsAutoBinds(fields) {
3083
+ const isString = (f) => !f.type || f.type === 'string';
3084
+ const label = fields.find(f => isString(f) && SHORT_TEXT.has(String(f.controller ?? 'text')))?.name ?? null;
3085
+ const text = fields.find(f => isString(f) && LONG_TEXT.has(String(f.controller ?? '')) && f.name !== label)?.name ?? null;
3086
+ const image = fields.find(f => IMAGE_CONTROLLERS.has(String(f.controller ?? '')) && f.name !== label && f.name !== text)?.name ?? null;
3087
+ return { label, text, image };
3088
+ }
3089
+ /** The bound props of a tab's words, a panel's words and its picture. */
3090
+ const TABS_LABEL_BIND_PROP = 'content';
3091
+ const TABS_TEXT_BIND_PROP = 'content';
3092
+ const TABS_IMAGE_BIND_PROP = 'src';
3093
+ /**
3094
+ * Manual → From content (board 9 "Items: Manual · From content"): the list becomes `listRepeater` and the panels
3095
+ * holder `panelsRepeater` (Repeater nodes the caller created with the same ids, roles, names, element styles and
3096
+ * styles — a type change is the consumer's). The FIRST tab and ITS panel become the templates (their parts, styles
3097
+ * and interactions kept; their keys dropped — a row's key is its id); every other pair is removed. Returns the removed
3098
+ * nodes.
3099
+ */
3100
+ function tabsToContent(root, listRepeater, panelsRepeater) {
3101
+ const [tab, ...restTabs] = tabsTriggers(root);
3102
+ const panel = tab ? tabPanelOf(root, tab) : undefined;
3103
+ const restPanels = tabsPanels(root).filter(p => p !== panel);
3104
+ if (tab)
3105
+ delete tab.itemKey;
3106
+ if (panel)
3107
+ delete panel.itemKey;
3108
+ listRepeater.role = TabsRole.List;
3109
+ panelsRepeater.role = TabsRole.Panels;
3110
+ listRepeater.children = tab ? [tab] : [];
3111
+ panelsRepeater.children = panel ? [panel] : [];
3112
+ root.children = (root.children ?? []).map(c => (c.role === TabsRole.List ? listRepeater : c.role === TabsRole.Panels ? panelsRepeater : c));
3113
+ return [...restTabs, ...restPanels];
3114
+ }
3115
+ function writeRowIn(node, row, resolve) {
3116
+ walkComposite(node, n => {
3117
+ const bindings = n.bindings ?? [];
3118
+ for (const b of bindings) {
3119
+ const value = resolve(row, b);
3120
+ if (value !== undefined && value !== null)
3121
+ writeProp(n, b.prop, value);
3122
+ }
3123
+ if (bindings.length)
3124
+ n.bindings = [];
3125
+ });
3126
+ }
3127
+ /**
3128
+ * From content → Manual: the rows come in as static pairs — each a copy of the template tab and panel (`clone`, which
3129
+ * gives the copy fresh ids) whose bound words are written in as its own and whose bindings are dropped. `list` and
3130
+ * `panels` are the manual holders the caller created. Returns the new pairs.
3131
+ */
3132
+ function tabsToManual(root, list, panels, rows, clone, resolve) {
3133
+ const [tabTemplate] = tabsTriggers(root);
3134
+ const panelTemplate = tabsPanels(root)[0];
3135
+ list.role = TabsRole.List;
3136
+ panels.role = TabsRole.Panels;
3137
+ const pairs = tabTemplate && panelTemplate ? rows.map((row, i) => {
3138
+ const tab = clone(tabTemplate);
3139
+ const panel = clone(panelTemplate);
3140
+ tab.itemKey = panel.itemKey = `k${i + 1}`;
3141
+ writeRowIn(tab, row, resolve);
3142
+ writeRowIn(panel, row, resolve);
3143
+ return { tab, panel };
3144
+ }) : [];
3145
+ list.children = pairs.map(p => p.tab);
3146
+ panels.children = pairs.map(p => p.panel);
3147
+ root.children = (root.children ?? []).map(c => (c.role === TabsRole.List ? list : c.role === TabsRole.Panels ? panels : c));
3148
+ return pairs;
3149
+ }
3150
+
3151
+ const KIND_ROOT = (kind) => `[${COMPOSITE_ATTRIBUTE}="${kind}"]`;
3152
+ const EXCLUSIVE_KINDS = Object.keys(EXCLUSIVE_KIND_ROLES);
3153
+ const ROOT_SELECTOR = EXCLUSIVE_KINDS.map(KIND_ROOT).join(', ');
3154
+ const roleSelector = (pick) => EXCLUSIVE_KINDS.map(k => EXCLUSIVE_KIND_ROLES[k][pick]).filter((r) => !!r).map(r => `.${roleClass(r)}`).join(', ');
3155
+ const LIST_SELECTOR = roleSelector('list');
3156
+ const TRIGGER_SELECTOR = roleSelector('trigger');
3157
+ const PANEL_SELECTOR = roleSelector('panel');
3158
+ const ITEM_PART_SELECTOR = `${TRIGGER_SELECTOR}, ${PANEL_SELECTOR}`;
3159
+ const TABLIST_SELECTOR = '[role="tablist"]';
3160
+ /** The exclusive composite an element belongs to (the element itself when it is a root). */
3161
+ function closestExclusive(el) {
3162
+ return el?.closest?.(ROOT_SELECTOR) ?? null;
3163
+ }
3164
+ /** The trigger or panel an element sits in (itself when it is one) — its ITEM part — or null. */
3165
+ function closestExclusiveItemPart(el) {
3166
+ return el?.closest?.(ITEM_PART_SELECTOR) ?? null;
3167
+ }
3168
+ const kindOf = (root) => {
3169
+ const raw = root.getAttribute(COMPOSITE_ATTRIBUTE);
3170
+ return isExclusiveKind(raw) ? raw : CompositeKind.Tabs;
3171
+ };
3172
+ const nodeIdOf = (el) => {
3173
+ for (const c of Array.from(el.classList))
3174
+ if (c.startsWith('iox-node-'))
3175
+ return c.slice('iox-node-'.length);
3176
+ return '';
3177
+ };
3178
+ function behaviourOf(root) {
3179
+ const on = root.getAttribute(ACTIVATE_ON_ATTRIBUTE);
3180
+ const orientation = root.getAttribute(ORIENTATION_ATTRIBUTE);
3181
+ return {
3182
+ activateOn: TABS_ACTIVATE_ONS.includes(on) ? on : TabsActivateOn.Click,
3183
+ orientation: TABS_ORIENTATIONS.includes(orientation) ? orientation : TabsOrientation.Horizontal,
3184
+ };
3185
+ }
3186
+ function setAttr(el, name, value) {
3187
+ if (!el)
3188
+ return;
3189
+ if (value === null) {
3190
+ if (el.hasAttribute(name))
3191
+ el.removeAttribute(name);
3192
+ }
3193
+ else if (el.getAttribute(name) !== value)
3194
+ el.setAttribute(name, value);
3195
+ }
3196
+ function dispatch(el, type) {
3197
+ const Ctor = globalThis.CustomEvent;
3198
+ if (el && Ctor)
3199
+ el.dispatchEvent(new Ctor(type, { bubbles: false }));
3200
+ }
3201
+ class ExclusiveRuntime {
3202
+ constructor(root, options = {}) {
3203
+ this.root = root;
3204
+ this.options = options;
3205
+ this.shown = null;
3206
+ this.observer = null;
3207
+ this.stampQueued = false;
3208
+ this.awaitingItems = false;
3209
+ this.onClick = (event) => {
3210
+ const trigger = this.ownTrigger(event.target);
3211
+ if (!trigger)
3212
+ return;
3213
+ event.preventDefault();
3214
+ this.activate(this.keyOf(trigger));
3215
+ };
3216
+ this.onHover = (event) => {
3217
+ if (this.behaviourValue.activateOn !== TabsActivateOn.Hover)
3218
+ return;
3219
+ const trigger = this.ownTrigger(event.target);
3220
+ if (trigger)
3221
+ this.activate(this.keyOf(trigger));
3222
+ };
3223
+ this.onKeydown = (event) => {
3224
+ const e = event;
3225
+ const trigger = this.ownTrigger(e.target);
3226
+ if (!trigger)
3227
+ return;
3228
+ const rtl = isRtl(trigger);
3229
+ const action = tabsKeyAction(e.key, this.behaviourValue.orientation, rtl);
3230
+ if (!action)
3231
+ return;
3232
+ const triggers = this.triggers();
3233
+ const target = triggers[accordionFocusIndex(action, triggers.indexOf(trigger), triggers.length)];
3234
+ if (!target)
3235
+ return;
3236
+ e.preventDefault();
3237
+ target.focus();
3238
+ this.activate(this.keyOf(target));
3239
+ };
3240
+ this.kind = kindOf(root);
3241
+ this.behaviourValue = options.behaviour ?? behaviourOf(root);
3242
+ root.setAttribute(COMPOSITE_ATTRIBUTE, this.kind);
3243
+ EXCLUSIVE_RUNTIMES.set(root, this);
3244
+ // The server rendered the first state (every trigger keyed, one on); with no server state the first is on.
3245
+ const triggers = this.triggers();
3246
+ const serverOn = !options.behaviour ? triggers.find(t => t.hasAttribute(ON_ATTRIBUTE) && t.hasAttribute(ITEM_KEY_ATTRIBUTE)) : undefined;
3247
+ this.live = serverOn ? this.keyOf(serverOn) : (triggers[0] ? this.keyOf(triggers[0]) : null);
3248
+ this.awaitingItems = !triggers.length;
3249
+ // Capture: on the builder canvas every part's own listener stops the click (it selects that part), and the tab
3250
+ // must still turn on — the canvas is a live runtime.
3251
+ root.addEventListener('click', this.onClick, true);
3252
+ root.addEventListener('mouseover', this.onHover, true);
3253
+ root.addEventListener('keydown', this.onKeydown);
3254
+ if (options.observe && typeof MutationObserver !== 'undefined') {
3255
+ this.observer = new MutationObserver(() => this.queueStamp());
3256
+ this.observer.observe(root, { childList: true, subtree: true });
3257
+ }
3258
+ this.stamp(true);
3259
+ }
3260
+ get behaviour() {
3261
+ return this.behaviourValue;
3262
+ }
3263
+ /** The behaviour changed in the panel (Activate on, Orientation). */
3264
+ setBehaviour(behaviour) {
3265
+ this.behaviourValue = behaviour;
3266
+ this.root.setAttribute(ACTIVATE_ON_ATTRIBUTE, behaviour.activateOn);
3267
+ this.root.setAttribute(ORIENTATION_ATTRIBUTE, behaviour.orientation);
3268
+ this.stamp();
3269
+ }
3270
+ /** THIS composite's triggers in document order (a nested composite's are its own). */
3271
+ triggers() {
3272
+ return this.own(TRIGGER_SELECTOR);
3273
+ }
3274
+ /** THIS composite's panels in document order. */
3275
+ panels() {
3276
+ return this.own(PANEL_SELECTOR);
3277
+ }
3278
+ /** The key on right now (forced ?? live). */
3279
+ get activeKey() {
3280
+ return this.shown;
3281
+ }
3282
+ /** The key the live state holds (before anything forced). */
3283
+ get liveKey() {
3284
+ return this.live;
3285
+ }
3286
+ /** The key of a trigger or a panel: its `data-iox-item-key`, else its index among its kind. */
3287
+ keyOf(part) {
3288
+ const own = part.getAttribute(ITEM_KEY_ATTRIBUTE);
3289
+ if (own)
3290
+ return own;
3291
+ const peers = part.matches(TRIGGER_SELECTOR) ? this.triggers() : this.panels();
3292
+ return `item-${peers.indexOf(part)}`;
3293
+ }
3294
+ /** The key of the item an element sits in (a part inside a tab or a panel), or null. */
3295
+ keyAt(el) {
3296
+ const part = closestExclusiveItemPart(el);
3297
+ return part && closestExclusive(part) === this.root ? this.keyOf(part) : null;
3298
+ }
3299
+ /** Turn `key` on (a click, a key, a hover, the `openItem` action, "Showing on canvas"). */
3300
+ activate(key) {
3301
+ if (!this.keys().includes(key))
3302
+ return;
3303
+ const changed = key !== this.live;
3304
+ this.live = key;
3305
+ this.stamp();
3306
+ if (changed)
3307
+ this.options.onChange?.(key);
3308
+ }
3309
+ /** Run an item action on `key`: Open turns it on; Close and Toggle do nothing (one item is always on). True when it
3310
+ * is on afterwards. */
3311
+ run(open, key) {
3312
+ if (open)
3313
+ this.activate(key);
3314
+ return this.shown === key;
3315
+ }
3316
+ /** The forced key changed (builder design state): show it. */
3317
+ refresh() {
3318
+ this.stamp();
3319
+ }
3320
+ destroy() {
3321
+ if (EXCLUSIVE_RUNTIMES.get(this.root) === this)
3322
+ EXCLUSIVE_RUNTIMES.delete(this.root);
3323
+ this.root.removeEventListener('click', this.onClick, true);
3324
+ this.root.removeEventListener('mouseover', this.onHover, true);
3325
+ this.root.removeEventListener('keydown', this.onKeydown);
3326
+ this.observer?.disconnect();
3327
+ this.observer = null;
3328
+ }
3329
+ keys() {
3330
+ return this.triggers().map(t => this.keyOf(t));
3331
+ }
3332
+ /** The element(s) that ARE the tablist: the ones a renderer already gave the role (the engine's component-path
3333
+ * Repeater puts it on its inner root, which holds the tabs), else the elements carrying the list's role class. */
3334
+ lists() {
3335
+ const stamped = this.own(TABLIST_SELECTOR);
3336
+ return stamped.length ? stamped : this.own(LIST_SELECTOR);
3337
+ }
3338
+ own(selector) {
3339
+ return Array.from(this.root.querySelectorAll(selector)).filter(el => closestExclusive(el.parentElement) === this.root);
3340
+ }
3341
+ queueStamp() {
3342
+ if (this.stampQueued)
3343
+ return;
3344
+ this.stampQueued = true;
3345
+ const run = () => { this.stampQueued = false; this.stamp(); };
3346
+ if (typeof queueMicrotask === 'function')
3347
+ queueMicrotask(run);
3348
+ else
3349
+ Promise.resolve().then(run);
3350
+ }
3351
+ rootId() {
3352
+ const own = this.options.rootId;
3353
+ return (typeof own === 'function' ? own() : own) ?? this.root.getAttribute(SCOPE_ATTRIBUTE) ?? nodeIdOf(this.root);
3354
+ }
3355
+ /** Write every part's state into its markup; announce what changed. */
3356
+ stamp(first = false) {
3357
+ const triggers = this.triggers();
3358
+ const keys = triggers.map(t => this.keyOf(t));
3359
+ if (this.awaitingItems && keys.length) {
3360
+ this.awaitingItems = false;
3361
+ this.live = keys[0];
3362
+ first = true;
3363
+ }
3364
+ if (this.live === null || !keys.includes(this.live))
3365
+ this.live = keys[0] ?? null;
3366
+ const forced = this.options.forced?.() ?? null;
3367
+ const next = forced !== null && keys.includes(forced) ? forced : this.live;
3368
+ const rootId = this.rootId();
3369
+ const vertical = this.behaviourValue.orientation === TabsOrientation.Vertical;
3370
+ for (const list of this.lists()) {
3371
+ setAttr(list, 'role', 'tablist');
3372
+ setAttr(list, 'aria-orientation', vertical ? 'vertical' : null);
3373
+ if (this.options.label)
3374
+ setAttr(list, 'aria-label', this.options.label() || null);
3375
+ }
3376
+ const panels = this.panels();
3377
+ const panelOf = (key) => panels.find(p => this.keyOf(p) === key) ?? null;
3378
+ for (const trigger of triggers) {
3379
+ const key = this.keyOf(trigger);
3380
+ const on = key === next;
3381
+ const ids = accordionDomIds(rootId, key, CompositeKind.Tabs);
3382
+ setAttr(trigger, ITEM_KEY_ATTRIBUTE, key);
3383
+ setAttr(trigger, 'type', trigger.tagName === 'BUTTON' ? 'button' : null);
3384
+ setAttr(trigger, 'role', 'tab');
3385
+ setAttr(trigger, 'id', ids.trigger);
3386
+ setAttr(trigger, 'aria-selected', on ? 'true' : 'false');
3387
+ setAttr(trigger, 'aria-controls', panelOf(key) ? ids.panel : null);
3388
+ setAttr(trigger, 'tabindex', on ? '0' : '-1');
3389
+ setAttr(trigger, ON_ATTRIBUTE, on ? '' : null);
3390
+ }
3391
+ for (const panel of panels) {
3392
+ const key = this.keyOf(panel);
3393
+ const on = key === next;
3394
+ const ids = accordionDomIds(rootId, key, CompositeKind.Tabs);
3395
+ setAttr(panel, ITEM_KEY_ATTRIBUTE, key);
3396
+ setAttr(panel, 'role', 'tabpanel');
3397
+ setAttr(panel, 'id', ids.panel);
3398
+ setAttr(panel, 'aria-labelledby', ids.trigger);
3399
+ setAttr(panel, 'tabindex', '0');
3400
+ setAttr(panel, ON_ATTRIBUTE, on ? '' : null);
3401
+ setAttr(panel, 'hidden', on ? null : '');
3402
+ }
3403
+ const was = first ? null : this.shown;
3404
+ this.shown = next;
3405
+ if (was === next && !first)
3406
+ return;
3407
+ if (was !== null) {
3408
+ dispatch(triggers.find(t => this.keyOf(t) === was), ITEM_OFF_EVENT);
3409
+ dispatch(panelOf(was), ITEM_OFF_EVENT);
3410
+ }
3411
+ if (next !== null) {
3412
+ dispatch(triggers.find(t => this.keyOf(t) === next), ITEM_ON_EVENT);
3413
+ dispatch(panelOf(next), ITEM_ON_EVENT);
3414
+ }
3415
+ }
3416
+ ownTrigger(target) {
3417
+ const trigger = target?.closest?.(TRIGGER_SELECTOR);
3418
+ return trigger && closestExclusive(trigger.parentElement) === this.root ? trigger : null;
3419
+ }
3420
+ }
3421
+ /** The direction the tab is laid out in — the computed `direction` (a page's own CSS included), else its nearest `dir`. */
3422
+ function isRtl(el) {
3423
+ let computed = '';
3424
+ try {
3425
+ computed = el.ownerDocument?.defaultView?.getComputedStyle(el).direction ?? '';
3426
+ }
3427
+ catch {
3428
+ computed = '';
3429
+ }
3430
+ if (computed)
3431
+ return computed === 'rtl';
3432
+ return el.closest('[dir]')?.getAttribute('dir') === 'rtl';
3433
+ }
3434
+ /** Every live exclusive runtime by its root — so an item action finds the runtime of whatever item it targets. */
3435
+ const EXCLUSIVE_RUNTIMES = new WeakMap();
3436
+ /** The runtime running an exclusive root, or null. */
3437
+ function exclusiveRuntimeOf(root) {
3438
+ return (root && EXCLUSIVE_RUNTIMES.get(root)) || null;
3439
+ }
3440
+ /** Attach the runtime to one rendered exclusive root, unless it already runs one. Returns the NEW runtime, or null. */
3441
+ function attachExclusiveAt(root, options = {}) {
3442
+ if (EXCLUSIVE_RUNTIMES.has(root))
3443
+ return null;
3444
+ return new ExclusiveRuntime(root, options);
3445
+ }
3446
+ /** True when `el` is (or sits in) an exclusive item part — a tab or a panel. */
3447
+ function isExclusiveItemOn(el) {
3448
+ return !!closestExclusiveItemPart(el)?.hasAttribute(ON_ATTRIBUTE);
3449
+ }
3450
+ /** Subscribe to the item part `el` sits in turning on / off. Returns the unsubscribe. */
3451
+ function onExclusiveItemState(el, on, handler) {
3452
+ const part = closestExclusiveItemPart(el);
3453
+ if (!part)
3454
+ return () => undefined;
3455
+ const type = on ? ITEM_ON_EVENT : ITEM_OFF_EVENT;
3456
+ part.addEventListener(type, handler);
3457
+ return () => part.removeEventListener(type, handler);
3458
+ }
3459
+
3460
+ /** Attach the runtime a rendered composite root needs (its `data-iox-composite`), unless it runs one already. Returns
3461
+ * the NEW runtime, or null. */
3462
+ function attachCompositeAt(root, options = {}) {
3463
+ const kind = root.getAttribute(COMPOSITE_ATTRIBUTE);
3464
+ if (isExclusiveKind(kind))
3465
+ return attachExclusiveAt(root, { observe: options.observe });
3466
+ return attachAccordionAt(root, { observe: options.observe, reducedMotion: options.reducedMotion });
3467
+ }
3468
+ /** The nearer of the item elements `el` sits in — an accordion item / a disclosure, or a tab / a tab panel. */
3469
+ function nearestItem(el) {
3470
+ const toggle = closestAccordionItem(el);
3471
+ const exclusive = closestExclusiveItemPart(el);
3472
+ if (!toggle && !exclusive)
3473
+ return null;
3474
+ if (toggle && (!exclusive || exclusive.contains(toggle)))
3475
+ return { el: toggle, exclusive: false };
3476
+ return { el: exclusive, exclusive: true };
3477
+ }
3478
+ /** Subscribe to the item `el` sits in turning on / off — what the `itemOn` / `itemOff` interaction triggers listen to,
3479
+ * in both families. Returns the unsubscribe. */
3480
+ function onCompositeItemState(el, on, handler) {
3481
+ const item = nearestItem(el);
3482
+ if (!item)
3483
+ return () => undefined;
3484
+ return item.exclusive ? onExclusiveItemState(item.el, on, handler) : onAccordionItemState(item.el, on, handler);
3485
+ }
3486
+ /** True when the item `el` sits in is on now (an open accordion item, the active tab or its panel). */
3487
+ function isCompositeItemOn(el) {
3488
+ const item = nearestItem(el);
3489
+ if (!item)
3490
+ return false;
3491
+ return item.exclusive ? isExclusiveItemOn(item.el) : isAccordionItemOn(item.el);
3492
+ }
3493
+ /**
3494
+ * Run an item action on the element of an ITEM — an accordion item, a disclosure, or a tab (CC4a: `openItem` turns it
3495
+ * on; a Tabs always shows one, so Close and Toggle leave it) — the `openItem` / `closeItem` / `toggleItem` interaction
3496
+ * actions, on the canvas and on the live site through this one function. Returns whether the item shows on
3497
+ * afterwards, or null when `el` is in no running composite.
3498
+ */
3499
+ function runCompositeItemAction(el, action) {
3500
+ const item = nearestItem(el);
3501
+ if (!item)
3502
+ return null;
3503
+ if (item.exclusive) {
3504
+ const runtime = exclusiveRuntimeOf(closestExclusive(item.el));
3505
+ return runtime ? runtime.run(action === CompositeItemAction.Open, runtime.keyOf(item.el)) : null;
3506
+ }
3507
+ const runtime = accordionRuntimeOf(accordionRootOfItem(item.el));
3508
+ return runtime ? runtime.run(action, runtime.keyOf(item.el)) : null;
3509
+ }
1892
3510
 
1893
3511
  /**
1894
3512
  * @vectoriox/iox-ui/composite-core — composites built from the builder's own parts (composite-components
1895
3513
  * plan §3a). FRAMEWORK-FREE: the builder, the SSR engine and iox-cms (generation-core's composer, the site
1896
3514
  * build) read the same recipes, roles and structure rules. Card v2 + Badge / Chip (T23 / T25) are the
1897
3515
  * first; the Accordion (CC2) adds the first composite RUNTIME (accordion-runtime.ts — DOM only, no
1898
- * framework), which the Disclosure (CC2.1) runs too — a one-item accordion whose root is its item; Tabs join with
1899
- * their own phase.
3516
+ * framework), which the Disclosure (CC2.1) runs too — a one-item accordion whose root is its item; Tabs (CC4a) run the
3517
+ * EXCLUSIVE runtime (exclusive-runtime.ts — one item always on); composite-runtime.ts is the one door to both.
1900
3518
  * ⚠️ Consumers wire the usual trio — ng-package raw-ts assets, jest moduleNameMapper, tsconfig paths.
1901
3519
  */
1902
3520
 
@@ -1904,5 +3522,5 @@ function isLockedPart(node) {
1904
3522
  * Generated bundle index. Do not edit.
1905
3523
  */
1906
3524
 
1907
- 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_KIND_ROLES, COMPOSITE_ROLE_SPECS, CardLayout, CardPart, CardRole, CompositeBehaviourTrait, CompositeItemAction, CompositeKind, DEFAULT_ON_ATTRIBUTE, DISCLOSURE_ITEM_KEY, DISCLOSURE_WORDS, DisclosureRole, ITEM_KEY_ATTRIBUTE, ITEM_OFF_EVENT, ITEM_ON_EVENT, ITEM_OPEN_STATE, MULTIPLE_ATTRIBUTE, ON_ATTRIBUTE, ON_STATE, PILL_LABEL_STYLE, SCOPE_ATTRIBUTE, 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, badgeStyleId, badgeToneOf, buildAccordionItem, buildAccordionRecipe, buildBadge, buildCardRecipe, buildChip, buildCompositeIcon, buildDisclosureRecipe, cardPartsOf, cardRootOf, cardTitleOf, clearLayerStyles, closeAccordionItem, closestAccordion, closestAccordionItem, compositeItemOf, compositeItems, compositeKindOfRole, compositeRootOf, compositeRowKeys, compositeScopeId, detachAccordion, detectAccordionIcon, detectCardLayout, disclosureOpenByDefault, domSafeKey, findByRole, hasLabelledRegion, initialAccordionOpen, isAccordionFromContent, isAccordionItemOn, isAccordionPart, isAccordionRoot, isCardLinked, isCardRoot, isCompositeItem, isCompositePanel, isCompositeRoot, isCompositeTrigger, isDisclosurePart, isDisclosureRoot, isLockedPart, isPillNode, itemOpenTriggerIds, makeCompositePart, moveAccordionItem, nextAccordionState, nextItemKey, onAccordionItemState, openAccordionItem, parentInComposite, pickAccordionAutoBinds, readLayerStyle, readProp, readStyle, removeFromComposite, renameAccordionItem, rowItemKey, runCompositeItemAction, serializeDomAttributes, setAccordionBehaviour, setAccordionIcon, setBadgeTone, setCardLinked, setCardPart, shouldScrollToItem, structureRefusal, walkComposite, writeLayerStyles, writeProp, writeStyles };
3525
+ export { ACCORDION_BIND_PROP, ACCORDION_CONTENT_LIMIT, ACCORDION_DEFAULT_ONS, ACCORDION_ICONS, ACCORDION_ICON_GLYPH, ACCORDION_NEW_ITEM_WORDS, ACCORDION_WORDS, ACTIVATE_ON_ATTRIBUTE, 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, EXCLUSIVE_KIND_ROLES, ExclusiveRuntime, 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, ORIENTATION_ATTRIBUTE, 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, TABS_ACTIVATE_ONS, TABS_CONTENT_LIMIT, TABS_IMAGE_BIND_PROP, TABS_LABEL_BIND_PROP, TABS_NEW_TAB_WORDS, TABS_ORIENTATIONS, TABS_STACK_BREAKPOINT, TABS_TEXT_BIND_PROP, TABS_WORDS, TabsActivateOn, TabsBehaviourTrait, TabsEditRefusal, TabsOrientation, TabsRole, UNTIL_FOUND, acceptsRefusal, accordionAnswerOf, accordionBehaviour, accordionDomIds, accordionFocusIndex, accordionHeadingLevel, accordionItemAttributes, accordionItemLabel, accordionItemOf, accordionItemState, accordionItems, accordionKeyAction, accordionPanelAttributes, accordionPanelOf, accordionPartStyle, accordionQuestionOf, accordionRegionAttributes, accordionRemovalRefusal, accordionRootAttributes, accordionRootOf, accordionRootOfItem, accordionRuntimeOf, accordionScope, accordionToContent, accordionToManual, accordionTrackAttributes, accordionTriggerAttributes, accordionTriggerOf, addAccordionItem, addTabPair, applyCardLayout, attachAccordionAt, attachAccordions, attachCarouselA11y, attachCompositeAt, attachExclusiveAt, badgeStyleId, badgeToneOf, buildAccordionItem, buildAccordionRecipe, buildBadge, buildCardRecipe, buildCarouselRecipe, buildChip, buildCompositeIcon, buildDisclosureRecipe, buildLightboxRecipe, buildSliderPart, buildTabPair, buildTabsRecipe, canOpenAtClickedItem, cardPartsOf, cardRootOf, cardTitleOf, carouselOf, clearLayerStyles, closeAccordionItem, closestAccordion, closestAccordionItem, closestExclusive, closestExclusiveItemPart, compositeItemOf, compositeItems, compositeKindOfRole, compositeRootOf, compositeRowKeys, compositeScopeId, detachAccordion, detectAccordionIcon, detectCardLayout, disclosureOpenByDefault, domSafeKey, exclusiveItemPartOf, exclusiveRootOf, exclusiveRuntimeOf, findByRole, hasLabelledRegion, hasSliderParts, initialAccordionOpen, isAccordionFromContent, isAccordionItemOn, isAccordionPart, isAccordionRoot, isAnyCompositeRoot, isCardLinked, isCardRoot, isCompositeItem, isCompositeItemOn, isCompositePanel, isCompositeRoot, isCompositeTrigger, isDisclosurePart, isDisclosureRoot, isExclusiveItemOn, isExclusiveItemPart, isExclusiveKind, isExclusiveList, isExclusivePanel, isExclusivePanels, isExclusiveRoot, isExclusiveTrigger, isLockedPart, isPillNode, isSlider, isSliderPart, isTabsFromContent, isTabsPart, isTabsRoot, isToggleKind, itemOpenTriggerIds, lightboxSliderOf, makeCompositePart, manualTabKeys, migrateSliderControls, moveAccordionItem, moveTabPair, needsSliderMigration, nextAccordionState, nextItemKey, nextTabKey, onAccordionItemState, onCompositeItemState, onExclusiveItemState, openAccordionItem, parentInComposite, pickAccordionAutoBinds, pickTabsAutoBinds, readLayerStyle, readProp, readStyle, removeFromComposite, removeTabPair, renameAccordionItem, renameTab, repeatSourceOf, rowIndexOf, rowItemKey, rowSourceOf, runCompositeItemAction, sameRepeatSource, serializeDomAttributes, setAccordionBehaviour, setAccordionIcon, setBadgeTone, setCardLinked, setCardPart, setSliderFlag, setTabsBehaviour, shouldScrollToItem, slideIndexForRow, sliderLiveRegion, sliderPart, sliderPartElement, sliderPauseAttributes, sliderPauseGlyph, sliderSlides, sliderWords, splitSliderChildren, structureRefusal, syncSliderParts, tabLabel, tabLabelNodeOf, tabListAttributes, tabPanelAttributes, tabPanelImageOf, tabPanelOf, tabPanelTextOf, tabPartKey, tabTriggerAttributes, tabTriggerOf, tabsBehaviour, tabsItemState, tabsItemTriggerIds, tabsKeyAction, tabsLabel, tabsListOf, tabsPanels, tabsPanelsOf, tabsPartAttributes, tabsPartnerOf, tabsRemovalRefusal, tabsRootAttributes, tabsScope, tabsScopeFor, tabsToContent, tabsToManual, tabsTriggers, walkComposite, wantedSliderParts, writeLayerStyles, writeProp, writeStyles };
1908
3526
  //# sourceMappingURL=vectoriox-iox-ui-composite-core.mjs.map