@vectoriox/iox-ui 5.38.0 → 5.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) 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 +125 -3
  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 +43 -0
  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 +68 -1
  29. package/src/generation-core/node-baselines.generated.ts +8 -2
  30. package/src/generation-core/patterns.ts +3 -0
  31. package/src/generation-core/site-spec.generated.ts +46 -0
  32. package/styles/iox-design.css +16 -0
@@ -852,6 +852,46 @@ declare const SPEC_SITE_PATTERNS: {
852
852
  }];
853
853
  };
854
854
  };
855
+ readonly "feature.tabbed": {
856
+ readonly family: "feature";
857
+ readonly label: "Features — tabs that switch";
858
+ readonly node: {
859
+ readonly t: "Section";
860
+ readonly es: "section";
861
+ readonly c: readonly [{
862
+ readonly t: "Container";
863
+ readonly es: "box";
864
+ readonly lay: {
865
+ readonly display: "flex";
866
+ readonly flexDirection: "column";
867
+ readonly gap: "14px";
868
+ readonly maxWidth: "1100px";
869
+ readonly width: "100%";
870
+ readonly margin: "0px auto 0px auto";
871
+ };
872
+ readonly c: readonly [{
873
+ readonly t: "Text";
874
+ readonly es: "eyebrow";
875
+ readonly slot: "eyebrow";
876
+ readonly ph: "copy";
877
+ readonly text: "What we offer";
878
+ }, {
879
+ readonly t: "Heading";
880
+ readonly es: "h2";
881
+ readonly level: "h2";
882
+ readonly slot: "title";
883
+ readonly ph: "copy";
884
+ readonly text: "What we design";
885
+ }, {
886
+ readonly t: "Container";
887
+ readonly recipe: "tabs";
888
+ readonly src: "$binding?";
889
+ readonly ph: "copy";
890
+ readonly name: "What we design";
891
+ }];
892
+ }];
893
+ };
894
+ };
855
895
  readonly "hero.full-bleed-media": {
856
896
  readonly family: "hero";
857
897
  readonly label: "Hero — words over a background video";
@@ -1400,6 +1440,9 @@ declare enum PatternRecipe {
1400
1440
  /** CC2: the Accordion — Manual (three placeholder items), or From content when the section names a list
1401
1441
  * `binding` (`src: '$binding?'`): one template item bound to the type's question / answer fields. */
1402
1442
  Accordion = "accordion",
1443
+ /** CC4a: Tabs — Manual (three placeholder tabs), or From content when the section names a list `binding`: the tab
1444
+ * list and the panels are both Repeaters over it, one row giving its tab AND its panel. */
1445
+ Tabs = "tabs",
1403
1446
  /** CC3: the accessible carousel — a named Slider whose controls are parts, one quote per slide (placeholders). */
1404
1447
  Carousel = "carousel",
1405
1448
  /** CC3: the Lightbox — a Dialog holding a Slider over the node's `src` (the gallery's own source). */
package/index.d.ts CHANGED
@@ -1706,21 +1706,26 @@ declare enum ComponentState {
1706
1706
  Current = "current",
1707
1707
  /** A composite part that IS on — an open accordion item, its header, its panel (board 9 "Open"). */
1708
1708
  On = "on",
1709
- /** A part INSIDE an item, styled by its item being open (board 9 "Item open") — a TARGETED state: its
1710
- * trigger is the item (`stateTriggers['trigger-on']`), compiled through `targetedStateSelector`. */
1709
+ /** A part INSIDE an item, styled by its item being open (board 9 "Item open"; "Tab active" inside a tab or a panel)
1710
+ * — a TARGETED state: its trigger is the item (`stateTriggers['trigger-on']`), compiled through
1711
+ * `targetedStateSelector`. On a node OUTSIDE every item (composite T20, CC4a) the author PICKS the item — "when tab
1712
+ * Night is on" — the same state and the same `:has()` scoped to the nearest common ancestor. */
1711
1713
  ItemOpen = "trigger-on"
1712
1714
  }
1713
1715
  /** The attribute selector each (non-targeted) component state compiles to. */
1714
1716
  declare const COMPONENT_STATE_SELECTOR: Readonly<Record<string, string>>;
1715
1717
  /** The composite roles that are themselves "on" when their item is open (they carry `data-iox-on`): an accordion's
1716
- * item, header and panel; a disclosure (its own item), its toggle and its panel (CC2.1). The main entry does not
1717
- * import composite-core, so the roles are spelled here — `style-compiler.spec` pins them to its enums. */
1718
+ * item, header and panel; a disclosure (its own item), its toggle and its panel (CC2.1); a tab and its panel (CC4a —
1719
+ * "Active"). The main entry does not import composite-core, so the roles are spelled here — `style-compiler.spec` pins
1720
+ * them to its enums. */
1718
1721
  declare const ON_STATE_ROLES: ReadonlyArray<string>;
1719
1722
  /**
1720
1723
  * The component states a node offers in the Style panel's "Component state" group: Current page (a link),
1721
- * Open (an accordion's item, header or panel), Item open (any other part inside an item — `insideItem`).
1724
+ * Open / Active (an accordion's item, header or panel; a tab or its panel), Item open (any other part inside an item —
1725
+ * `insideItem`), and — on a node outside every item, on a page that has items to pick (`itemsToPick`, composite T20) —
1726
+ * the same Item open state with a picked trigger.
1722
1727
  */
1723
- declare function componentStatesFor(type: string | null | undefined, role?: string | null, insideItem?: boolean): ComponentState[];
1728
+ declare function componentStatesFor(type: string | null | undefined, role?: string | null, insideItem?: boolean, itemsToPick?: boolean): ComponentState[];
1724
1729
  /**
1725
1730
  * What follows `.iox-node-<id>` in a stored state's rule: `:hover`, `:focus-visible`, or a component
1726
1731
  * state's attribute selector (`[aria-current="page"]`). Shared by the builder canvas and both engine paths.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.38.0",
3
+ "version": "5.39.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -1,9 +1,11 @@
1
- import { AccordionRole, CompositeItemAction, DisclosureRole, SLIDE_TO_ROW } from '@vectoriox/iox-ui/composite-core';
1
+ import { AccordionRole, CompositeItemAction, DisclosureRole, SLIDE_TO_ROW, TabsRole } from '@vectoriox/iox-ui/composite-core';
2
2
 
3
3
  import { ActionDescriptor } from './action.model';
4
4
 
5
5
  /** The items an item action may target: an accordion's item, or a disclosure (its own one item). */
6
6
  const ITEM_TARGET_ROLES = [AccordionRole.Item, DisclosureRole.Disclosure];
7
+ /** Open also takes a TAB (CC4a): it turns it on. A Tabs always shows one, so Close and Toggle never aim at one. */
8
+ const OPEN_TARGET_ROLES = [...ITEM_TARGET_ROLES, TabsRole.Trigger];
7
9
 
8
10
  /**
9
11
  * The ELEMENT action catalog (actions plan A1) — what a node can DO, as data. One list feeds the
@@ -58,7 +60,7 @@ export const ELEMENT_ACTION_DESCRIPTORS: readonly ActionDescriptor[] = Object.fr
58
60
  // opens FAQ item 3). The canvas RUNS them, like the dialog actions: it is the same runtime, and an item you
59
61
  // cannot open from the button is a button you cannot design.
60
62
  { type: 'openItem', labelKey: 'builder.actions.openItem', kind: 'instant', group: 'item', builderBehaviour: 'run',
61
- target: { required: true, roles: ITEM_TARGET_ROLES } },
63
+ target: { required: true, roles: OPEN_TARGET_ROLES } },
62
64
  { type: 'closeItem', labelKey: 'builder.actions.closeItem', kind: 'instant', group: 'item', builderBehaviour: 'run',
63
65
  target: { required: true, roles: ITEM_TARGET_ROLES } },
64
66
  { type: 'toggleItem', labelKey: 'builder.actions.toggleItem', kind: 'instant', group: 'item', builderBehaviour: 'run',
@@ -3,7 +3,7 @@ import {
3
3
  ACCORDION_DEFAULT_ONS, closeAccordionItem, COMPOSITE_ATTRIBUTE, DEFAULT_ON_ATTRIBUTE, hasLabelledRegion, initialAccordionOpen, ITEM_KEY_ATTRIBUTE,
4
4
  MULTIPLE_ATTRIBUTE, nextAccordionState, ON_ATTRIBUTE, openAccordionItem, SCOPE_ATTRIBUTE, UNTIL_FOUND,
5
5
  } from './accordion';
6
- import { CompositeKind } from './composite-kinds';
6
+ import { COMPOSITE_KIND_ROLES, CompositeKind, ToggleCompositeKind } from './composite-kinds';
7
7
 
8
8
  /**
9
9
  * The Accordion's RUNTIME (CC2) — ONE copy, DOM only, for the builder canvas and the live site (both engine
@@ -33,7 +33,7 @@ export const ITEM_ON_EVENT = 'iox-item-on';
33
33
  export const ITEM_OFF_EVENT = 'iox-item-off';
34
34
 
35
35
  const KIND_ROOT = (kind: CompositeKind) => `[${COMPOSITE_ATTRIBUTE}="${kind}"]`;
36
- const ROOT_SELECTOR = Object.values(CompositeKind).map(KIND_ROOT).join(', ');
36
+ const ROOT_SELECTOR = (Object.keys(COMPOSITE_KIND_ROLES) as ToggleCompositeKind[]).map(KIND_ROOT).join(', ');
37
37
  /** An accordion's items are its `.iox-role-accordion-item` children; a disclosure is its own item. */
38
38
  const ACCORDION_ITEM_SELECTOR = '.iox-role-accordion-item';
39
39
  const ITEM_SELECTOR = `${ACCORDION_ITEM_SELECTOR}, ${KIND_ROOT(CompositeKind.Disclosure)}`;
@@ -75,7 +75,7 @@ const kindOf = (root: Element): CompositeKind =>
75
75
  root.getAttribute(COMPOSITE_ATTRIBUTE) === CompositeKind.Disclosure ? CompositeKind.Disclosure : CompositeKind.Accordion;
76
76
 
77
77
  /** The composite root whose items an item belongs to: a disclosure is its own root. */
78
- function rootOfItem(item: HTMLElement): HTMLElement | null {
78
+ export function accordionRootOfItem(item: HTMLElement): HTMLElement | null {
79
79
  return kindOf(item) === CompositeKind.Disclosure && item.matches(KIND_ROOT(CompositeKind.Disclosure)) ? item : closestAccordion(item.parentElement);
80
80
  }
81
81
 
@@ -421,18 +421,6 @@ export enum CompositeItemAction {
421
421
  Toggle = 'toggle',
422
422
  }
423
423
 
424
- /**
425
- * Run an item action on the element of an ITEM (an accordion item, or a disclosure) — the `openItem` /
426
- * `closeItem` / `toggleItem` interaction actions, on the canvas and on the live site through this one function.
427
- * Returns whether the item shows open afterwards, or null when `el` is in no running composite.
428
- */
429
- export function runCompositeItemAction(el: Element | null | undefined, action: CompositeItemAction): boolean | null {
430
- const item = closestAccordionItem(el);
431
- const runtime = item ? accordionRuntimeOf(rootOfItem(item)) : null;
432
- if (!item || !runtime) return null;
433
- return runtime.run(action, runtime.keyOf(item));
434
- }
435
-
436
424
  /** How much of an opened item must already be on screen for the action NOT to scroll to it (its header row). */
437
425
  const VISIBLE_HEAD_PX = 48;
438
426
 
@@ -1,7 +1,7 @@
1
1
  import { readOwnedTrait } from '@vectoriox/iox-ui/design-core';
2
2
 
3
3
  import {
4
- AccordionRole, COMPOSITE_KIND_ROLES, CompositeKind, compositeKindOfRole,
4
+ AccordionRole, COMPOSITE_KIND_ROLES, CompositeKind, compositeKindOfRole, isToggleKind,
5
5
  } from './composite-kinds';
6
6
  import {
7
7
  CompositeNode, CompositeNodeFactory, findByRole, makeCompositePart, readProp, removeFromComposite, walkComposite, writeProp, writeStyles,
@@ -233,7 +233,8 @@ export function setAccordionBehaviour(root: CompositeNode, patch: Partial<Accord
233
233
 
234
234
  /** The roles of the kind an item belongs to (an accordion item, or a disclosure — its own item). */
235
235
  function kindRolesOf(node: CompositeNode) {
236
- return COMPOSITE_KIND_ROLES[compositeKindOfRole(node.role) ?? CompositeKind.Accordion];
236
+ const kind = compositeKindOfRole(node.role);
237
+ return COMPOSITE_KIND_ROLES[isToggleKind(kind) ? kind : CompositeKind.Accordion];
237
238
  }
238
239
 
239
240
  /** The header of an item (either kind). */
@@ -479,6 +480,7 @@ export interface AccordionDomIds {
479
480
  const DOM_ID_PREFIX: Readonly<Record<CompositeKind, string>> = {
480
481
  [CompositeKind.Accordion]: 'iox-acc',
481
482
  [CompositeKind.Disclosure]: 'iox-dis',
483
+ [CompositeKind.Tabs]: 'iox-tab',
482
484
  };
483
485
 
484
486
  /** The ARIA pairing ids of an item: unique per composite (its SCOPE id — `compositeScopeId`) and per item (its key). */
@@ -1,15 +1,23 @@
1
1
  /**
2
- * The open/close composites' kinds and roles (CC2 Accordion, CC2.1 Disclosure — composite plan T9 / T21). ONE
3
- * family: a Disclosure is a one-item accordion whose ROOT is its item, so both run the same state machine, the
4
- * same attribute planners and the same runtime (`accordion-runtime.ts`). Every consumer that treats "an item",
5
- * "a header" or "a panel" alike asks these predicates, never a role literal.
2
+ * The composites' kinds and roles. Two families:
3
+ * - OPEN/CLOSE (CC2 Accordion, CC2.1 Disclosure — composite plan T9 / T21): a Disclosure is a one-item accordion whose
4
+ * ROOT is its item, so both run the same state machine, attribute planners and runtime (`accordion-runtime.ts`);
5
+ * - EXCLUSIVE (CC4 Tabs — composite T17; the Segmented control joins in CC4b): exactly one item is on, a trigger and
6
+ * its panel paired by `itemKey` (`tabs.ts`, `exclusive-runtime.ts`).
7
+ * Every consumer that treats "an item", "a trigger" or "a panel" alike asks these predicates, never a role literal.
6
8
  */
7
9
 
8
10
  export enum CompositeKind {
9
11
  Accordion = 'accordion',
10
12
  Disclosure = 'disclosure',
13
+ Tabs = 'tabs',
11
14
  }
12
15
 
16
+ /** The kinds the open/close runtime runs (`AccordionRuntime`). */
17
+ export type ToggleCompositeKind = CompositeKind.Accordion | CompositeKind.Disclosure;
18
+ /** The kinds the exclusive runtime runs (`ExclusiveRuntime`): one item on at a time, always one. */
19
+ export type ExclusiveCompositeKind = CompositeKind.Tabs;
20
+
13
21
  export enum AccordionRole {
14
22
  Accordion = 'accordion',
15
23
  Item = 'accordion.item',
@@ -32,9 +40,22 @@ export enum DisclosureRole {
32
40
  Panel = 'disclosure.panel',
33
41
  }
34
42
 
43
+ /**
44
+ * TABS (CC4a — composite T17). The tab LIST holds the triggers, the PANELS holder holds the panels; a tab and its panel
45
+ * share an `itemKey` (never their index — moving a tab keeps its own panel). From content (R3) the list and the panels
46
+ * holder are both Repeaters over the SAME source, one row giving its tab AND its panel (paired by the row's id).
47
+ */
48
+ export enum TabsRole {
49
+ Tabs = 'tabs',
50
+ List = 'tabs.list',
51
+ Trigger = 'tabs.trigger',
52
+ Panels = 'tabs.panels',
53
+ Panel = 'tabs.panel',
54
+ }
55
+
35
56
  interface RoleLike { role?: string | null }
36
57
 
37
- /** The roles each kind plays in the shared runtime. */
58
+ /** The roles each open/close kind plays in the shared runtime. */
38
59
  export interface CompositeKindRoles {
39
60
  root: string;
40
61
  item: string;
@@ -43,17 +64,40 @@ export interface CompositeKindRoles {
43
64
  icon: string;
44
65
  }
45
66
 
46
- export const COMPOSITE_KIND_ROLES: Readonly<Record<CompositeKind, CompositeKindRoles>> = {
67
+ export const COMPOSITE_KIND_ROLES: Readonly<Record<ToggleCompositeKind, CompositeKindRoles>> = {
47
68
  [CompositeKind.Accordion]: { root: AccordionRole.Accordion, item: AccordionRole.Item, trigger: AccordionRole.Trigger, panel: AccordionRole.Panel, icon: AccordionRole.Icon },
48
69
  [CompositeKind.Disclosure]: { root: DisclosureRole.Disclosure, item: DisclosureRole.Disclosure, trigger: DisclosureRole.Trigger, panel: DisclosureRole.Panel, icon: DisclosureRole.Icon },
49
70
  };
50
71
 
72
+ /** The roles each EXCLUSIVE kind plays: its root, the holder of its triggers, its triggers, and — when its items show
73
+ * a panel (Tabs; a Segmented control has none) — the holder of its panels and the panels. */
74
+ export interface ExclusiveKindRoles {
75
+ root: string;
76
+ list: string;
77
+ trigger: string;
78
+ panels: string | null;
79
+ panel: string | null;
80
+ }
81
+
82
+ export const EXCLUSIVE_KIND_ROLES: Readonly<Record<ExclusiveCompositeKind, ExclusiveKindRoles>> = {
83
+ [CompositeKind.Tabs]: { root: TabsRole.Tabs, list: TabsRole.List, trigger: TabsRole.Trigger, panels: TabsRole.Panels, panel: TabsRole.Panel },
84
+ };
85
+
51
86
  const KINDS = Object.values(CompositeKind);
52
- const rolesOf = (pick: keyof CompositeKindRoles): ReadonlySet<string> => new Set(KINDS.map(k => COMPOSITE_KIND_ROLES[k][pick]));
87
+ const TOGGLE_KINDS = Object.keys(COMPOSITE_KIND_ROLES) as ToggleCompositeKind[];
88
+ const EXCLUSIVE_KINDS = Object.keys(EXCLUSIVE_KIND_ROLES) as ExclusiveCompositeKind[];
89
+ const rolesOf = (pick: keyof CompositeKindRoles): ReadonlySet<string> => new Set(TOGGLE_KINDS.map(k => COMPOSITE_KIND_ROLES[k][pick]));
90
+ const exclusiveRolesOf = (pick: keyof ExclusiveKindRoles): ReadonlySet<string> =>
91
+ new Set(EXCLUSIVE_KINDS.map(k => EXCLUSIVE_KIND_ROLES[k][pick]).filter((r): r is string => !!r));
53
92
  const ROOT_ROLES = rolesOf('root');
54
93
  const ITEM_ROLES = rolesOf('item');
55
94
  const TRIGGER_ROLES = rolesOf('trigger');
56
95
  const PANEL_ROLES = rolesOf('panel');
96
+ const EXCLUSIVE_ROOT_ROLES = exclusiveRolesOf('root');
97
+ const EXCLUSIVE_LIST_ROLES = exclusiveRolesOf('list');
98
+ const EXCLUSIVE_TRIGGER_ROLES = exclusiveRolesOf('trigger');
99
+ const EXCLUSIVE_PANELS_ROLES = exclusiveRolesOf('panels');
100
+ const EXCLUSIVE_PANEL_ROLES = exclusiveRolesOf('panel');
57
101
 
58
102
  /** The kind a role belongs to (`disclosure.panel` → Disclosure), or null for any other role. */
59
103
  export function compositeKindOfRole(role: string | null | undefined): CompositeKind | null {
@@ -61,6 +105,16 @@ export function compositeKindOfRole(role: string | null | undefined): CompositeK
61
105
  return (KINDS as string[]).includes(head ?? '') ? head as CompositeKind : null;
62
106
  }
63
107
 
108
+ /** True for a kind the open/close runtime runs. */
109
+ export function isToggleKind(kind: CompositeKind | null | undefined): kind is ToggleCompositeKind {
110
+ return !!kind && (TOGGLE_KINDS as string[]).includes(kind);
111
+ }
112
+
113
+ /** True for a kind the exclusive runtime runs. */
114
+ export function isExclusiveKind(kind: CompositeKind | null | undefined): kind is ExclusiveCompositeKind {
115
+ return !!kind && (EXCLUSIVE_KINDS as string[]).includes(kind);
116
+ }
117
+
64
118
  /** A root that runs the open/close runtime (an Accordion or a Disclosure). */
65
119
  export function isCompositeRoot(node: RoleLike | null | undefined): boolean {
66
120
  return !!node?.role && ROOT_ROLES.has(node.role);
@@ -108,3 +162,63 @@ export function compositeItemOf<T extends RoleLike>(node: T, path: ReadonlyArray
108
162
  }
109
163
  return null;
110
164
  }
165
+
166
+ // ── the exclusive family (CC4a Tabs) ───────────────────────────────────────────────────────────
167
+
168
+ /** A root that runs the exclusive runtime (Tabs). */
169
+ export function isExclusiveRoot(node: RoleLike | null | undefined): boolean {
170
+ return !!node?.role && EXCLUSIVE_ROOT_ROLES.has(node.role);
171
+ }
172
+
173
+ /** The holder of an exclusive composite's triggers (a tab list): it takes only triggers. */
174
+ export function isExclusiveList(node: RoleLike | null | undefined): boolean {
175
+ return !!node?.role && EXCLUSIVE_LIST_ROLES.has(node.role);
176
+ }
177
+
178
+ /** One of an exclusive composite's triggers (a tab): pressing it turns its item on. */
179
+ export function isExclusiveTrigger(node: RoleLike | null | undefined): boolean {
180
+ return !!node?.role && EXCLUSIVE_TRIGGER_ROLES.has(node.role);
181
+ }
182
+
183
+ /** The holder of an exclusive composite's panels: it takes only panels. */
184
+ export function isExclusivePanels(node: RoleLike | null | undefined): boolean {
185
+ return !!node?.role && EXCLUSIVE_PANELS_ROLES.has(node.role);
186
+ }
187
+
188
+ /** One of an exclusive composite's panels (a tab panel): shown only while its item is on. */
189
+ export function isExclusivePanel(node: RoleLike | null | undefined): boolean {
190
+ return !!node?.role && EXCLUSIVE_PANEL_ROLES.has(node.role);
191
+ }
192
+
193
+ /** True for a trigger or a panel — a part that IS one item of an exclusive composite (it carries the `itemKey`). */
194
+ export function isExclusiveItemPart(node: RoleLike | null | undefined): boolean {
195
+ return isExclusiveTrigger(node) || isExclusivePanel(node);
196
+ }
197
+
198
+ /** True for any part of a Tabs (its root included). */
199
+ export function isTabsPart(node: RoleLike | null | undefined): boolean {
200
+ return compositeKindOfRole(node?.role) === CompositeKind.Tabs;
201
+ }
202
+
203
+ /** A root of either family — anything that runs a composite runtime. */
204
+ export function isAnyCompositeRoot(node: RoleLike | null | undefined): boolean {
205
+ return isCompositeRoot(node) || isExclusiveRoot(node);
206
+ }
207
+
208
+ /** The exclusive root a node belongs to (the node itself when it is one), or null. `path` = its ancestors, root first. */
209
+ export function exclusiveRootOf<T extends RoleLike>(node: T, path: ReadonlyArray<T>): T | null {
210
+ if (isExclusiveRoot(node)) return node;
211
+ for (let i = path.length - 1; i >= 0; i--) if (isExclusiveRoot(path[i])) return path[i];
212
+ return null;
213
+ }
214
+
215
+ /** The trigger or panel a node sits in (itself when it is one) inside an exclusive composite, or null. A nested
216
+ * exclusive root ends the walk. */
217
+ export function exclusiveItemPartOf<T extends RoleLike>(node: T, path: ReadonlyArray<T>): T | null {
218
+ if (isExclusiveItemPart(node)) return node;
219
+ for (let i = path.length - 1; i >= 0; i--) {
220
+ if (isExclusiveItemPart(path[i])) return path[i];
221
+ if (isExclusiveRoot(path[i])) return null;
222
+ }
223
+ return null;
224
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ accordionRootOfItem, accordionRuntimeOf, AccordionRuntime, AccordionRuntimeOptions, attachAccordionAt, closestAccordionItem, CompositeItemAction,
3
+ isAccordionItemOn, onAccordionItemState,
4
+ } from './accordion-runtime';
5
+ import { COMPOSITE_ATTRIBUTE } from './accordion';
6
+ import { CompositeKind, isExclusiveKind } from './composite-kinds';
7
+ import {
8
+ attachExclusiveAt, closestExclusive, closestExclusiveItemPart, ExclusiveRuntime, exclusiveRuntimeOf, ExclusiveRuntimeOptions, isExclusiveItemOn,
9
+ onExclusiveItemState,
10
+ } from './exclusive-runtime';
11
+
12
+ /**
13
+ * The composites' runtimes behind ONE door (CC4a): the open/close runtime (Accordion, Disclosure) and the exclusive
14
+ * runtime (Tabs; the Segmented control in CC4b). The engine attaches whichever a rendered root needs; the interaction
15
+ * engines (builder canvas and live site) ask these for `itemOn` / `itemOff` and run the item actions through them —
16
+ * never a family's own function, so a new kind joins without touching either engine.
17
+ */
18
+
19
+ export type CompositeRuntime = AccordionRuntime | ExclusiveRuntime;
20
+
21
+ /** Attach the runtime a rendered composite root needs (its `data-iox-composite`), unless it runs one already. Returns
22
+ * the NEW runtime, or null. */
23
+ export function attachCompositeAt(root: HTMLElement, options: Omit<AccordionRuntimeOptions, 'rootId' | 'behaviour'> & Omit<ExclusiveRuntimeOptions, 'behaviour'> = {}): CompositeRuntime | null {
24
+ const kind = root.getAttribute(COMPOSITE_ATTRIBUTE) as CompositeKind | null;
25
+ if (isExclusiveKind(kind)) return attachExclusiveAt(root, { observe: options.observe });
26
+ return attachAccordionAt(root, { observe: options.observe, reducedMotion: options.reducedMotion });
27
+ }
28
+
29
+ /** The nearer of the item elements `el` sits in — an accordion item / a disclosure, or a tab / a tab panel. */
30
+ function nearestItem(el: Element | null | undefined): { el: HTMLElement; exclusive: boolean } | null {
31
+ const toggle = closestAccordionItem(el);
32
+ const exclusive = closestExclusiveItemPart(el);
33
+ if (!toggle && !exclusive) return null;
34
+ if (toggle && (!exclusive || exclusive.contains(toggle))) return { el: toggle, exclusive: false };
35
+ return { el: exclusive!, exclusive: true };
36
+ }
37
+
38
+ /** Subscribe to the item `el` sits in turning on / off — what the `itemOn` / `itemOff` interaction triggers listen to,
39
+ * in both families. Returns the unsubscribe. */
40
+ export function onCompositeItemState(el: Element, on: boolean, handler: () => void): () => void {
41
+ const item = nearestItem(el);
42
+ if (!item) return () => undefined;
43
+ return item.exclusive ? onExclusiveItemState(item.el, on, handler) : onAccordionItemState(item.el, on, handler);
44
+ }
45
+
46
+ /** True when the item `el` sits in is on now (an open accordion item, the active tab or its panel). */
47
+ export function isCompositeItemOn(el: Element): boolean {
48
+ const item = nearestItem(el);
49
+ if (!item) return false;
50
+ return item.exclusive ? isExclusiveItemOn(item.el) : isAccordionItemOn(item.el);
51
+ }
52
+
53
+ /**
54
+ * Run an item action on the element of an ITEM — an accordion item, a disclosure, or a tab (CC4a: `openItem` turns it
55
+ * on; a Tabs always shows one, so Close and Toggle leave it) — the `openItem` / `closeItem` / `toggleItem` interaction
56
+ * actions, on the canvas and on the live site through this one function. Returns whether the item shows on
57
+ * afterwards, or null when `el` is in no running composite.
58
+ */
59
+ export function runCompositeItemAction(el: Element | null | undefined, action: CompositeItemAction): boolean | null {
60
+ const item = nearestItem(el);
61
+ if (!item) return null;
62
+ if (item.exclusive) {
63
+ const runtime = exclusiveRuntimeOf(closestExclusive(item.el));
64
+ return runtime ? runtime.run(action === CompositeItemAction.Open, runtime.keyOf(item.el)) : null;
65
+ }
66
+ const runtime = accordionRuntimeOf(accordionRootOfItem(item.el));
67
+ return runtime ? runtime.run(action, runtime.keyOf(item.el)) : null;
68
+ }