@vectoriox/iox-ui 5.38.0 → 5.40.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 (33) hide show
  1. package/composite-core/index.d.ts +392 -16
  2. package/design-core/index.d.ts +322 -3
  3. package/fesm2022/vectoriox-iox-ui-actions-core.mjs +4 -2
  4. package/fesm2022/vectoriox-iox-ui-actions-core.mjs.map +1 -1
  5. package/fesm2022/vectoriox-iox-ui-composite-core.mjs +1129 -146
  6. package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -1
  7. package/fesm2022/vectoriox-iox-ui-design-core.mjs +383 -3
  8. package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
  9. package/fesm2022/vectoriox-iox-ui-generation-core.mjs +219 -8
  10. package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
  11. package/fesm2022/vectoriox-iox-ui.mjs +16 -9
  12. package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
  13. package/generation-core/index.d.ts +107 -3
  14. package/index.d.ts +11 -6
  15. package/package.json +1 -1
  16. package/src/actions-core/element-actions.ts +4 -2
  17. package/src/composite-core/accordion-runtime.ts +3 -15
  18. package/src/composite-core/accordion.ts +4 -2
  19. package/src/composite-core/composite-kinds.ts +121 -7
  20. package/src/composite-core/composite-runtime.ts +68 -0
  21. package/src/composite-core/exclusive-runtime.ts +358 -0
  22. package/src/composite-core/public-api.ts +5 -2
  23. package/src/composite-core/structure.ts +22 -2
  24. package/src/composite-core/tabs.ts +628 -0
  25. package/src/design-core/contrast-audit.ts +5 -0
  26. package/src/design-core/element-styles.ts +7 -3
  27. package/src/design-core/flavor-spec.generated.ts +371 -0
  28. package/src/generation-core/compose-site.ts +147 -1
  29. package/src/generation-core/nav-shape.ts +53 -3
  30. package/src/generation-core/node-baselines.generated.ts +8 -2
  31. package/src/generation-core/patterns.ts +3 -0
  32. package/src/generation-core/site-spec.generated.ts +46 -0
  33. package/styles/iox-design.css +16 -0
@@ -1,4 +1,4 @@
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
3
  /** What a recipe asks its consumer to create: a NEW node of `type`, the way that consumer creates one
4
4
  * (the builder's `createConfig`, generation-core's `createSerializedNode`), with these props and this
@@ -125,15 +125,18 @@ function removeFromComposite(root, node) {
125
125
  }
126
126
 
127
127
  /**
128
- * The open/close composites' kinds and roles (CC2 Accordion, CC2.1 Disclosure — composite plan T9 / T21). ONE
129
- * family: a Disclosure is a one-item accordion whose ROOT is its item, so both run the same state machine, the
130
- * same attribute planners and the same runtime (`accordion-runtime.ts`). Every consumer that treats "an item",
131
- * "a header" or "a panel" alike asks these predicates, never a role literal.
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.
132
134
  */
133
135
  var CompositeKind;
134
136
  (function (CompositeKind) {
135
137
  CompositeKind["Accordion"] = "accordion";
136
138
  CompositeKind["Disclosure"] = "disclosure";
139
+ CompositeKind["Tabs"] = "tabs";
137
140
  })(CompositeKind || (CompositeKind = {}));
138
141
  var AccordionRole;
139
142
  (function (AccordionRole) {
@@ -157,21 +160,53 @@ var DisclosureRole;
157
160
  DisclosureRole["Icon"] = "disclosure.icon";
158
161
  DisclosureRole["Panel"] = "disclosure.panel";
159
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 = {}));
160
176
  const COMPOSITE_KIND_ROLES = {
161
177
  [CompositeKind.Accordion]: { root: AccordionRole.Accordion, item: AccordionRole.Item, trigger: AccordionRole.Trigger, panel: AccordionRole.Panel, icon: AccordionRole.Icon },
162
178
  [CompositeKind.Disclosure]: { root: DisclosureRole.Disclosure, item: DisclosureRole.Disclosure, trigger: DisclosureRole.Trigger, panel: DisclosureRole.Panel, icon: DisclosureRole.Icon },
163
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
+ };
164
183
  const KINDS = Object.values(CompositeKind);
165
- 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));
166
188
  const ROOT_ROLES = rolesOf('root');
167
189
  const ITEM_ROLES = rolesOf('item');
168
190
  const TRIGGER_ROLES = rolesOf('trigger');
169
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');
170
197
  /** The kind a role belongs to (`disclosure.panel` → Disclosure), or null for any other role. */
171
198
  function compositeKindOfRole(role) {
172
199
  const head = role?.split('.')[0];
173
200
  return KINDS.includes(head ?? '') ? head : null;
174
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
+ }
175
210
  /** A root that runs the open/close runtime (an Accordion or a Disclosure). */
176
211
  function isCompositeRoot(node) {
177
212
  return !!node?.role && ROOT_ROLES.has(node.role);
@@ -218,6 +253,61 @@ function compositeItemOf(node, path) {
218
253
  }
219
254
  return null;
220
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
+ }
221
311
 
222
312
  /**
223
313
  * ACCORDION (CC2 — composite K1; complex-components boards 3, 4 and 9, APPROVED 2026-10-20). Not a
@@ -278,7 +368,7 @@ const ACCORDION_WORDS = [
278
368
  { question: 'Write the third question', answer: 'Answer it plainly, in two or three short sentences.' },
279
369
  ];
280
370
  const ACCORDION_NEW_ITEM_WORDS = { question: 'Write a new question', answer: 'Answer it plainly, in two or three short sentences.' };
281
- var Type$3;
371
+ var Type$4;
282
372
  (function (Type) {
283
373
  Type["Container"] = "Container";
284
374
  Type["LinkedContainer"] = "LinkedContainer";
@@ -286,8 +376,8 @@ var Type$3;
286
376
  Type["Text"] = "Text";
287
377
  Type["Icon"] = "Icon";
288
378
  Type["Repeater"] = "Repeater";
289
- })(Type$3 || (Type$3 = {}));
290
- var Style$3;
379
+ })(Type$4 || (Type$4 = {}));
380
+ var Style$4;
291
381
  (function (Style) {
292
382
  Style["Root"] = "box";
293
383
  Style["Item"] = "accordion-item";
@@ -296,14 +386,14 @@ var Style$3;
296
386
  Style["Icon"] = "icon";
297
387
  Style["Panel"] = "accordion-panel";
298
388
  Style["Answer"] = "body";
299
- })(Style$3 || (Style$3 = {}));
300
- const ZERO_BOX$2 = '0px 0px 0px 0px';
389
+ })(Style$4 || (Style$4 = {}));
390
+ const ZERO_BOX$3 = '0px 0px 0px 0px';
301
391
  const HEADING_LEVELS = ['h2', 'h3', 'h4', 'h5', 'h6'];
302
392
  const DEFAULT_LEVEL = 'h3';
303
393
  /** A linked trait the header must never carry: it is a button, not a link. */
304
394
  const LINK_TYPE$1 = 'linkType';
305
395
  /** What each part holds in the recipe — layout only; its LOOK is its element style. */
306
- const PART_BASE$1 = {
396
+ const PART_BASE$2 = {
307
397
  // The items are a column a token apart (`accordion-gap`: 0 for a divider list, 12px for cards).
308
398
  [AccordionRole.Accordion]: { display: 'flex', flexDirection: 'column', gap: 'var(--iox-accordion-gap)', width: '100%', minHeight: 'auto', height: 'auto' },
309
399
  [AccordionRole.Item]: { display: 'block', width: '100%', minHeight: 'auto', height: 'auto' },
@@ -314,16 +404,16 @@ const PART_BASE$1 = {
314
404
  };
315
405
  /** The question INHERITS the header's ink (board 4's wears table: the Question reads no colour token), so the
316
406
  * header's hover ink reaches the words; and it sits in the row with no margin of its own. */
317
- const QUESTION_BASE = { color: 'inherit', margin: ZERO_BOX$2, flexGrow: '1', minWidth: '0px' };
318
- const ANSWER_BASE = { margin: ZERO_BOX$2, padding: ZERO_BOX$2, minHeight: 'auto' };
319
- function make$2(create, key, type, role, style, props = {}, label) {
320
- return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE$1[role] : undefined);
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);
321
411
  }
322
412
  /** The icon of an item (an accordion's, or a disclosure's — `role`); its Item-open state (the turn) is driven by
323
413
  * ITS item (`stateTriggers`). ONE builder for both kinds. */
324
414
  function buildCompositeIcon(create, key, icon, itemId, role = AccordionRole.Icon) {
325
415
  const glyph = ACCORDION_ICON_GLYPH[icon];
326
- const node = makeCompositePart(create, { key, type: Type$3.Icon, props: { iconClass: glyph.iconClass }, elementStyle: Style$3.Icon }, role, 'Icon', PART_BASE$1[AccordionRole.Icon]);
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]);
327
417
  node.stateStyles = { ...(node.stateStyles ?? {}), [ITEM_OPEN_STATE]: { rotate: glyph.turn } };
328
418
  if (itemId)
329
419
  node.stateTriggers = { ...(node.stateTriggers ?? {}), [ITEM_OPEN_STATE]: itemId };
@@ -331,14 +421,14 @@ function buildCompositeIcon(create, key, icon, itemId, role = AccordionRole.Icon
331
421
  }
332
422
  /** One item: header (question + icon) and panel (answer). `key` makes every part's recipe key unique. */
333
423
  function buildAccordionItem(create, words, itemKey, icon = AccordionIcon.Chevron) {
334
- const item = make$2(create, `${itemKey}`, Type$3.Container, AccordionRole.Item, Style$3.Item, {}, 'Item');
424
+ const item = make$3(create, `${itemKey}`, Type$4.Container, AccordionRole.Item, Style$4.Item, {}, 'Item');
335
425
  item.itemKey = itemKey;
336
- const trigger = make$2(create, `${itemKey}-trigger`, Type$3.LinkedContainer, AccordionRole.Trigger, Style$3.Trigger, {}, 'Header');
337
- const question = make$2(create, `${itemKey}-question`, Type$3.Heading, null, Style$3.Question, { content: words.question, level: DEFAULT_LEVEL }, 'Heading');
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');
338
428
  writeStyles(question, QUESTION_BASE);
339
429
  trigger.children = [question, ...(icon === AccordionIcon.None ? [] : [buildCompositeIcon(create, `${itemKey}-icon`, icon, item.id)])];
340
- const panel = make$2(create, `${itemKey}-panel`, Type$3.Container, AccordionRole.Panel, Style$3.Panel, {}, 'Panel');
341
- const answer = make$2(create, `${itemKey}-answer`, Type$3.Text, null, Style$3.Answer, { content: words.answer, tag: 'p' }, 'Text');
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');
342
432
  writeStyles(answer, ANSWER_BASE);
343
433
  panel.children = [answer];
344
434
  item.children = [trigger, panel];
@@ -346,7 +436,7 @@ function buildAccordionItem(create, words, itemKey, icon = AccordionIcon.Chevron
346
436
  }
347
437
  /** A fresh Accordion: three items, one open at a time, the first open by default (board 4). */
348
438
  function buildAccordionRecipe(create, options = {}) {
349
- const root = make$2(create, 'accordion', Type$3.Container, AccordionRole.Accordion, Style$3.Root, {}, 'Accordion');
439
+ const root = make$3(create, 'accordion', Type$4.Container, AccordionRole.Accordion, Style$4.Root, {}, 'Accordion');
350
440
  writeProp(root, CompositeBehaviourTrait.Multiple, options.multiple ?? false);
351
441
  writeProp(root, CompositeBehaviourTrait.DefaultOn, options.defaultOn ?? AccordionDefaultOn.First);
352
442
  const words = options.items ?? ACCORDION_WORDS;
@@ -385,7 +475,7 @@ function accordionItemOf(node, path) {
385
475
  }
386
476
  /** True when the root takes its items from content (a Repeater bound to a list, R3). */
387
477
  function isAccordionFromContent(root) {
388
- return !!root && root.type === Type$3.Repeater;
478
+ return !!root && root.type === Type$4.Repeater;
389
479
  }
390
480
  /** The root's items in order — every item (Manual), or the one template item (From content). */
391
481
  function accordionItems(root) {
@@ -409,7 +499,8 @@ function setAccordionBehaviour(root, patch) {
409
499
  }
410
500
  /** The roles of the kind an item belongs to (an accordion item, or a disclosure — its own item). */
411
501
  function kindRolesOf(node) {
412
- 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];
413
504
  }
414
505
  /** The header of an item (either kind). */
415
506
  function accordionTriggerOf(item) {
@@ -431,9 +522,9 @@ function accordionQuestionOf(item) {
431
522
  let text;
432
523
  if (trigger)
433
524
  walkComposite(trigger, n => {
434
- if (!hit && n.type === Type$3.Heading)
525
+ if (!hit && n.type === Type$4.Heading)
435
526
  hit = n;
436
- if (!text && n.type === Type$3.Text)
527
+ if (!text && n.type === Type$4.Text)
437
528
  text = n;
438
529
  });
439
530
  return hit ?? text;
@@ -443,15 +534,15 @@ function accordionAnswerOf(item) {
443
534
  const panel = accordionPanelOf(item);
444
535
  let hit;
445
536
  if (panel)
446
- walkComposite(panel, n => { if (!hit && n.type === Type$3.Text)
537
+ walkComposite(panel, n => { if (!hit && n.type === Type$4.Text)
447
538
  hit = n; });
448
539
  return hit;
449
540
  }
450
- const stripTags = (v) => String(v ?? '').replace(/<[^>]*>/g, '').trim();
541
+ const stripTags$1 = (v) => String(v ?? '').replace(/<[^>]*>/g, '').trim();
451
542
  /** An item's name in the Items list and the layer tree: its question's words. */
452
543
  function accordionItemLabel(item) {
453
544
  const q = accordionQuestionOf(item);
454
- return q ? stripTags(readProp(q, 'content')) : '';
545
+ return q ? stripTags$1(readProp(q, 'content')) : '';
455
546
  }
456
547
  /** Items ▸ rename: the question's words. */
457
548
  function renameAccordionItem(item, question) {
@@ -464,7 +555,7 @@ function accordionHeadingLevel(trigger) {
464
555
  const visit = (n) => {
465
556
  if (level !== undefined)
466
557
  return;
467
- if (n.type === Type$3.Heading) {
558
+ if (n.type === Type$4.Heading) {
468
559
  level = readProp(n, 'level');
469
560
  return;
470
561
  }
@@ -644,6 +735,7 @@ function rowItemKey(row, index) {
644
735
  const DOM_ID_PREFIX = {
645
736
  [CompositeKind.Accordion]: 'iox-acc',
646
737
  [CompositeKind.Disclosure]: 'iox-dis',
738
+ [CompositeKind.Tabs]: 'iox-tab',
647
739
  };
648
740
  /** The ARIA pairing ids of an item: unique per composite (its SCOPE id — `compositeScopeId`) and per item (its key). */
649
741
  function accordionDomIds(rootId, itemKey, kind = CompositeKind.Accordion) {
@@ -731,14 +823,14 @@ function accordionPanelAttributes(state) {
731
823
  function serializeDomAttributes(attrs, esc) {
732
824
  return Object.entries(attrs).map(([k, v]) => (v === '' ? ` ${k}` : ` ${k}="${esc(v)}"`)).join('');
733
825
  }
734
- const SHORT_TEXT = new Set(['text']);
735
- 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']);
736
828
  /** R3 (board 9 Q19): switching to From content binds the question to the first short-text field and the
737
829
  * answer to the first long-text field. ONE rule for the builder's switch and the composer. */
738
830
  function pickAccordionAutoBinds(fields) {
739
831
  const isString = (f) => !f.type || f.type === 'string';
740
- const question = fields.find(f => isString(f) && SHORT_TEXT.has(String(f.controller ?? 'text')))?.name ?? null;
741
- 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;
742
834
  return { question, answer };
743
835
  }
744
836
  /** The bound props of the question and the answer (each a content trait). */
@@ -818,12 +910,12 @@ function accordionPartStyle(node, name) {
818
910
  */
819
911
  const ITEM_ON_EVENT = 'iox-item-on';
820
912
  const ITEM_OFF_EVENT = 'iox-item-off';
821
- const KIND_ROOT = (kind) => `[${COMPOSITE_ATTRIBUTE}="${kind}"]`;
822
- 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(', ');
823
915
  /** An accordion's items are its `.iox-role-accordion-item` children; a disclosure is its own item. */
824
916
  const ACCORDION_ITEM_SELECTOR = '.iox-role-accordion-item';
825
- const ITEM_SELECTOR = `${ACCORDION_ITEM_SELECTOR}, ${KIND_ROOT(CompositeKind.Disclosure)}`;
826
- const PANEL_SELECTOR = '.iox-role-accordion-panel, .iox-role-disclosure-panel';
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';
827
919
  const BUTTON_SELECTOR = `.${AccordionClass.Button}`;
828
920
  const TRACK_SELECTOR = `.${AccordionClass.Track}`;
829
921
  const REGION_SELECTOR = `.${AccordionClass.Region}`;
@@ -832,16 +924,16 @@ const MAX_CLOSE_MS = 600;
832
924
  const REDUCED_MOTION = '(prefers-reduced-motion: reduce)';
833
925
  /** The composite (accordion or disclosure) an element belongs to (the element itself when it is a root). */
834
926
  function closestAccordion(el) {
835
- return el?.closest?.(ROOT_SELECTOR) ?? null;
927
+ return el?.closest?.(ROOT_SELECTOR$1) ?? null;
836
928
  }
837
929
  /** The item an element sits in (itself when it is one) — an accordion item, or a disclosure. */
838
930
  function closestAccordionItem(el) {
839
931
  return el?.closest?.(ITEM_SELECTOR) ?? null;
840
932
  }
841
- 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;
842
934
  /** The composite root whose items an item belongs to: a disclosure is its own root. */
843
- function rootOfItem(item) {
844
- return kindOf(item) === CompositeKind.Disclosure && item.matches(KIND_ROOT(CompositeKind.Disclosure)) ? item : closestAccordion(item.parentElement);
935
+ function accordionRootOfItem(item) {
936
+ return kindOf$1(item) === CompositeKind.Disclosure && item.matches(KIND_ROOT$1(CompositeKind.Disclosure)) ? item : closestAccordion(item.parentElement);
845
937
  }
846
938
  /** Subscribe to an item turning on / off — what the `itemOn` / `itemOff` interaction triggers listen to. The
847
939
  * item is the element's own item (the element itself when it is one). Returns the unsubscribe. */
@@ -857,20 +949,20 @@ function onAccordionItemState(el, on, handler) {
857
949
  function isAccordionItemOn(el) {
858
950
  return !!closestAccordionItem(el)?.hasAttribute(ON_ATTRIBUTE);
859
951
  }
860
- const nodeIdOf = (el) => {
952
+ const nodeIdOf$1 = (el) => {
861
953
  for (const c of Array.from(el.classList))
862
954
  if (c.startsWith('iox-node-'))
863
955
  return c.slice('iox-node-'.length);
864
956
  return '';
865
957
  };
866
- function behaviourOf(root) {
958
+ function behaviourOf$1(root) {
867
959
  const raw = root.getAttribute(DEFAULT_ON_ATTRIBUTE);
868
960
  return {
869
961
  multiple: root.hasAttribute(MULTIPLE_ATTRIBUTE),
870
962
  defaultOn: ACCORDION_DEFAULT_ONS.includes(raw) ? raw : AccordionDefaultOn.First,
871
963
  };
872
964
  }
873
- function setAttr(el, name, value) {
965
+ function setAttr$1(el, name, value) {
874
966
  if (!el)
875
967
  return;
876
968
  if (value === null) {
@@ -925,8 +1017,8 @@ class AccordionRuntime {
925
1017
  return;
926
1018
  this.open(this.keyOf(item, items));
927
1019
  };
928
- this.kind = options.kind ?? kindOf(root);
929
- this.behaviourValue = options.behaviour ?? behaviourOf(root);
1020
+ this.kind = options.kind ?? kindOf$1(root);
1021
+ this.behaviourValue = options.behaviour ?? behaviourOf$1(root);
930
1022
  root.setAttribute(COMPOSITE_ATTRIBUTE, this.kind);
931
1023
  RUNTIMES.set(root, this);
932
1024
  // The server rendered the first state; with no server state the root's default decides.
@@ -1059,7 +1151,7 @@ class AccordionRuntime {
1059
1151
  }
1060
1152
  const forced = this.options.forced?.() ?? new Set();
1061
1153
  const own = this.options.rootId;
1062
- 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);
1063
1155
  const next = new Set();
1064
1156
  items.forEach((item, index) => {
1065
1157
  const key = this.keyOf(item, items);
@@ -1076,31 +1168,31 @@ class AccordionRuntime {
1076
1168
  for (const item of items) {
1077
1169
  const key = this.keyOf(item, items);
1078
1170
  if (next.has(key) && !was.has(key))
1079
- dispatch(item, ITEM_ON_EVENT);
1171
+ dispatch$1(item, ITEM_ON_EVENT);
1080
1172
  else if (!next.has(key) && was.has(key))
1081
- dispatch(item, ITEM_OFF_EVENT);
1173
+ dispatch$1(item, ITEM_OFF_EVENT);
1082
1174
  }
1083
1175
  }
1084
1176
  stampItem(item, key, index, on, rootId, first) {
1085
1177
  const ids = accordionDomIds(rootId, key, this.kind);
1086
1178
  if (!item.hasAttribute(ITEM_KEY_ATTRIBUTE))
1087
1179
  item.setAttribute(ITEM_KEY_ATTRIBUTE, key);
1088
- setAttr(item, ON_ATTRIBUTE, on ? '' : null);
1180
+ setAttr$1(item, ON_ATTRIBUTE, on ? '' : null);
1089
1181
  const button = this.own(item, BUTTON_SELECTOR);
1090
1182
  const track = this.own(item, TRACK_SELECTOR);
1091
1183
  const region = this.own(item, REGION_SELECTOR);
1092
- const panel = this.own(item, PANEL_SELECTOR);
1093
- setAttr(button, 'type', 'button');
1094
- setAttr(button, 'id', ids.trigger);
1095
- setAttr(button, 'aria-expanded', on ? 'true' : 'false');
1096
- setAttr(button, 'aria-controls', ids.panel);
1097
- setAttr(button, ON_ATTRIBUTE, on ? '' : null);
1098
- setAttr(track, ON_ATTRIBUTE, on ? '' : null);
1099
- 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);
1100
1192
  const labelled = hasLabelledRegion(this.kind);
1101
- setAttr(region, 'role', labelled ? 'region' : null);
1102
- setAttr(region, 'id', ids.panel);
1103
- 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);
1104
1196
  if (!region)
1105
1197
  return;
1106
1198
  const pending = this.closing.get(key);
@@ -1109,7 +1201,7 @@ class AccordionRuntime {
1109
1201
  clearTimeout(pending);
1110
1202
  this.closing.delete(key);
1111
1203
  }
1112
- setAttr(region, 'hidden', null);
1204
+ setAttr$1(region, 'hidden', null);
1113
1205
  return;
1114
1206
  }
1115
1207
  if (region.hasAttribute('hidden') || pending)
@@ -1117,13 +1209,13 @@ class AccordionRuntime {
1117
1209
  // Closing: the track collapses first (its transition), THEN the region is hidden — hiding it at once
1118
1210
  // would collapse it with no motion. At first paint, or under reduced motion, it is hidden at once.
1119
1211
  if (first || this.reduced()) {
1120
- setAttr(region, 'hidden', UNTIL_FOUND);
1212
+ setAttr$1(region, 'hidden', UNTIL_FOUND);
1121
1213
  return;
1122
1214
  }
1123
1215
  const hide = () => {
1124
1216
  this.closing.delete(key);
1125
1217
  if (!item.hasAttribute(ON_ATTRIBUTE))
1126
- setAttr(region, 'hidden', UNTIL_FOUND);
1218
+ setAttr$1(region, 'hidden', UNTIL_FOUND);
1127
1219
  };
1128
1220
  this.closing.set(key, setTimeout(hide, this.closeDelay(track)));
1129
1221
  }
@@ -1146,7 +1238,7 @@ class AccordionRuntime {
1146
1238
  return this.items().map(i => this.own(i, BUTTON_SELECTOR)).filter((b) => !!b);
1147
1239
  }
1148
1240
  }
1149
- function dispatch(el, type) {
1241
+ function dispatch$1(el, type) {
1150
1242
  const Ctor = globalThis.CustomEvent;
1151
1243
  if (Ctor)
1152
1244
  el.dispatchEvent(new Ctor(type, { bubbles: false }));
@@ -1161,7 +1253,7 @@ function attachAccordionAt(root, options = {}) {
1161
1253
  /** Attach the runtime to every accordion under `container` that has none yet. Returns the new runtimes. */
1162
1254
  function attachAccordions(container, options = {}) {
1163
1255
  const out = [];
1164
- container.querySelectorAll(ROOT_SELECTOR).forEach(root => {
1256
+ container.querySelectorAll(ROOT_SELECTOR$1).forEach(root => {
1165
1257
  const runtime = attachAccordionAt(root, options);
1166
1258
  if (runtime)
1167
1259
  out.push(runtime);
@@ -1198,18 +1290,6 @@ var CompositeItemAction;
1198
1290
  CompositeItemAction["Close"] = "close";
1199
1291
  CompositeItemAction["Toggle"] = "toggle";
1200
1292
  })(CompositeItemAction || (CompositeItemAction = {}));
1201
- /**
1202
- * Run an item action on the element of an ITEM (an accordion item, or a disclosure) — the `openItem` /
1203
- * `closeItem` / `toggleItem` interaction actions, on the canvas and on the live site through this one function.
1204
- * Returns whether the item shows open afterwards, or null when `el` is in no running composite.
1205
- */
1206
- function runCompositeItemAction(el, action) {
1207
- const item = closestAccordionItem(el);
1208
- const runtime = item ? accordionRuntimeOf(rootOfItem(item)) : null;
1209
- if (!item || !runtime)
1210
- return null;
1211
- return runtime.run(action, runtime.keyOf(item));
1212
- }
1213
1293
  /** How much of an opened item must already be on screen for the action NOT to scroll to it (its header row). */
1214
1294
  const VISIBLE_HEAD_PX = 48;
1215
1295
  /** True when an item an action just opened is out of view and the page should scroll to it (T14: "opens that item
@@ -1226,21 +1306,21 @@ const DISCLOSURE_WORDS = {
1226
1306
  };
1227
1307
  /** A disclosure's one item key — its root IS its item. */
1228
1308
  const DISCLOSURE_ITEM_KEY = 'k1';
1229
- var Type$2;
1309
+ var Type$3;
1230
1310
  (function (Type) {
1231
1311
  Type["Container"] = "Container";
1232
1312
  Type["LinkedContainer"] = "LinkedContainer";
1233
1313
  Type["Text"] = "Text";
1234
- })(Type$2 || (Type$2 = {}));
1235
- var Style$2;
1314
+ })(Type$3 || (Type$3 = {}));
1315
+ var Style$3;
1236
1316
  (function (Style) {
1237
1317
  Style["Root"] = "box";
1238
1318
  Style["Trigger"] = "accordion-trigger";
1239
1319
  Style["Word"] = "body";
1240
1320
  Style["Panel"] = "accordion-panel";
1241
1321
  Style["Body"] = "body";
1242
- })(Style$2 || (Style$2 = {}));
1243
- const ZERO_BOX$1 = '0px 0px 0px 0px';
1322
+ })(Style$3 || (Style$3 = {}));
1323
+ const ZERO_BOX$2 = '0px 0px 0px 0px';
1244
1324
  const HIDDEN = 'none';
1245
1325
  const INHERIT = 'inherit';
1246
1326
  /** A word is a flex item of the toggle — `block` (never `inline`: the compiler drops a value equal to the CSS initial,
@@ -1249,7 +1329,7 @@ const SHOWN = 'block';
1249
1329
  /** What each part holds — layout only; its LOOK is its element style. The trigger sizes to its words (a button in
1250
1330
  * the flow, not a full-width header row — `fit-content` keeps it from stretching) and the panel spans the root. The
1251
1331
  * trigger is `flex`, not `inline-flex`: it is a flex item of the root, which blockifies it on the site anyway. */
1252
- const PART_BASE = {
1332
+ const PART_BASE$1 = {
1253
1333
  [DisclosureRole.Disclosure]: { display: 'flex', flexDirection: 'column', width: '100%', minHeight: 'auto', height: 'auto' },
1254
1334
  // The toggle reads the text around it (the page's body type, through the root) — a LinkedContainer is born with
1255
1335
  // its own literal font, which an accordion's heading masks and a disclosure's words would show.
@@ -1259,17 +1339,17 @@ const PART_BASE = {
1259
1339
  },
1260
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
1261
1341
  // padding is the click target.
1262
- [DisclosureRole.More]: { display: SHOWN, margin: ZERO_BOX$1, padding: ZERO_BOX$1, width: 'auto' },
1263
- [DisclosureRole.Less]: { display: HIDDEN, margin: ZERO_BOX$1, padding: ZERO_BOX$1, width: 'auto' },
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' },
1264
1344
  [DisclosureRole.Panel]: { display: 'block', width: '100%', minHeight: 'auto', height: 'auto' },
1265
1345
  };
1266
- const BODY_BASE = { margin: ZERO_BOX$1, padding: ZERO_BOX$1, minHeight: 'auto' };
1267
- function make$1(create, key, type, role, style, props, label) {
1268
- return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE[role] : undefined);
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);
1269
1349
  }
1270
1350
  /** One of the trigger's two words: shown while the disclosure is closed (`more`) or open (`less`). */
1271
1351
  function buildWord(create, role, words, rootId) {
1272
- const node = make$1(create, role === DisclosureRole.More ? 'more' : 'less', Type$2.Text, role, Style$2.Word, { content: words, tag: 'span' }, 'Text');
1352
+ const node = make$2(create, role === DisclosureRole.More ? 'more' : 'less', Type$3.Text, role, Style$3.Word, { content: words, tag: 'span' }, 'Text');
1273
1353
  node.stateStyles = { ...(node.stateStyles ?? {}), [ITEM_OPEN_STATE]: { display: role === DisclosureRole.More ? HIDDEN : SHOWN } };
1274
1354
  if (rootId)
1275
1355
  node.stateTriggers = { ...(node.stateTriggers ?? {}), [ITEM_OPEN_STATE]: rootId };
@@ -1278,19 +1358,19 @@ function buildWord(create, role, words, rootId) {
1278
1358
  /** A fresh Disclosure: "Read more" opens the text below and reads "Read less" while it is open; closed by default. */
1279
1359
  function buildDisclosureRecipe(create, options = {}) {
1280
1360
  const words = { ...DISCLOSURE_WORDS, ...(options.words ?? {}) };
1281
- const root = make$1(create, 'disclosure', Type$2.Container, DisclosureRole.Disclosure, Style$2.Root, {}, 'Disclosure');
1361
+ const root = make$2(create, 'disclosure', Type$3.Container, DisclosureRole.Disclosure, Style$3.Root, {}, 'Disclosure');
1282
1362
  root.itemKey = DISCLOSURE_ITEM_KEY;
1283
1363
  writeProp(root, CompositeBehaviourTrait.DefaultOn, options.open ? AccordionDefaultOn.First : AccordionDefaultOn.None);
1284
1364
  const icon = options.icon ?? AccordionIcon.Chevron;
1285
- const trigger = make$1(create, 'trigger', Type$2.LinkedContainer, DisclosureRole.Trigger, Style$2.Trigger, {}, 'Toggle');
1365
+ const trigger = make$2(create, 'trigger', Type$3.LinkedContainer, DisclosureRole.Trigger, Style$3.Trigger, {}, 'Toggle');
1286
1366
  trigger.children = [
1287
1367
  buildWord(create, DisclosureRole.More, words.more, root.id),
1288
1368
  buildWord(create, DisclosureRole.Less, words.less, root.id),
1289
1369
  ...(icon === AccordionIcon.None ? [] : [buildCompositeIcon(create, 'icon', icon, root.id, DisclosureRole.Icon)]),
1290
1370
  ];
1291
- const panel = make$1(create, 'panel', Type$2.Container, DisclosureRole.Panel, Style$2.Panel, {}, 'Panel');
1292
- const body = make$1(create, 'body', Type$2.Text, null, Style$2.Body, { content: words.body, tag: 'p' }, 'Text');
1293
- writeStyles(body, BODY_BASE);
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);
1294
1374
  panel.children = [body];
1295
1375
  root.children = [trigger, panel];
1296
1376
  return root;
@@ -1452,7 +1532,7 @@ const CARD_WORDS = {
1452
1532
  };
1453
1533
  /** The breakpoint whose layer stacks an "Image side" card (Breakpoint.Mobile, ≤767px). */
1454
1534
  const CARD_STACK_BREAKPOINT = 'mobile';
1455
- var Type$1;
1535
+ var Type$2;
1456
1536
  (function (Type) {
1457
1537
  Type["Container"] = "Container";
1458
1538
  Type["Image"] = "Image";
@@ -1460,8 +1540,8 @@ var Type$1;
1460
1540
  Type["Text"] = "Text";
1461
1541
  Type["Icon"] = "Icon";
1462
1542
  Type["ButtonBlock"] = "ButtonBlock";
1463
- })(Type$1 || (Type$1 = {}));
1464
- var Style$1;
1543
+ })(Type$2 || (Type$2 = {}));
1544
+ var Style$2;
1465
1545
  (function (Style) {
1466
1546
  Style["Card"] = "card";
1467
1547
  Style["Box"] = "box";
@@ -1474,7 +1554,7 @@ var Style$1;
1474
1554
  Style["Ghost"] = "button-ghost";
1475
1555
  Style["Icon"] = "icon";
1476
1556
  Style["Scrim"] = "media-scrim";
1477
- })(Style$1 || (Style$1 = {}));
1557
+ })(Style$2 || (Style$2 = {}));
1478
1558
  var Scheme;
1479
1559
  (function (Scheme) {
1480
1560
  Scheme["Default"] = "default";
@@ -1484,11 +1564,11 @@ const LINK_TYPE = 'linkType';
1484
1564
  const SAVE_ICON = 'ph ph-bookmark-simple';
1485
1565
  /** A value an element style writes — read from the contract, never retyped. */
1486
1566
  const styleValue = (style, trait) => String(elementStyleById(style)?.base[trait] ?? '');
1487
- const CARD_PAD = styleValue(Style$1.Card, 'padding');
1488
- const IMAGE_RADIUS = styleValue(Style$1.Media, 'borderRadius');
1489
- const TITLE_SIZE = styleValue(Style$1.Title, 'fontSize');
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');
1490
1570
  const LEAD_SIZE = 'var(--iox-size-lead)';
1491
- const ZERO_BOX = '0px 0px 0px 0px';
1571
+ const ZERO_BOX$1 = '0px 0px 0px 0px';
1492
1572
  /** What every card part holds in every layout — the recipe's box. */
1493
1573
  const CARD_BASE = {
1494
1574
  // No height: a card is a grid or flex item, and the cell's stretch is what makes a row's cards equal.
@@ -1496,25 +1576,25 @@ const CARD_BASE = {
1496
1576
  [CardRole.Card]: { display: 'flex', gap: '14px', width: '100%', minWidth: '0px', overflow: 'hidden', position: 'relative' },
1497
1577
  [CardRole.Media]: { display: 'block', width: '100%', minHeight: 'auto', flexGrow: '0', flexShrink: '0' },
1498
1578
  [CardRole.Content]: { display: 'flex', flexDirection: 'column', gap: '8px', width: '100%', minWidth: '0px', minHeight: 'auto' },
1499
- [CardRole.Eyebrow]: { margin: ZERO_BOX, padding: ZERO_BOX, minHeight: 'auto' },
1500
- [CardRole.Title]: { margin: ZERO_BOX },
1501
- [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' },
1502
1582
  // Buttons pinned to the bottom (board 8 Q: equal heights + buttons pinned): the content column grows,
1503
1583
  // and the row's auto top margin takes the slack.
1504
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' },
1505
1585
  };
1506
- const IMAGE_BASE$1 = { display: 'block', width: '100%', maxWidth: '100%' };
1586
+ const IMAGE_BASE$2 = { display: 'block', width: '100%', maxWidth: '100%' };
1507
1587
  /** A button's label and a pill's words: inherit the type and ink of what holds them. */
1508
- 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 };
1509
1589
  const INLINE_ICON = { fontSize: '1em' };
1510
1590
  const IN_FLOW = { position: 'relative', top: 'auto', right: 'auto', bottom: 'auto', left: 'auto' };
1511
- 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 };
1512
1592
  const LAYOUTS = {
1513
1593
  [CardLayout.Top]: {
1514
1594
  card: { flexDirection: 'column', alignItems: 'stretch', justifyContent: 'flex-start', minHeight: 'auto', padding: CARD_PAD },
1515
1595
  media: { ...IN_FLOW, flexBasis: 'auto' },
1516
1596
  image: { aspectRatio: '4 / 3', height: 'auto', minHeight: 'auto', borderRadius: IMAGE_RADIUS },
1517
- content: GROWING_CONTENT, contentStyle: Style$1.Box, contentScheme: Scheme.Default,
1597
+ content: GROWING_CONTENT, contentStyle: Style$2.Box, contentScheme: Scheme.Default,
1518
1598
  title: { fontSize: TITLE_SIZE },
1519
1599
  badgeOnMedia: true,
1520
1600
  },
@@ -1522,7 +1602,7 @@ const LAYOUTS = {
1522
1602
  card: { flexDirection: 'row', alignItems: 'stretch', justifyContent: 'flex-start', minHeight: 'auto', padding: CARD_PAD },
1523
1603
  media: { ...IN_FLOW, flexBasis: '42%' },
1524
1604
  image: { aspectRatio: 'auto', height: '100%', minHeight: '180px', borderRadius: IMAGE_RADIUS },
1525
- content: GROWING_CONTENT, contentStyle: Style$1.Box, contentScheme: Scheme.Default,
1605
+ content: GROWING_CONTENT, contentStyle: Style$2.Box, contentScheme: Scheme.Default,
1526
1606
  title: { fontSize: TITLE_SIZE },
1527
1607
  badgeOnMedia: true,
1528
1608
  stack: {
@@ -1532,11 +1612,11 @@ const LAYOUTS = {
1532
1612
  },
1533
1613
  },
1534
1614
  [CardLayout.Overlay]: {
1535
- 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 },
1536
1616
  media: { position: 'absolute', top: '0px', right: '0px', bottom: '0px', left: '0px', flexBasis: 'auto' },
1537
1617
  image: { aspectRatio: 'auto', height: '100%', minHeight: 'auto', borderRadius: '0px' },
1538
1618
  content: { position: 'relative', flexGrow: '0', flexShrink: '0', flexBasis: 'auto', padding: `72px ${CARD_PAD} ${CARD_PAD} ${CARD_PAD}` },
1539
- contentStyle: Style$1.Scrim, contentScheme: Scheme.Inverse,
1619
+ contentStyle: Style$2.Scrim, contentScheme: Scheme.Inverse,
1540
1620
  title: { fontSize: TITLE_SIZE },
1541
1621
  badgeOnMedia: false,
1542
1622
  },
@@ -1544,7 +1624,7 @@ const LAYOUTS = {
1544
1624
  card: { flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', minHeight: 'auto', padding: CARD_PAD },
1545
1625
  media: { ...IN_FLOW, flexBasis: '72px' },
1546
1626
  image: { aspectRatio: '1 / 1', height: 'auto', minHeight: 'auto', borderRadius: IMAGE_RADIUS },
1547
- content: GROWING_CONTENT, contentStyle: Style$1.Box, contentScheme: Scheme.Default,
1627
+ content: GROWING_CONTENT, contentStyle: Style$2.Box, contentScheme: Scheme.Default,
1548
1628
  title: { fontSize: LEAD_SIZE },
1549
1629
  badgeOnMedia: false,
1550
1630
  partsOff: [CardPart.Text, CardPart.Actions],
@@ -1555,7 +1635,7 @@ const STACK_KEYS = {
1555
1635
  media: Object.keys(LAYOUTS[CardLayout.Side].stack.media),
1556
1636
  image: Object.keys(LAYOUTS[CardLayout.Side].stack.image),
1557
1637
  };
1558
- function make(create, key, type, role, elementStyle, props = {}, label) {
1638
+ function make$1(create, key, type, role, elementStyle, props = {}, label) {
1559
1639
  const node = create({ key, type, props, elementStyle });
1560
1640
  if (role)
1561
1641
  node.role = role;
@@ -1565,16 +1645,16 @@ function make(create, key, type, role, elementStyle, props = {}, label) {
1565
1645
  return node;
1566
1646
  }
1567
1647
  function buttonLabel(create, key, words) {
1568
- const label = make(create, key, Type$1.Text, null, Style$1.Label, { content: words, tag: 'span' });
1569
- 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);
1570
1650
  return label;
1571
1651
  }
1572
1652
  /** Each optional part (and the locked title) as the recipe builds it. */
1573
1653
  const PART_BUILDERS = {
1574
1654
  [CardPart.Media]: (create) => {
1575
- const media = make(create, 'media', Type$1.Container, CardRole.Media, Style$1.Box, {}, 'Media');
1576
- const image = make(create, 'image', Type$1.Image, null, Style$1.Media, { src: '', alt: '' });
1577
- writeStyles(image, IMAGE_BASE$1);
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);
1578
1658
  media.children = [image];
1579
1659
  return media;
1580
1660
  },
@@ -1584,14 +1664,14 @@ const PART_BUILDERS = {
1584
1664
  badge.label = 'Badge';
1585
1665
  return badge;
1586
1666
  },
1587
- [CardPart.Eyebrow]: (create, words) => make(create, 'eyebrow', Type$1.Text, CardRole.Eyebrow, Style$1.Eyebrow, { content: words.eyebrow, tag: 'p' }, 'Eyebrow'),
1588
- [CardPart.Text]: (create, words) => make(create, 'text', Type$1.Text, CardRole.Text, Style$1.Body, { content: words.text, tag: 'p' }, 'Text'),
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'),
1589
1669
  [CardPart.Actions]: (create, words) => {
1590
- const actions = make(create, 'actions', Type$1.Container, CardRole.Actions, Style$1.Box, {}, 'Buttons');
1591
- const primary = make(create, 'primary', Type$1.ButtonBlock, null, Style$1.Primary);
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);
1592
1672
  primary.children = [buttonLabel(create, 'primary-label', words.primary)];
1593
- const ghost = make(create, 'secondary', Type$1.ButtonBlock, null, Style$1.Ghost);
1594
- const icon = make(create, 'secondary-icon', Type$1.Icon, null, Style$1.Icon, { iconClass: SAVE_ICON });
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 });
1595
1675
  writeStyles(icon, INLINE_ICON);
1596
1676
  ghost.children = [icon, buttonLabel(create, 'secondary-label', words.secondary)];
1597
1677
  actions.children = [primary, ghost];
@@ -1599,15 +1679,15 @@ const PART_BUILDERS = {
1599
1679
  },
1600
1680
  };
1601
1681
  function buildTitle(create, words) {
1602
- return make(create, 'title', Type$1.Heading, CardRole.Title, Style$1.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');
1603
1683
  }
1604
1684
  /** A fresh Card v2: every part a node with its role and element style, laid out by `options.layout`. */
1605
1685
  function buildCardRecipe(create, options = {}) {
1606
1686
  const words = { ...CARD_WORDS, ...(options.words ?? {}) };
1607
1687
  const tone = options.badgeTone ?? BadgeTone.Accent;
1608
1688
  const on = (part) => options.parts?.[part] !== false;
1609
- const card = make(create, 'card', Type$1.Container, CardRole.Card, Style$1.Card, {}, 'Card');
1610
- const content = make(create, 'content', Type$1.Container, CardRole.Content, Style$1.Box, {}, 'Content');
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');
1611
1691
  content.children = [
1612
1692
  ...(on(CardPart.Eyebrow) ? [PART_BUILDERS[CardPart.Eyebrow](create, words, tone)] : []),
1613
1693
  buildTitle(create, words),
@@ -1638,7 +1718,7 @@ function cardRootOf(node, path) {
1638
1718
  function cardPartsOf(card) {
1639
1719
  return Object.fromEntries(CARD_PARTS.map(part => [part, !!findByRole(card, CARD_PART_ROLE[part])]));
1640
1720
  }
1641
- const imageOf = (media) => media?.children?.find(c => c.type === Type$1.Image);
1721
+ const imageOf = (media) => media?.children?.find(c => c.type === Type$2.Image);
1642
1722
  /** Put the badge where the layout says: the Media's top corner, or the head of the content column
1643
1723
  * (Image behind text, Compact, or no Media at all). The corner's insets are logical, in iox-design.css
1644
1724
  * (mirrored in RTL); `position` is the node's own, because the editing canvas forces every node's
@@ -1670,7 +1750,7 @@ function applyPartLayout(card, spec, part, baseline) {
1670
1750
  setStack(part, spec.stack?.media, STACK_KEYS.media);
1671
1751
  const image = imageOf(part);
1672
1752
  if (image) {
1673
- writeStyles(image, { ...IMAGE_BASE$1, ...spec.image });
1753
+ writeStyles(image, { ...IMAGE_BASE$2, ...spec.image });
1674
1754
  setStack(image, spec.stack?.image, STACK_KEYS.image);
1675
1755
  }
1676
1756
  return;
@@ -1921,17 +2001,17 @@ function sliderSlides(children) {
1921
2001
  return (children ?? []).filter(c => !isSliderPart(c));
1922
2002
  }
1923
2003
  // ── the parts' recipe ───────────────────────────────────────────────────────────────────────────────
1924
- var Type;
2004
+ var Type$1;
1925
2005
  (function (Type) {
1926
2006
  Type["ButtonBlock"] = "ButtonBlock";
1927
2007
  Type["Icon"] = "Icon";
1928
2008
  Type["Container"] = "Container";
1929
- })(Type || (Type = {}));
1930
- var Style;
2009
+ })(Type$1 || (Type$1 = {}));
2010
+ var Style$1;
1931
2011
  (function (Style) {
1932
2012
  Style["Control"] = "button-secondary";
1933
2013
  Style["Icon"] = "icon";
1934
- })(Style || (Style = {}));
2014
+ })(Style$1 || (Style$1 = {}));
1935
2015
  /** Each part's glyph (Phosphor) and its default accessible name. */
1936
2016
  const SLIDER_PART_GLYPH = {
1937
2017
  [SliderRole.Pause]: 'ph ph-pause',
@@ -1973,7 +2053,7 @@ function legacyLookOf(slider) {
1973
2053
  /** One part, from the recipe (board 6: `button-secondary` controls, token dots) or in a migrated slider's legacy look. */
1974
2054
  function buildSliderPart(create, role, legacy) {
1975
2055
  if (role === SliderRole.Pagination) {
1976
- const dots = makeCompositePart(create, { key: 'pagination', type: Type.Container, props: {}, elementStyle: NO_ELEMENT_STYLE }, role, PART_LABEL[role], legacy?.paginationColor ? { ...PAGINATION_BASE, color: legacy.paginationColor } : PAGINATION_BASE);
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);
1977
2057
  dots.children = [];
1978
2058
  return dots;
1979
2059
  }
@@ -1989,9 +2069,9 @@ function buildSliderPart(create, role, legacy) {
1989
2069
  const props = { tag: 'button', type: 'button' };
1990
2070
  if (role !== SliderRole.Pause)
1991
2071
  props[SLIDER_NAME_TRAIT] = role === SliderRole.Prev ? SLIDER_WORDS.en.prev : SLIDER_WORDS.en.next;
1992
- const button = makeCompositePart(create, { key, type: Type.ButtonBlock, props, elementStyle: legacy ? NO_ELEMENT_STYLE : Style.Control }, role, PART_LABEL[role], base);
2072
+ const button = makeCompositePart(create, { key, type: Type$1.ButtonBlock, props, elementStyle: legacy ? NO_ELEMENT_STYLE : Style$1.Control }, role, PART_LABEL[role], base);
1993
2073
  const iconBase = legacy ? { ...ICON_BASE, fontSize: `${Math.round((legacy.navigationSize ?? 44) * 0.6)}px` } : ICON_BASE;
1994
- const icon = makeCompositePart(create, { key: `${key}-icon`, type: Type.Icon, props: { iconClass: SLIDER_PART_GLYPH[role] }, elementStyle: legacy ? NO_ELEMENT_STYLE : Style.Icon }, null, 'Icon', iconBase);
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);
1995
2075
  button.children = [icon];
1996
2076
  return button;
1997
2077
  }
@@ -2175,6 +2255,14 @@ const COMPOSITE_ROLE_SPECS = {
2175
2255
  [DisclosureRole.More]: { parent: DisclosureRole.Trigger },
2176
2256
  [DisclosureRole.Less]: { parent: DisclosureRole.Trigger },
2177
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 },
2178
2266
  // CC3 — a Slider's controls are its own direct children (Swiper is handed them). The pause control stays while
2179
2267
  // Autoplay is on (WCAG 2.2.2): the Autoplay switch removes it, never Delete. The arrows and the dots follow
2180
2268
  // their switches and may be deleted.
@@ -2195,6 +2283,8 @@ var StructureRefusal;
2195
2283
  StructureRefusal["LockedPart"] = "locked-part";
2196
2284
  /** A locked part can only live directly inside its own parent role. */
2197
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";
2198
2288
  })(StructureRefusal || (StructureRefusal = {}));
2199
2289
  /**
2200
2290
  * Why the structure guard refuses `edit` on `node`, or null when it is allowed. `ancestors` (Move only) =
@@ -2202,11 +2292,13 @@ var StructureRefusal;
2202
2292
  */
2203
2293
  function structureRefusal(node, edit, ancestors = []) {
2204
2294
  const spec = node?.role ? COMPOSITE_ROLE_SPECS[node.role] : undefined;
2295
+ const parent = ancestors[ancestors.length - 1];
2296
+ if (edit === StructureEdit.Move && acceptsRefusal(node, parent))
2297
+ return StructureRefusal.NotAccepted;
2205
2298
  if (!spec?.locked && !spec?.pinned)
2206
2299
  return null;
2207
2300
  if (edit === StructureEdit.Delete)
2208
2301
  return spec.locked ? StructureRefusal.LockedPart : null;
2209
- const parent = ancestors[ancestors.length - 1];
2210
2302
  const placed = spec.parentType ? parent?.type === spec.parentType : parent?.role === spec.parent;
2211
2303
  return placed ? null : StructureRefusal.LockedPlacement;
2212
2304
  }
@@ -2215,6 +2307,13 @@ function structureRefusal(node, edit, ancestors = []) {
2215
2307
  function isLockedPart(node) {
2216
2308
  return !!(node?.role && COMPOSITE_ROLE_SPECS[node.role]?.locked);
2217
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
+ }
2218
2317
 
2219
2318
  const SLIDE_VISIBLE = 'swiper-slide-visible';
2220
2319
  const RUNTIME_KEY = '__ioxCarousel';
@@ -2400,7 +2499,7 @@ const SLIDE_BASE = {
2400
2499
  width: '100%', height: '100%', aspectRatio: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'center',
2401
2500
  justifyContent: 'center', gap: '12px', padding: '56px 64px 16px 64px',
2402
2501
  };
2403
- const IMAGE_BASE = { width: '100%', height: 'auto', maxHeight: '72dvh', minHeight: '0px', objectFit: 'contain', aspectRatio: '4 / 3', flexShrink: '1' };
2502
+ const IMAGE_BASE$1 = { width: '100%', height: 'auto', maxHeight: '72dvh', minHeight: '0px', objectFit: 'contain', aspectRatio: '4 / 3', flexShrink: '1' };
2404
2503
  const CAPTION_BASE = { textAlign: 'center', margin: ZERO, padding: ZERO, minHeight: 'auto', width: 'auto' };
2405
2504
  /** The counter "3 / 6" sits at the START of the top row, Close at its end (board 6). */
2406
2505
  const COUNTER_BASE = { color: 'var(--iox-text-muted)', top: '12px', alignItems: 'flex-start', justifyContent: 'flex-start' };
@@ -2421,7 +2520,7 @@ function bindRow(node, prop, source, field) {
2421
2520
  }
2422
2521
  function buildSlide(create, key, options, rowAlias) {
2423
2522
  const slide = makeCompositePart(create, { key, type: SLIDE_TYPE, props: {}, elementStyle: NO_ELEMENT_STYLE }, null, 'Slide', SLIDE_BASE);
2424
- const image = makeCompositePart(create, { key: `${key}-image`, type: 'Image', props: { src: '', alt: '' }, elementStyle: 'media' }, null, 'Image', IMAGE_BASE);
2523
+ const image = makeCompositePart(create, { key: `${key}-image`, type: 'Image', props: { src: '', alt: '' }, elementStyle: 'media' }, null, 'Image', IMAGE_BASE$1);
2425
2524
  const caption = makeCompositePart(create, { key: `${key}-caption`, type: 'Text', props: { content: 'Caption', tag: 'p' }, elementStyle: 'small' }, null, 'Caption', CAPTION_BASE);
2426
2525
  if (rowAlias) {
2427
2526
  bindRow(image, 'src', rowAlias, options.imageField ?? 'item');
@@ -2525,13 +2624,897 @@ function rowIndexOf(el) {
2525
2624
  return Number.isInteger(index) && index >= 0 ? index : 0;
2526
2625
  }
2527
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
+ }
3510
+
2528
3511
  /**
2529
3512
  * @vectoriox/iox-ui/composite-core — composites built from the builder's own parts (composite-components
2530
3513
  * plan §3a). FRAMEWORK-FREE: the builder, the SSR engine and iox-cms (generation-core's composer, the site
2531
3514
  * build) read the same recipes, roles and structure rules. Card v2 + Badge / Chip (T23 / T25) are the
2532
3515
  * first; the Accordion (CC2) adds the first composite RUNTIME (accordion-runtime.ts — DOM only, no
2533
- * framework), which the Disclosure (CC2.1) runs too — a one-item accordion whose root is its item; Tabs join with
2534
- * their own phase.
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.
2535
3518
  * ⚠️ Consumers wire the usual trio — ng-package raw-ts assets, jest moduleNameMapper, tsconfig paths.
2536
3519
  */
2537
3520
 
@@ -2539,5 +3522,5 @@ function rowIndexOf(el) {
2539
3522
  * Generated bundle index. Do not edit.
2540
3523
  */
2541
3524
 
2542
- export { ACCORDION_BIND_PROP, ACCORDION_CONTENT_LIMIT, ACCORDION_DEFAULT_ONS, ACCORDION_ICONS, ACCORDION_ICON_GLYPH, ACCORDION_NEW_ITEM_WORDS, ACCORDION_WORDS, AccordionClass, AccordionDefaultOn, AccordionEditRefusal, AccordionIcon, AccordionKeyAction, AccordionRole, AccordionRuntime, BADGE_TONES, BADGE_TONE_ICONS, BadgeTone, CARD_LAYOUTS, CARD_LAYOUT_CUSTOM, CARD_PARTS, CARD_PART_ROLE, CARD_STACK_BREAKPOINT, CARD_WORDS, CAROUSEL_QUOTES, CHIP_ICON, CHIP_STYLE, COMPOSITE_ATTRIBUTE, COMPOSITE_KIND_ROLES, COMPOSITE_ROLE_SPECS, CardLayout, CardPart, CardRole, CarouselA11y, CompositeBehaviourTrait, CompositeItemAction, CompositeKind, DEFAULT_ON_ATTRIBUTE, DISCLOSURE_ITEM_KEY, DISCLOSURE_WORDS, DisclosureRole, ITEM_KEY_ATTRIBUTE, ITEM_OFF_EVENT, ITEM_ON_EVENT, ITEM_OPEN_STATE, LIGHTBOX_ARROWS_HIDE_AT, LIGHTBOX_LABEL, MULTIPLE_ATTRIBUTE, NO_ELEMENT_STYLE, ON_ATTRIBUTE, ON_STATE, PILL_LABEL_STYLE, ROW_INDEX_ATTRIBUTE, SCOPE_ATTRIBUTE, SLIDER_NAME_TRAIT, SLIDER_PART_GLYPH, SLIDER_PART_ROLES, SLIDER_PLAY_GLYPH, SLIDER_TYPE, SLIDER_WORDS, SLIDE_TO_ROW, SLIDE_TYPE, SWIPER_THEME_COLOR, SliderFlag, SliderRole, StructureEdit, StructureRefusal, UNTIL_FOUND, accordionAnswerOf, accordionBehaviour, accordionDomIds, accordionFocusIndex, accordionHeadingLevel, accordionItemAttributes, accordionItemLabel, accordionItemOf, accordionItemState, accordionItems, accordionKeyAction, accordionPanelAttributes, accordionPanelOf, accordionPartStyle, accordionQuestionOf, accordionRegionAttributes, accordionRemovalRefusal, accordionRootAttributes, accordionRootOf, accordionRuntimeOf, accordionScope, accordionToContent, accordionToManual, accordionTrackAttributes, accordionTriggerAttributes, accordionTriggerOf, addAccordionItem, applyCardLayout, attachAccordionAt, attachAccordions, attachCarouselA11y, badgeStyleId, badgeToneOf, buildAccordionItem, buildAccordionRecipe, buildBadge, buildCardRecipe, buildCarouselRecipe, buildChip, buildCompositeIcon, buildDisclosureRecipe, buildLightboxRecipe, buildSliderPart, canOpenAtClickedItem, cardPartsOf, cardRootOf, cardTitleOf, carouselOf, clearLayerStyles, closeAccordionItem, closestAccordion, closestAccordionItem, compositeItemOf, compositeItems, compositeKindOfRole, compositeRootOf, compositeRowKeys, compositeScopeId, detachAccordion, detectAccordionIcon, detectCardLayout, disclosureOpenByDefault, domSafeKey, findByRole, hasLabelledRegion, hasSliderParts, initialAccordionOpen, isAccordionFromContent, isAccordionItemOn, isAccordionPart, isAccordionRoot, isCardLinked, isCardRoot, isCompositeItem, isCompositePanel, isCompositeRoot, isCompositeTrigger, isDisclosurePart, isDisclosureRoot, isLockedPart, isPillNode, isSlider, isSliderPart, itemOpenTriggerIds, lightboxSliderOf, makeCompositePart, migrateSliderControls, moveAccordionItem, needsSliderMigration, nextAccordionState, nextItemKey, onAccordionItemState, openAccordionItem, parentInComposite, pickAccordionAutoBinds, readLayerStyle, readProp, readStyle, removeFromComposite, renameAccordionItem, repeatSourceOf, rowIndexOf, rowItemKey, rowSourceOf, runCompositeItemAction, sameRepeatSource, serializeDomAttributes, setAccordionBehaviour, setAccordionIcon, setBadgeTone, setCardLinked, setCardPart, setSliderFlag, shouldScrollToItem, slideIndexForRow, sliderLiveRegion, sliderPart, sliderPartElement, sliderPauseAttributes, sliderPauseGlyph, sliderSlides, sliderWords, splitSliderChildren, structureRefusal, syncSliderParts, walkComposite, wantedSliderParts, writeLayerStyles, writeProp, writeStyles };
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 };
2543
3526
  //# sourceMappingURL=vectoriox-iox-ui-composite-core.mjs.map