@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
@@ -67,6 +67,8 @@ export interface NavShapeOptions {
67
67
  barItems?: number;
68
68
  /** A new part's id (the builder passes its id generator). Default: `<nav id>-<suffix>`. */
69
69
  newId?: (suffix: string) => string;
70
+ /** The studio's pages, for a header that links to none (a hand-built one): its menu lists these. */
71
+ pages?: ReadonlyArray<NavPage>;
70
72
  }
71
73
 
72
74
  export interface NavShapeResult {
@@ -199,6 +201,21 @@ export function navPages(root: SerializedNode): NavPage[] {
199
201
  return source.filter(isPageLink).map(n => ({ label: textOf(n), route: String(prop(n, 'pageRoute')) }));
200
202
  }
201
203
 
204
+ /** Where a phone menu's pages come from: the header's own page Links, else the studio's pages. */
205
+ export enum NavPagesSource {
206
+ Header = 'header',
207
+ Site = 'site',
208
+ None = 'none',
209
+ }
210
+
211
+ /** The pages a phone menu lists: the header's (`navPages`), else `sitePages` — a hand-built header with no
212
+ * page links still gets a working menu. */
213
+ export function phoneMenuPages(root: SerializedNode, sitePages: ReadonlyArray<NavPage> = []): { pages: NavPage[]; source: NavPagesSource } {
214
+ const own = navPages(root);
215
+ if (own.length) return { pages: own, source: NavPagesSource.Header };
216
+ return sitePages.length ? { pages: [...sitePages], source: NavPagesSource.Site } : { pages: [], source: NavPagesSource.None };
217
+ }
218
+
202
219
  /** A hand-built header: every page Link outside a drawer. */
203
220
  function collectHeaderLinks(root: SerializedNode): SerializedNode[] {
204
221
  const out: SerializedNode[] = [];
@@ -439,7 +456,7 @@ export function planNavShape(globals: ReadonlyArray<SerializedNode>, shape: NavS
439
456
  const kit = makeKit(out, root.id, options.newId);
440
457
 
441
458
  adoptRoles(root);
442
- const pages = navPages(root);
459
+ const { pages } = phoneMenuPages(root, options.pages);
443
460
  const wantDrawer = shape !== NavShape.Bar;
444
461
  const wantBar = shape !== NavShape.Drawer;
445
462
  const wantMenuButton = shape === NavShape.Drawer;
@@ -538,7 +555,7 @@ export interface AddBarItemResult {
538
555
  * The bar's Items list "Add item" (board 2): one more item, before More — the next navigation page the
539
556
  * bar does not link to yet (Home when every page is there). Refused at five (Q2). Input not mutated.
540
557
  */
541
- export function addBarItem(globals: ReadonlyArray<SerializedNode>, options: Pick<NavShapeOptions, 'newId'> = {}): AddBarItemResult {
558
+ export function addBarItem(globals: ReadonlyArray<SerializedNode>, options: Pick<NavShapeOptions, 'newId' | 'pages'> = {}): AddBarItemResult {
542
559
  const out = clone(globals) as SerializedNode[];
543
560
  const root = findNavRoot(out);
544
561
  const b = find(out, NavRole.Bar);
@@ -546,10 +563,43 @@ export function addBarItem(globals: ReadonlyArray<SerializedNode>, options: Pick
546
563
  const kids = b.children ?? (b.children = []);
547
564
  if (kids.length >= BAR_ITEMS_MAX) return { globals: out, added: false };
548
565
  const linked = new Set(kids.filter(c => c.role === NavRole.BarItem).map(c => prop(c, 'pageRoute')));
549
- const page = barPages(root, navPages(root), BAR_ITEMS_MAX + 1).find(p => !linked.has(p.route))
566
+ const page = barPages(root, phoneMenuPages(root, options.pages).pages, BAR_ITEMS_MAX + 1).find(p => !linked.has(p.route))
550
567
  ?? { label: NAV_SHAPE_COPY.home, route: HOME_ROUTE };
551
568
  const item = barItem(makeKit(out, root.id, options.newId), kids.length, page);
552
569
  const more = kids.findIndex(c => c.role === NavRole.More);
553
570
  if (more >= 0) kids.splice(more, 0, item); else kids.push(item);
554
571
  return { globals: out, added: true };
555
572
  }
573
+
574
+ export interface BarItemsEditResult {
575
+ globals: SerializedNode[];
576
+ /** False when nothing changed: no bar, an unknown item, or the bar would drop below three items (Q2). */
577
+ changed: boolean;
578
+ }
579
+
580
+ function barOf(globals: ReadonlyArray<SerializedNode>): { out: SerializedNode[]; kids: SerializedNode[] | null } {
581
+ const out = clone(globals) as SerializedNode[];
582
+ const b = find(out, NavRole.Bar);
583
+ return { out, kids: b ? (b.children ?? (b.children = [])) : null };
584
+ }
585
+
586
+ /**
587
+ * The bar's Items list "Remove" (board 2): the item goes, More included — refused below three items (Q2:
588
+ * 3–5), so the bar never shrinks into something a thumb cannot use. Input not mutated.
589
+ */
590
+ export function removeBarItem(globals: ReadonlyArray<SerializedNode>, itemId: string): BarItemsEditResult {
591
+ const { out, kids } = barOf(globals);
592
+ const i = kids ? kids.findIndex(c => c.id === itemId) : -1;
593
+ if (!kids || i < 0 || kids.length <= BAR_ITEMS_MIN) return { globals: out, changed: false };
594
+ kids.splice(i, 1);
595
+ return { globals: out, changed: true };
596
+ }
597
+
598
+ /** The bar's Items list drag-to-reorder (board 2): item `from` moves to `to`. Input not mutated. */
599
+ export function moveBarItem(globals: ReadonlyArray<SerializedNode>, from: number, to: number): BarItemsEditResult {
600
+ const { out, kids } = barOf(globals);
601
+ if (!kids || from === to || from < 0 || to < 0 || from >= kids.length || to >= kids.length) return { globals: out, changed: false };
602
+ const [item] = kids.splice(from, 1);
603
+ kids.splice(to, 0, item);
604
+ return { globals: out, changed: true };
605
+ }
@@ -667,7 +667,10 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
667
667
  "htmlTag",
668
668
  "colorScheme",
669
669
  "multiple",
670
- "defaultOn"
670
+ "defaultOn",
671
+ "activateOn",
672
+ "orientation",
673
+ "ariaLabel"
671
674
  ],
672
675
  "styleTraits": [
673
676
  "position",
@@ -2900,7 +2903,10 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
2900
2903
  "source",
2901
2904
  "limit",
2902
2905
  "multiple",
2903
- "defaultOn"
2906
+ "defaultOn",
2907
+ "activateOn",
2908
+ "orientation",
2909
+ "ariaLabel"
2904
2910
  ],
2905
2911
  "styleTraits": [
2906
2912
  "position",
@@ -67,6 +67,9 @@ export enum PatternRecipe {
67
67
  /** CC2: the Accordion — Manual (three placeholder items), or From content when the section names a list
68
68
  * `binding` (`src: '$binding?'`): one template item bound to the type's question / answer fields. */
69
69
  Accordion = 'accordion',
70
+ /** CC4a: Tabs — Manual (three placeholder tabs), or From content when the section names a list `binding`: the tab
71
+ * list and the panels are both Repeaters over it, one row giving its tab AND its panel. */
72
+ Tabs = 'tabs',
70
73
  /** CC3: the accessible carousel — a named Slider whose controls are parts, one quote per slide (placeholders). */
71
74
  Carousel = 'carousel',
72
75
  /** CC3: the Lightbox — a Dialog holding a Slider over the node's `src` (the gallery's own source). */
@@ -979,6 +979,52 @@ export const SPEC_SITE_PATTERNS = {
979
979
  ]
980
980
  }
981
981
  },
982
+ "feature.tabbed": {
983
+ "family": "feature",
984
+ "label": "Features — tabs that switch",
985
+ "node": {
986
+ "t": "Section",
987
+ "es": "section",
988
+ "c": [
989
+ {
990
+ "t": "Container",
991
+ "es": "box",
992
+ "lay": {
993
+ "display": "flex",
994
+ "flexDirection": "column",
995
+ "gap": "14px",
996
+ "maxWidth": "1100px",
997
+ "width": "100%",
998
+ "margin": "0px auto 0px auto"
999
+ },
1000
+ "c": [
1001
+ {
1002
+ "t": "Text",
1003
+ "es": "eyebrow",
1004
+ "slot": "eyebrow",
1005
+ "ph": "copy",
1006
+ "text": "What we offer"
1007
+ },
1008
+ {
1009
+ "t": "Heading",
1010
+ "es": "h2",
1011
+ "level": "h2",
1012
+ "slot": "title",
1013
+ "ph": "copy",
1014
+ "text": "What we design"
1015
+ },
1016
+ {
1017
+ "t": "Container",
1018
+ "recipe": "tabs",
1019
+ "src": "$binding?",
1020
+ "ph": "copy",
1021
+ "name": "What we design"
1022
+ }
1023
+ ]
1024
+ }
1025
+ ]
1026
+ }
1027
+ },
982
1028
  "hero.full-bleed-media": {
983
1029
  "family": "hero",
984
1030
  "label": "Hero — words over a background video",
@@ -238,6 +238,22 @@ dialog[data-iox-dialog]:not([open]) {
238
238
  .iox-accordion-track { transition: none; }
239
239
  }
240
240
 
241
+ /* ── CC4a: Tabs (complex-components boards 3 and 9; composite K2) ────────────────────────────────────
242
+ The renderers write ONE markup (composite-core tabs.ts): the list is role=tablist, each tab a <button role=tab>, each
243
+ panel role=tabpanel. Structural only — the look is the parts' element styles (tab-list, tab), on their own rules. */
244
+
245
+ /* A tab is a real button: its UA look is reset at zero specificity, so the node's own rule (the `tab` style) wins. */
246
+ :where(.iox-role-tabs-trigger) {
247
+ appearance: none; -webkit-appearance: none; background: none; border: 0 solid transparent; border-radius: 0; margin: 0;
248
+ font: inherit; color: inherit; cursor: pointer; box-sizing: border-box;
249
+ }
250
+ /* The ONE hiding rule for Tabs (board 9: "Tabs keep display:none"): a panel that is not on takes no space and leaves the
251
+ Tab order. `!important` because a panel is any Container: the author's display (its own rule, a breakpoint layer, a
252
+ state — each at (0,1,0) or (0,2,0), emitted after this sheet) must never bring an inactive panel back. */
253
+ .iox-role-tabs-panel[hidden] { display: none !important; }
254
+ /* A panel is a Tab stop of its own (APG): its focus ring is the look's. */
255
+ .iox-role-tabs-panel:focus-visible { outline: none; box-shadow: var(--iox-focus-ring); }
256
+
241
257
  /* ── CC3: Video (complex-components plan §7b, board 5) ────────────────────────────────────────────────
242
258
  One markup from media-core `videoInnerHtml` (both engine paths, the canvas). The frame is the node (it wears
243
259
  `media`: radius, fit); the poster, the play button, the provider badge and the consent prompt are drawn by the