@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.
- package/composite-core/index.d.ts +392 -16
- package/design-core/index.d.ts +322 -3
- package/fesm2022/vectoriox-iox-ui-actions-core.mjs +4 -2
- package/fesm2022/vectoriox-iox-ui-actions-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs +1129 -146
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +383 -3
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs +219 -8
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +16 -9
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/generation-core/index.d.ts +107 -3
- package/index.d.ts +11 -6
- package/package.json +1 -1
- package/src/actions-core/element-actions.ts +4 -2
- package/src/composite-core/accordion-runtime.ts +3 -15
- package/src/composite-core/accordion.ts +4 -2
- package/src/composite-core/composite-kinds.ts +121 -7
- package/src/composite-core/composite-runtime.ts +68 -0
- package/src/composite-core/exclusive-runtime.ts +358 -0
- package/src/composite-core/public-api.ts +5 -2
- package/src/composite-core/structure.ts +22 -2
- package/src/composite-core/tabs.ts +628 -0
- package/src/design-core/contrast-audit.ts +5 -0
- package/src/design-core/element-styles.ts +7 -3
- package/src/design-core/flavor-spec.generated.ts +371 -0
- package/src/generation-core/compose-site.ts +147 -1
- package/src/generation-core/nav-shape.ts +53 -3
- package/src/generation-core/node-baselines.generated.ts +8 -2
- package/src/generation-core/patterns.ts +3 -0
- package/src/generation-core/site-spec.generated.ts +46 -0
- 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 =
|
|
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,
|
|
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",
|
package/styles/iox-design.css
CHANGED
|
@@ -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
|