@vectoriox/iox-ui 5.36.0 → 5.37.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/actions-core/index.d.ts +12 -3
- package/composite-core/index.d.ts +180 -28
- package/fesm2022/vectoriox-iox-ui-actions-core.mjs +26 -2
- package/fesm2022/vectoriox-iox-ui-actions-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs +397 -91
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +4 -2
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/index.d.ts +3 -1
- package/package.json +1 -1
- package/src/actions-core/action.model.ts +4 -2
- package/src/actions-core/element-actions.ts +29 -1
- package/src/composite-core/accordion-runtime.ts +117 -28
- package/src/composite-core/accordion.ts +89 -45
- package/src/composite-core/composite-kinds.ts +110 -0
- package/src/composite-core/composite-node.ts +11 -0
- package/src/composite-core/disclosure.ts +125 -0
- package/src/composite-core/public-api.ts +4 -1
- package/src/composite-core/structure.ts +8 -1
|
@@ -0,0 +1,110 @@
|
|
|
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.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export enum CompositeKind {
|
|
9
|
+
Accordion = 'accordion',
|
|
10
|
+
Disclosure = 'disclosure',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export enum AccordionRole {
|
|
14
|
+
Accordion = 'accordion',
|
|
15
|
+
Item = 'accordion.item',
|
|
16
|
+
Trigger = 'accordion.trigger',
|
|
17
|
+
Icon = 'accordion.icon',
|
|
18
|
+
Panel = 'accordion.panel',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* DISCLOSURE ("Read more") — one trigger, one panel. The root IS its item (it carries the item key and
|
|
23
|
+
* `data-iox-on`); the trigger holds two words, the one shown while closed (`more`) and the one shown while open
|
|
24
|
+
* (`less`), swapped by the Item open state — never by script.
|
|
25
|
+
*/
|
|
26
|
+
export enum DisclosureRole {
|
|
27
|
+
Disclosure = 'disclosure',
|
|
28
|
+
Trigger = 'disclosure.trigger',
|
|
29
|
+
More = 'disclosure.more',
|
|
30
|
+
Less = 'disclosure.less',
|
|
31
|
+
Icon = 'disclosure.icon',
|
|
32
|
+
Panel = 'disclosure.panel',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface RoleLike { role?: string | null }
|
|
36
|
+
|
|
37
|
+
/** The roles each kind plays in the shared runtime. */
|
|
38
|
+
export interface CompositeKindRoles {
|
|
39
|
+
root: string;
|
|
40
|
+
item: string;
|
|
41
|
+
trigger: string;
|
|
42
|
+
panel: string;
|
|
43
|
+
icon: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const COMPOSITE_KIND_ROLES: Readonly<Record<CompositeKind, CompositeKindRoles>> = {
|
|
47
|
+
[CompositeKind.Accordion]: { root: AccordionRole.Accordion, item: AccordionRole.Item, trigger: AccordionRole.Trigger, panel: AccordionRole.Panel, icon: AccordionRole.Icon },
|
|
48
|
+
[CompositeKind.Disclosure]: { root: DisclosureRole.Disclosure, item: DisclosureRole.Disclosure, trigger: DisclosureRole.Trigger, panel: DisclosureRole.Panel, icon: DisclosureRole.Icon },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const KINDS = Object.values(CompositeKind);
|
|
52
|
+
const rolesOf = (pick: keyof CompositeKindRoles): ReadonlySet<string> => new Set(KINDS.map(k => COMPOSITE_KIND_ROLES[k][pick]));
|
|
53
|
+
const ROOT_ROLES = rolesOf('root');
|
|
54
|
+
const ITEM_ROLES = rolesOf('item');
|
|
55
|
+
const TRIGGER_ROLES = rolesOf('trigger');
|
|
56
|
+
const PANEL_ROLES = rolesOf('panel');
|
|
57
|
+
|
|
58
|
+
/** The kind a role belongs to (`disclosure.panel` → Disclosure), or null for any other role. */
|
|
59
|
+
export function compositeKindOfRole(role: string | null | undefined): CompositeKind | null {
|
|
60
|
+
const head = role?.split('.')[0];
|
|
61
|
+
return (KINDS as string[]).includes(head ?? '') ? head as CompositeKind : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A root that runs the open/close runtime (an Accordion or a Disclosure). */
|
|
65
|
+
export function isCompositeRoot(node: RoleLike | null | undefined): boolean {
|
|
66
|
+
return !!node?.role && ROOT_ROLES.has(node.role);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** An ITEM — an accordion's item, or a disclosure (its own item). */
|
|
70
|
+
export function isCompositeItem(node: RoleLike | null | undefined): boolean {
|
|
71
|
+
return !!node?.role && ITEM_ROLES.has(node.role);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A trigger — rendered as a `<button>` that opens its item, never a link. */
|
|
75
|
+
export function isCompositeTrigger(node: RoleLike | null | undefined): boolean {
|
|
76
|
+
return !!node?.role && TRIGGER_ROLES.has(node.role);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A panel — rendered inside the track + region the one hiding rule collapses. */
|
|
80
|
+
export function isCompositePanel(node: RoleLike | null | undefined): boolean {
|
|
81
|
+
return !!node?.role && PANEL_ROLES.has(node.role);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isDisclosureRoot(node: RoleLike | null | undefined): boolean {
|
|
85
|
+
return node?.role === DisclosureRole.Disclosure;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** True for any part of a disclosure (its root included). */
|
|
89
|
+
export function isDisclosurePart(node: RoleLike | null | undefined): boolean {
|
|
90
|
+
return compositeKindOfRole(node?.role) === CompositeKind.Disclosure;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The composite root a node belongs to: the nearest ancestor (or the node itself) that is one. `path` = the
|
|
94
|
+
* node's ancestors, root first. */
|
|
95
|
+
export function compositeRootOf<T extends RoleLike>(node: T, path: ReadonlyArray<T>): T | null {
|
|
96
|
+
if (isCompositeRoot(node)) return node;
|
|
97
|
+
for (let i = path.length - 1; i >= 0; i--) if (isCompositeRoot(path[i])) return path[i];
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The item a node sits in — the nearest item of either kind (itself when it is one) — or null. An accordion
|
|
102
|
+
* root ends the walk (its items are below it). */
|
|
103
|
+
export function compositeItemOf<T extends RoleLike>(node: T, path: ReadonlyArray<T>): T | null {
|
|
104
|
+
if (isCompositeItem(node)) return node;
|
|
105
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
106
|
+
if (isCompositeItem(path[i])) return path[i];
|
|
107
|
+
if (isCompositeRoot(path[i])) return null;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
@@ -46,6 +46,17 @@ export interface CompositeNodeSpec {
|
|
|
46
46
|
|
|
47
47
|
export type CompositeNodeFactory = (spec: CompositeNodeSpec) => CompositeNode;
|
|
48
48
|
|
|
49
|
+
/** A recipe part: a NEW node (`create`) playing `role` under `label`, with the layout `base` styles its recipe
|
|
50
|
+
* gives it — its LOOK is its element style. Shared by every recipe (Accordion, Disclosure). */
|
|
51
|
+
export function makeCompositePart(create: CompositeNodeFactory, spec: CompositeNodeSpec, role: string | null, label: string | undefined,
|
|
52
|
+
base?: Readonly<Record<string, string>>): CompositeNode {
|
|
53
|
+
const node = create(spec);
|
|
54
|
+
if (role) node.role = role;
|
|
55
|
+
if (label) node.label = label;
|
|
56
|
+
if (base) writeStyles(node, base);
|
|
57
|
+
return node;
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
const usesTraits = (node: CompositeNode): boolean => Array.isArray(node.traits);
|
|
50
61
|
|
|
51
62
|
/** A content trait's value — `traits[].default` on a builder node, `props[name]` on a saved one. */
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { accordionBehaviour, AccordionDefaultOn, AccordionIcon, buildCompositeIcon, CompositeBehaviourTrait, ITEM_OPEN_STATE } from './accordion';
|
|
2
|
+
import { DisclosureRole } from './composite-kinds';
|
|
3
|
+
import { CompositeNode, CompositeNodeFactory, makeCompositePart, writeProp, writeStyles } from './composite-node';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DISCLOSURE / "Read more" (CC2.1 — composite T21). Not a component: a recipe of ordinary nodes on the
|
|
7
|
+
* ACCORDION's machinery — a disclosure is a one-item accordion whose root is its item, so it runs the same
|
|
8
|
+
* state machine, attribute planners and runtime (`accordion-runtime.ts`), on the canvas and on the live site.
|
|
9
|
+
*
|
|
10
|
+
* Container `box` role disclosure itemKey k1 · trait defaultOn (none | first)
|
|
11
|
+
* ├─ LinkedContainer `accordion-trigger` role disclosure.trigger → <button aria-expanded aria-controls>
|
|
12
|
+
* │ ├─ Text `body` (span) role disclosure.more "Read more" — hidden while open
|
|
13
|
+
* │ ├─ Text `body` (span) role disclosure.less "Read less" — shown only while open
|
|
14
|
+
* │ └─ Icon `icon` role disclosure.icon Item open → turns
|
|
15
|
+
* └─ Container `accordion-panel` role disclosure.panel inside a hidden="until-found" track
|
|
16
|
+
* └─ Text `body` the rest of the text
|
|
17
|
+
*
|
|
18
|
+
* The words swap through the ordinary Item open state (`trigger-on` → the root), so the label an author sees on
|
|
19
|
+
* the canvas is the one the visitor gets, with no script and nothing to break. The look is the accordion's own
|
|
20
|
+
* element styles (no `disclosure-*` contract: the boards draw none) — the header's ink, hover ink and focus ring;
|
|
21
|
+
* the panel's padding and muted ink — and the words are `body` text: the site's body type, with `color: inherit`, so
|
|
22
|
+
* the toggle's ink and its hover ink reach them.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface DisclosureWords {
|
|
26
|
+
more: string;
|
|
27
|
+
less: string;
|
|
28
|
+
body: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The words a fresh disclosure starts with — generic, the author's to replace. */
|
|
32
|
+
export const DISCLOSURE_WORDS: Readonly<DisclosureWords> = {
|
|
33
|
+
more: 'Read more',
|
|
34
|
+
less: 'Read less',
|
|
35
|
+
body: 'Write the rest of the story here. It stays out of the way until someone asks for it.',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export interface DisclosureOptions {
|
|
39
|
+
words?: Partial<DisclosureWords>;
|
|
40
|
+
/** Open on first render (default: closed). */
|
|
41
|
+
open?: boolean;
|
|
42
|
+
icon?: AccordionIcon;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A disclosure's one item key — its root IS its item. */
|
|
46
|
+
export const DISCLOSURE_ITEM_KEY = 'k1';
|
|
47
|
+
|
|
48
|
+
enum Type {
|
|
49
|
+
Container = 'Container',
|
|
50
|
+
LinkedContainer = 'LinkedContainer',
|
|
51
|
+
Text = 'Text',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
enum Style {
|
|
55
|
+
Root = 'box',
|
|
56
|
+
Trigger = 'accordion-trigger',
|
|
57
|
+
Word = 'body',
|
|
58
|
+
Panel = 'accordion-panel',
|
|
59
|
+
Body = 'body',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const ZERO_BOX = '0px 0px 0px 0px';
|
|
63
|
+
const HIDDEN = 'none';
|
|
64
|
+
const INHERIT = 'inherit';
|
|
65
|
+
/** A word is a flex item of the toggle — `block` (never `inline`: the compiler drops a value equal to the CSS initial,
|
|
66
|
+
* and the Text's own default would come back in its place). */
|
|
67
|
+
const SHOWN = 'block';
|
|
68
|
+
|
|
69
|
+
/** What each part holds — layout only; its LOOK is its element style. The trigger sizes to its words (a button in
|
|
70
|
+
* the flow, not a full-width header row — `fit-content` keeps it from stretching) and the panel spans the root. The
|
|
71
|
+
* trigger is `flex`, not `inline-flex`: it is a flex item of the root, which blockifies it on the site anyway. */
|
|
72
|
+
const PART_BASE: Readonly<Record<string, Readonly<Record<string, string>>>> = {
|
|
73
|
+
[DisclosureRole.Disclosure]: { display: 'flex', flexDirection: 'column', width: '100%', minHeight: 'auto', height: 'auto' },
|
|
74
|
+
// The toggle reads the text around it (the page's body type, through the root) — a LinkedContainer is born with
|
|
75
|
+
// its own literal font, which an accordion's heading masks and a disclosure's words would show.
|
|
76
|
+
[DisclosureRole.Trigger]: {
|
|
77
|
+
display: 'flex', alignItems: 'center', gap: '8px', width: 'fit-content', minHeight: 'auto', height: 'auto', textAlign: 'start', cursor: 'pointer',
|
|
78
|
+
fontFamily: INHERIT, fontSize: INHERIT, fontWeight: INHERIT, lineHeight: INHERIT,
|
|
79
|
+
},
|
|
80
|
+
// A word is just its words: none of a Text's own box (its type's 10px padding, its full width) — the toggle's
|
|
81
|
+
// padding is the click target.
|
|
82
|
+
[DisclosureRole.More]: { display: SHOWN, margin: ZERO_BOX, padding: ZERO_BOX, width: 'auto' },
|
|
83
|
+
[DisclosureRole.Less]: { display: HIDDEN, margin: ZERO_BOX, padding: ZERO_BOX, width: 'auto' },
|
|
84
|
+
[DisclosureRole.Panel]: { display: 'block', width: '100%', minHeight: 'auto', height: 'auto' },
|
|
85
|
+
};
|
|
86
|
+
const BODY_BASE: Readonly<Record<string, string>> = { margin: ZERO_BOX, padding: ZERO_BOX, minHeight: 'auto' };
|
|
87
|
+
|
|
88
|
+
function make(create: CompositeNodeFactory, key: string, type: string, role: DisclosureRole | null, style: string,
|
|
89
|
+
props: Record<string, unknown>, label: string): CompositeNode {
|
|
90
|
+
return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE[role] : undefined);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** One of the trigger's two words: shown while the disclosure is closed (`more`) or open (`less`). */
|
|
94
|
+
function buildWord(create: CompositeNodeFactory, role: DisclosureRole.More | DisclosureRole.Less, words: string, rootId?: string): CompositeNode {
|
|
95
|
+
const node = make(create, role === DisclosureRole.More ? 'more' : 'less', Type.Text, role, Style.Word, { content: words, tag: 'span' }, 'Text');
|
|
96
|
+
node.stateStyles = { ...(node.stateStyles ?? {}), [ITEM_OPEN_STATE]: { display: role === DisclosureRole.More ? HIDDEN : SHOWN } };
|
|
97
|
+
if (rootId) node.stateTriggers = { ...(node.stateTriggers ?? {}), [ITEM_OPEN_STATE]: rootId };
|
|
98
|
+
return node;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A fresh Disclosure: "Read more" opens the text below and reads "Read less" while it is open; closed by default. */
|
|
102
|
+
export function buildDisclosureRecipe(create: CompositeNodeFactory, options: DisclosureOptions = {}): CompositeNode {
|
|
103
|
+
const words = { ...DISCLOSURE_WORDS, ...(options.words ?? {}) };
|
|
104
|
+
const root = make(create, 'disclosure', Type.Container, DisclosureRole.Disclosure, Style.Root, {}, 'Disclosure');
|
|
105
|
+
root.itemKey = DISCLOSURE_ITEM_KEY;
|
|
106
|
+
writeProp(root, CompositeBehaviourTrait.DefaultOn, options.open ? AccordionDefaultOn.First : AccordionDefaultOn.None);
|
|
107
|
+
const icon = options.icon ?? AccordionIcon.Chevron;
|
|
108
|
+
const trigger = make(create, 'trigger', Type.LinkedContainer, DisclosureRole.Trigger, Style.Trigger, {}, 'Toggle');
|
|
109
|
+
trigger.children = [
|
|
110
|
+
buildWord(create, DisclosureRole.More, words.more, root.id),
|
|
111
|
+
buildWord(create, DisclosureRole.Less, words.less, root.id),
|
|
112
|
+
...(icon === AccordionIcon.None ? [] : [buildCompositeIcon(create, 'icon', icon, root.id, DisclosureRole.Icon)]),
|
|
113
|
+
];
|
|
114
|
+
const panel = make(create, 'panel', Type.Container, DisclosureRole.Panel, Style.Panel, {}, 'Panel');
|
|
115
|
+
const body = make(create, 'body', Type.Text, null, Style.Body, { content: words.body, tag: 'p' }, 'Text');
|
|
116
|
+
writeStyles(body, BODY_BASE);
|
|
117
|
+
panel.children = [body];
|
|
118
|
+
root.children = [trigger, panel];
|
|
119
|
+
return root;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Behaviour ▸ Open by default (a disclosure: on or off). */
|
|
123
|
+
export function disclosureOpenByDefault(root: CompositeNode): boolean {
|
|
124
|
+
return accordionBehaviour(root).defaultOn !== AccordionDefaultOn.None;
|
|
125
|
+
}
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
* plan §3a). FRAMEWORK-FREE: the builder, the SSR engine and iox-cms (generation-core's composer, the site
|
|
4
4
|
* build) read the same recipes, roles and structure rules. Card v2 + Badge / Chip (T23 / T25) are the
|
|
5
5
|
* first; the Accordion (CC2) adds the first composite RUNTIME (accordion-runtime.ts — DOM only, no
|
|
6
|
-
* framework); Tabs join with
|
|
6
|
+
* framework), which the Disclosure (CC2.1) runs too — a one-item accordion whose root is its item; Tabs join with
|
|
7
|
+
* their own phase.
|
|
7
8
|
* ⚠️ Consumers wire the usual trio — ng-package raw-ts assets, jest moduleNameMapper, tsconfig paths.
|
|
8
9
|
*/
|
|
9
10
|
export * from './composite-node';
|
|
11
|
+
export * from './composite-kinds';
|
|
10
12
|
export * from './accordion';
|
|
11
13
|
export * from './accordion-runtime';
|
|
14
|
+
export * from './disclosure';
|
|
12
15
|
export * from './badge';
|
|
13
16
|
export * from './card';
|
|
14
17
|
export * from './structure';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AccordionRole } from './
|
|
1
|
+
import { AccordionRole, DisclosureRole } from './composite-kinds';
|
|
2
2
|
import { CardPart, CardRole } from './card';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -29,6 +29,13 @@ export const COMPOSITE_ROLE_SPECS: Readonly<Record<string, CompositeRoleSpec>> =
|
|
|
29
29
|
[AccordionRole.Trigger]: { parent: AccordionRole.Item, locked: true },
|
|
30
30
|
[AccordionRole.Panel]: { parent: AccordionRole.Item, locked: true },
|
|
31
31
|
[AccordionRole.Icon]: { parent: AccordionRole.Trigger },
|
|
32
|
+
// CC2.1 — a disclosure always keeps its toggle and its panel; the words and the icon are the author's.
|
|
33
|
+
[DisclosureRole.Disclosure]: { parent: null },
|
|
34
|
+
[DisclosureRole.Trigger]: { parent: DisclosureRole.Disclosure, locked: true },
|
|
35
|
+
[DisclosureRole.Panel]: { parent: DisclosureRole.Disclosure, locked: true },
|
|
36
|
+
[DisclosureRole.More]: { parent: DisclosureRole.Trigger },
|
|
37
|
+
[DisclosureRole.Less]: { parent: DisclosureRole.Trigger },
|
|
38
|
+
[DisclosureRole.Icon]: { parent: DisclosureRole.Trigger },
|
|
32
39
|
};
|
|
33
40
|
|
|
34
41
|
export enum StructureEdit {
|