@vectoriox/iox-ui 5.35.0 → 5.36.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.
@@ -58,6 +58,9 @@ export interface PatternNode {
58
58
  /** The composite-core recipes a pattern may stamp (`recipe`). */
59
59
  export enum PatternRecipe {
60
60
  Card = 'card',
61
+ /** CC2: the Accordion — Manual (three placeholder items), or From content when the section names a list
62
+ * `binding` (`src: '$binding?'`): one template item bound to the type's question / answer fields. */
63
+ Accordion = 'accordion',
61
64
  }
62
65
 
63
66
  /** A recipe node's `binds` keys → the part each binds (Card v2). */
@@ -100,6 +103,8 @@ export enum PatternPlaceholder {
100
103
  export enum PatternDirective {
101
104
  /** A Repeater's source: the section's `binding`. */
102
105
  Binding = '$binding',
106
+ /** The section MAY name a list `binding` (faq.accordion): with one, the recipe's items come from it. */
107
+ OptionalBinding = '$binding?',
103
108
  /** `each`: one plain node per page of the plan that is in the navigation (`inNav`), in plan order —
104
109
  * words and routes written in, editable on the canvas (W1.1). */
105
110
  NavPages = '$navPages',
@@ -150,6 +155,11 @@ export function patternNeedsBinding(pattern: SitePattern): boolean {
150
155
  return patternNodes(pattern).some(n => n.src === PatternDirective.Binding);
151
156
  }
152
157
 
158
+ /** True when the pattern may take a section `binding` — it needs one, or names `$binding?` (CC2). */
159
+ export function patternTakesBinding(pattern: SitePattern): boolean {
160
+ return patternNeedsBinding(pattern) || patternNodes(pattern).some(n => n.src === PatternDirective.OptionalBinding);
161
+ }
162
+
153
163
  const IMAGE_TYPE = 'Image';
154
164
 
155
165
  /** The scopes whose ITEM a pattern's bound Images take their alt text from (an Image that names no `alt`
@@ -30,6 +30,10 @@ export interface SerializedNode {
30
30
  /** The part this node plays in a composite (composite-core: `card.title`; a navigation's `nav.drawer`,
31
31
  * written by `planNavShape`). Additive; a node without one is a plain node. */
32
32
  role?: string;
33
+ /** An item's identity inside its composite (CC2: an Accordion item's `k1`…). Additive. */
34
+ itemKey?: string;
35
+ /** A targeted state's driving node (CC2: an icon's `trigger-on` → its item). */
36
+ stateTriggers?: Partial<Record<string, string>>;
33
37
  bindings: SerializedBinding[];
34
38
  interactions: unknown[];
35
39
  isGlobal?: boolean;
@@ -89,7 +89,7 @@ export const SPEC_SITE_PLAN_SHAPE = {
89
89
  "section": {
90
90
  "pattern": "string — a non-global pattern id",
91
91
  "slots?": "{ [slot]: string } — the pattern's declared slots",
92
- "binding?": "string — the list source a pattern's $binding repeater iterates (exactly when it has one)",
92
+ "binding?": "string — the list source a pattern's $binding repeater iterates (exactly when it has one); a $binding? pattern (faq.accordion) MAY take one — then its items come from that content",
93
93
  "without?": "string[] — the pattern's optional parts to leave out"
94
94
  },
95
95
  "globals": {
@@ -933,6 +933,51 @@ export const SPEC_SITE_PATTERNS = {
933
933
  }
934
934
  ]
935
935
  }
936
+ },
937
+ "faq.accordion": {
938
+ "family": "faq",
939
+ "label": "FAQ — questions that open",
940
+ "node": {
941
+ "t": "Section",
942
+ "es": "section",
943
+ "c": [
944
+ {
945
+ "t": "Container",
946
+ "es": "box",
947
+ "lay": {
948
+ "display": "flex",
949
+ "flexDirection": "column",
950
+ "gap": "14px",
951
+ "maxWidth": "860px",
952
+ "width": "100%",
953
+ "margin": "0px auto 0px auto"
954
+ },
955
+ "c": [
956
+ {
957
+ "t": "Text",
958
+ "es": "eyebrow",
959
+ "slot": "eyebrow",
960
+ "ph": "copy",
961
+ "text": "FAQ"
962
+ },
963
+ {
964
+ "t": "Heading",
965
+ "es": "h2",
966
+ "level": "h2",
967
+ "slot": "title",
968
+ "ph": "copy",
969
+ "text": "Questions people ask"
970
+ },
971
+ {
972
+ "t": "Container",
973
+ "recipe": "accordion",
974
+ "src": "$binding?",
975
+ "ph": "copy"
976
+ }
977
+ ]
978
+ }
979
+ ]
980
+ }
936
981
  }
937
982
  } as const;
938
983
 
@@ -1,7 +1,7 @@
1
1
  import { flavorPackById, parseHexColor, SPEC_AXES } from '@vectoriox/iox-ui/design-core';
2
2
 
3
3
  import {
4
- altSourceScopes, BindScope, patternById, patternNeedsBinding, patternOptionalParts, patternScopes, patternSlots, PatternGlobalPosition,
4
+ altSourceScopes, BindScope, patternById, patternNeedsBinding, patternOptionalParts, patternTakesBinding, patternScopes, patternSlots, PatternGlobalPosition,
5
5
  } from './patterns';
6
6
  import {
7
7
  FACT_KEYS, GlobalSlot, PlanDataSourceMode, PlanDataSourceType, RelativeOp, SitePlanSourceKind, SITE_PLAN_VERSION,
@@ -367,9 +367,10 @@ function validatePages(c: Collector, list: unknown[], types: Map<string, Content
367
367
  });
368
368
  }
369
369
  const binding = c.string(s, 'binding', sp, false);
370
- if (patternNeedsBinding(pattern)) {
371
- if (!binding) c.add(SitePlanIssueCode.Binding, `${sp}.binding`, `"${patternId}" repeats the section's binding — name a list source`);
372
- else {
370
+ if (patternTakesBinding(pattern)) {
371
+ if (!binding) {
372
+ if (patternNeedsBinding(pattern)) c.add(SitePlanIssueCode.Binding, `${sp}.binding`, `"${patternId}" repeats the section's binding — name a list source`);
373
+ } else {
373
374
  const src = sources.get(binding);
374
375
  if (!src) c.add(SitePlanIssueCode.UnknownDataSource, `${sp}.binding`, `no data source "${binding}"`);
375
376
  else if (src.type !== PlanDataSourceType.Cms || src.mode !== PlanDataSourceMode.Query) c.add(SitePlanIssueCode.Binding, `${sp}.binding`, `"${binding}" is not a list source`);
@@ -207,3 +207,33 @@ dialog[data-iox-dialog]:not([open]) {
207
207
  --iox-heading-color: currentColor;
208
208
  --iox-text-muted: currentColor;
209
209
  }
210
+
211
+ /* ── CC2: the Accordion (complex-components boards 3–4; composite K1) ──────────────────────────────
212
+ The renderers write ONE markup (composite-core accordion.ts): the header is a <button> inside a heading of
213
+ its question's level; the panel sits in a region inside a TRACK. These rules are structural only — the look
214
+ is the parts' element styles (accordion-item / -trigger / -panel), on their own node rules. */
215
+
216
+ /* The heading wrapper carries the outline level and nothing of its own. */
217
+ .iox-accordion-heading { margin: 0; padding: 0; font: inherit; color: inherit; }
218
+ /* The header is a real button: its UA look is reset at zero specificity, so the node's own rule (the
219
+ accordion-trigger style and the recipe's row layout) always wins. */
220
+ :where(.iox-accordion-button) {
221
+ appearance: none; -webkit-appearance: none; background: none; border: 0; border-radius: 0; margin: 0;
222
+ font: inherit; color: inherit; text-align: start; cursor: pointer; box-sizing: border-box;
223
+ /* A <button> sizes to its content even as a block: the header is the whole row (its width, an OUTER
224
+ prop, lands on the canvas twin's host — the button fills it). */
225
+ width: 100%;
226
+ }
227
+ /* The ONE hiding rule (board 4, Q11): a closed item's panel track collapses to a 0 row. The region inside it
228
+ is `hidden="until-found"` while closed — content-visibility, not display:none — so find-in-page still
229
+ reaches the answer; it carries no padding (a hidden box keeps its own padding), the panel node inside it
230
+ does. Opening animates the row over dur-base (0ms under reduced motion: the motion tokens collapse). */
231
+ .iox-accordion-track {
232
+ display: grid; grid-template-rows: 0fr; min-width: 0;
233
+ transition: grid-template-rows var(--iox-dur-base) var(--iox-ease-standard);
234
+ }
235
+ .iox-accordion-track[data-iox-on] { grid-template-rows: 1fr; }
236
+ .iox-accordion-region { min-height: 0; min-width: 0; overflow: hidden; padding: 0; margin: 0; }
237
+ @media (prefers-reduced-motion: reduce) {
238
+ .iox-accordion-track { transition: none; }
239
+ }