@vectoriox/iox-ui 5.63.0 → 5.65.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.
@@ -1,7 +1,7 @@
1
1
  import { NEW_NODE_STYLE_DEFAULTS, defaultElementStyleFor, elementStyleById, ownedTraitsOf, applyElementStyle, flavorPackById, parseHexColor, SPEC_AXES, EntranceParam, staggerDelay, LinkType, COLOR_SCHEMES, setColorScheme, resolveDesignChoices, ANIMATE_IDS, auditDesignContrast } from '@vectoriox/iox-ui/design-core';
2
2
  import { formPresetById, FORM_PRESETS, ControlType, FORM_NODE_TYPE, collectFormSpec, FIELD_NODE_TYPE } from '@vectoriox/iox-ui/forms-core';
3
3
  import { SEO_DESCRIPTION_MAX, SEO_TITLE_MAX } from '@vectoriox/iox-ui/seo-core';
4
- import { segmentedOptions, ITEM_OPEN_STATE, CardPart, CardRole, CardLayout, CARD_LAYOUTS, CARD_PARTS, BADGE_TONES, buildCardRecipe, findByRole, buildSegmentedRecipe, AccordionRole, buildAccordionRecipe, ACCORDION_WORDS, accordionItems, accordionQuestionOf, accordionAnswerOf, ACCORDION_BIND_PROP, pickAccordionAutoBinds, ACCORDION_CONTENT_LIMIT, buildTabsRecipe, TABS_WORDS, tabsTriggers, tabLabelNodeOf, TABS_LABEL_BIND_PROP, tabsPanels, tabPanelTextOf, TABS_TEXT_BIND_PROP, tabPanelImageOf, pickTabsAutoBinds, tabsListOf, tabsPanelsOf, TABS_CONTENT_LIMIT, TABS_IMAGE_BIND_PROP, buildCarouselRecipe, SLIDE_TYPE, buildLightboxRecipe, LIGHTBOX_LABEL, SLIDE_TO_ROW } from '@vectoriox/iox-ui/composite-core';
4
+ import { pickTabsAutoBinds, pickAccordionAutoBinds, segmentedOptions, ITEM_OPEN_STATE, CardPart, CardRole, CardLayout, CARD_LAYOUTS, CARD_PARTS, BADGE_TONES, buildCardRecipe, findByRole, buildSegmentedRecipe, AccordionRole, buildAccordionRecipe, ACCORDION_WORDS, accordionItems, accordionQuestionOf, accordionAnswerOf, ACCORDION_BIND_PROP, ACCORDION_CONTENT_LIMIT, buildTabsRecipe, TABS_WORDS, tabsTriggers, tabLabelNodeOf, TABS_LABEL_BIND_PROP, tabsPanels, tabPanelTextOf, TABS_TEXT_BIND_PROP, tabPanelImageOf, tabsListOf, tabsPanelsOf, TABS_CONTENT_LIMIT, TABS_IMAGE_BIND_PROP, buildCarouselRecipe, SLIDE_TYPE, buildLightboxRecipe, LIGHTBOX_LABEL, SLIDE_TO_ROW } from '@vectoriox/iox-ui/composite-core';
5
5
  import { ELEMENT_ACTION_DESCRIPTORS, resolveActionAvailability } from '@vectoriox/iox-ui/actions-core';
6
6
 
7
7
  // GENERATED from iox-ai-guidance flavor-spec.v1.json — do not edit; re-run scripts/gen-flavor-spec.mjs
@@ -10752,6 +10752,31 @@ function jsonSchemaObjects(node, out = []) {
10752
10752
  return out;
10753
10753
  }
10754
10754
 
10755
+ const LIST_RECIPES = {
10756
+ [PatternRecipe.Accordion]: { picks: f => ({ ...pickAccordionAutoBinds(f) }), required: ['question', 'answer'], needs: 'short-text field for the question and long-text field for the answer' },
10757
+ [PatternRecipe.Tabs]: { picks: f => ({ ...pickTabsAutoBinds(f) }), required: ['label', 'text'], needs: 'short-text field for the tab and long-text field for its panel' },
10758
+ };
10759
+ /** The fields `recipe` binds from a listed type's `fields`, and what they lack. A recipe that lists nothing lacks nothing. */
10760
+ function recipeListBinds(recipe, fields) {
10761
+ const spec = recipe ? LIST_RECIPES[recipe] : undefined;
10762
+ if (!spec)
10763
+ return { picks: {}, gap: null };
10764
+ const picks = spec.picks(fields);
10765
+ return { picks: picks, gap: spec.required.every(key => picks[key]) ? null : spec.needs };
10766
+ }
10767
+ const listsBinding = (src) => src === PatternDirective.Binding || src === PatternDirective.OptionalBinding;
10768
+ /** What a listed type with `fields` lacks for the pattern's recipes that repeat the section's binding — the first gap, or null. */
10769
+ function patternListGap(pattern, fields) {
10770
+ for (const node of patternNodes(pattern)) {
10771
+ if (!listsBinding(node.src))
10772
+ continue;
10773
+ const { gap } = recipeListBinds(node.recipe, fields);
10774
+ if (gap)
10775
+ return gap;
10776
+ }
10777
+ return null;
10778
+ }
10779
+
10755
10780
  /**
10756
10781
  * The Site Plan validator — the gate in front of the composer. The wizard's plan and (later) the AI's
10757
10782
  * plan go through the SAME checks: an unknown pattern, a slot the pattern does not declare, a source
@@ -10972,10 +10997,11 @@ function validateContentTypes(c, list) {
10972
10997
  if (name && types.has(name))
10973
10998
  c.add(SitePlanIssueCode.Duplicate, `${path}.name`, `content type "${name}" is used twice`);
10974
10999
  else if (name)
10975
- types.set(name, { fields: fieldSet, roles });
11000
+ types.set(name, { fields: fieldSet, described: fields.filter(isPlainObject).map(describeField), roles });
10976
11001
  });
10977
11002
  return types;
10978
11003
  }
11004
+ const describeField = (f) => ({ name: String(f['name']), type: typeof f['type'] === 'string' ? f['type'] : undefined, controller: typeof f['controller'] === 'string' ? f['controller'] : undefined });
10979
11005
  /** A content type's roles (L0-3): known roles only, each played by a field of the type; `title` always; `gallery` a list. */
10980
11006
  function validateRoles(c, roles, path, typeName, fields, fieldTypes) {
10981
11007
  if (!isPlainObject(roles)) {
@@ -11202,6 +11228,12 @@ function validatePages(c, list, types, sources, density) {
11202
11228
  c.add(SitePlanIssueCode.UnknownDataSource, `${sp}.binding`, `no data source "${binding}"`);
11203
11229
  else if (src.type !== PlanDataSourceType.Cms || src.mode !== PlanDataSourceMode.Query)
11204
11230
  c.add(SitePlanIssueCode.Binding, `${sp}.binding`, `"${binding}" is not a list source`);
11231
+ else {
11232
+ const listed = src.contentType ? types.get(src.contentType) : undefined;
11233
+ const gap = listed ? patternListGap(pattern, listed.described) : null;
11234
+ if (gap)
11235
+ c.add(SitePlanIssueCode.Binding, `${sp}.binding`, `"${binding}" lists ${src.contentType}, which has no ${gap} — bind "${patternId}" to a list that has${patternNeedsBinding(pattern) ? '' : ', or to none'}`);
11236
+ }
11205
11237
  }
11206
11238
  }
11207
11239
  else if (binding)
@@ -11627,11 +11659,23 @@ function makeKit(globals, rootId, newId) {
11627
11659
  }
11628
11660
  /** Point an opener's dialog actions at `dialogId` (a drawer rebuilt under a new id keeps its openers working). */
11629
11661
  function retarget(node, dialogId) {
11630
- for (const ix of node.interactions) {
11662
+ for (const ix of (node.interactions ?? [])) {
11631
11663
  for (const a of ix.actions ?? [])
11632
- if (a.type === 'openDialog' || a.type === 'toggleDialog')
11664
+ if (a.type === NavDialogAction.Open || a.type === NavDialogAction.Toggle)
11633
11665
  a.target = dialogId;
11634
11666
  }
11667
+ return asToggle(node);
11668
+ }
11669
+ /** The menu button and More are TOGGLES (a second click closes what the first opened): a part saved before that
11670
+ * rule opened only — kept through a change of shape, it toggles from then on. */
11671
+ function asToggle(node) {
11672
+ for (const ix of (node.interactions ?? [])) {
11673
+ if (ix.trigger !== CLICK)
11674
+ continue;
11675
+ for (const a of ix.actions ?? [])
11676
+ if (a.type === NavDialogAction.Open)
11677
+ a.type = NavDialogAction.Toggle;
11678
+ }
11635
11679
  return node;
11636
11680
  }
11637
11681
  function part(kit, suffix, type, props, style, role, lay = {}) {
@@ -11644,6 +11688,13 @@ function part(kit, suffix, type, props, style, role, lay = {}) {
11644
11688
  Object.assign(node.styleProps, lay);
11645
11689
  return node;
11646
11690
  }
11691
+ /** The dialog actions a nav part runs on click. */
11692
+ var NavDialogAction;
11693
+ (function (NavDialogAction) {
11694
+ NavDialogAction["Open"] = "openDialog";
11695
+ NavDialogAction["Close"] = "closeDialog";
11696
+ NavDialogAction["Toggle"] = "toggleDialog";
11697
+ })(NavDialogAction || (NavDialogAction = {}));
11647
11698
  function clickTo(kit, suffix, type, target) {
11648
11699
  return { id: kit.id(`${suffix}-click`), trigger: CLICK, actions: [{ type, target, duration: 0, delay: 0, easing: 'ease' }] };
11649
11700
  }
@@ -11675,10 +11726,14 @@ function iconNode(kit, suffix, iconClass) {
11675
11726
  function labelText(kit, suffix, words) {
11676
11727
  return part(kit, suffix, TEXT$1, { content: words, tag: 'span' }, 'button-label');
11677
11728
  }
11729
+ /** The menu button and the drawer's Close are ONE size — and the shared CSS pads the drawer like the header
11730
+ * (`.iox-role-nav-drawer` re-declares dialog-pad as nav-pad) — so the Close lands where the menu button is and
11731
+ * reads as the same button turned into an X. */
11732
+ const MENU_BUTTON_LAY = { width: '48px', height: '48px', alignItems: 'center', justifyContent: 'center' };
11678
11733
  function menuButton(kit, drawerId, at) {
11679
- const btn = part(kit, 'menu', BUTTON, { tag: 'button', ariaLabel: NAV_SHAPE_COPY.menu }, 'button-ghost', NavRole.MenuButton, { width: '48px', height: '48px', alignItems: 'center', justifyContent: 'center' });
11734
+ const btn = part(kit, 'menu', BUTTON, { tag: 'button', ariaLabel: NAV_SHAPE_COPY.menu }, 'button-ghost', NavRole.MenuButton, { ...MENU_BUTTON_LAY });
11680
11735
  btn.children = [iconNode(kit, 'menu-icon', MENU_ICON)];
11681
- btn.interactions = [clickTo(kit, 'menu', 'openDialog', drawerId)];
11736
+ btn.interactions = [clickTo(kit, 'menu', NavDialogAction.Toggle, drawerId)];
11682
11737
  phoneOnly(btn, at, 'inline-flex');
11683
11738
  return btn;
11684
11739
  }
@@ -11691,9 +11746,9 @@ function drawer(kit, root, pages, at) {
11691
11746
  }, 'dialog', NavRole.Drawer, { width: '86vw', maxWidth: '360px', display: 'flex', flexDirection: 'column', gap: '20px' });
11692
11747
  const brandWords = textOf(find([root], NavRole.Brand) ?? root) || NAV_PART_LABELS[NavRole.Root];
11693
11748
  const head = part(kit, 'drawer-head', CONTAINER, {}, 'box', NavRole.DrawerHead, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '12px' });
11694
- const close = part(kit, 'drawer-close', BUTTON, { tag: 'button', ariaLabel: NAV_SHAPE_COPY.closeMenu }, 'button-ghost', NavRole.DrawerClose, { width: '44px', height: '44px', alignItems: 'center', justifyContent: 'center' });
11749
+ const close = part(kit, 'drawer-close', BUTTON, { tag: 'button', ariaLabel: NAV_SHAPE_COPY.closeMenu }, 'button-ghost', NavRole.DrawerClose, { ...MENU_BUTTON_LAY });
11695
11750
  close.children = [iconNode(kit, 'drawer-close-icon', CLOSE_ICON)];
11696
- close.interactions = [clickTo(kit, 'drawer-close', 'closeDialog', dr.id)];
11751
+ close.interactions = [clickTo(kit, 'drawer-close', NavDialogAction.Close, dr.id)];
11697
11752
  head.children = [part(kit, 'drawer-brand', HEADING, { content: brandWords, level: 'h3' }, 'h3'), close];
11698
11753
  const list = part(kit, 'drawer-links', CONTAINER, {}, 'box', NavRole.DrawerLinks, { display: 'flex', flexDirection: 'column', gap: '4px' });
11699
11754
  // Board 1: every page, Home first, at lead size (on each Link: a Link's own baseline font size would
@@ -11729,7 +11784,7 @@ const BAR_ITEM_LAY = {
11729
11784
  function moreItem(kit, drawerId) {
11730
11785
  const more = part(kit, 'bar-more', BUTTON, { tag: 'button' }, 'bottom-bar-item', NavRole.More, BAR_ITEM_LAY);
11731
11786
  more.children = [iconNode(kit, 'bar-more-icon', MORE_ICON), labelText(kit, 'bar-more-label', NAV_SHAPE_COPY.more)];
11732
- more.interactions = [clickTo(kit, 'bar-more', 'openDialog', drawerId)];
11787
+ more.interactions = [clickTo(kit, 'bar-more', NavDialogAction.Toggle, drawerId)];
11733
11788
  return more;
11734
11789
  }
11735
11790
  /** Home first, then the header's pages: the header reaches Home through its brand, a phone menu names it. */
@@ -11839,7 +11894,7 @@ function planNavShape(globals, shape, options = {}, navId) {
11839
11894
  dr.props = { ...dr.props, closeAbove: at };
11840
11895
  // The menu button: Drawer only (Q4: with a bar, the phone header keeps only the brand).
11841
11896
  const existingMenu = removeRole(root, NavRole.MenuButton);
11842
- const menu = wantMenuButton ? (existingMenu ?? menuButton(kit, dr.id, at)) : null;
11897
+ const menu = wantMenuButton ? (existingMenu ? asToggle(existingMenu) : menuButton(kit, dr.id, at)) : null;
11843
11898
  for (const p of phoneHiddenParts(root)) {
11844
11899
  moveDisplayOverride(p, at);
11845
11900
  hideAt(p, at);
@@ -12698,6 +12753,13 @@ function composeSegmented(pn, id) {
12698
12753
  applyLayout(root, pn);
12699
12754
  return root;
12700
12755
  }
12756
+ /** The content type a section binding lists, through its data source. */
12757
+ function boundContentType(binding, ctx) {
12758
+ const source = ctx.plan.dataSources.find(d => d.alias === binding);
12759
+ return source && 'contentType' in source ? ctx.plan.contentTypes.find(t => t.name === source.contentType) : undefined;
12760
+ }
12761
+ /** A type's fields as composite-core's pickers read them. */
12762
+ const fieldsOf = (type) => (type?.fields ?? []).map(f => ({ name: f.name, type: f.type, controller: f.controller }));
12701
12763
  /**
12702
12764
  * An Accordion (CC2) — the composite-core recipe stamped with this composer's factory. Manual (no section
12703
12765
  * binding): three items whose questions and answers are placeholders the studio rewrites (`ph: 'copy'`), never
@@ -12726,12 +12788,9 @@ function composeAccordion(pn, id, ctx) {
12726
12788
  }
12727
12789
  return root;
12728
12790
  }
12729
- const source = ctx.plan.dataSources.find(d => d.alias === binding);
12730
- const type = source && 'contentType' in source ? ctx.plan.contentTypes.find(t => t.name === source.contentType) : undefined;
12731
- const picks = pickAccordionAutoBinds((type?.fields ?? []).map(f => ({ name: f.name, type: f.type, controller: f.controller })));
12732
- if (!picks.question || !picks.answer) {
12733
- throw new ComposeError(`${id}: "${binding}" has no short-text field for the question and long-text field for the answer`);
12734
- }
12791
+ const { picks, gap } = recipeListBinds(pn.recipe, fieldsOf(boundContentType(binding, ctx)));
12792
+ if (gap)
12793
+ throw new ComposeError(`${id}: "${binding}" has no ${gap}`);
12735
12794
  addBinding(root, { prop: 'source', source: binding, path: '' });
12736
12795
  setProp(root, 'limit', ACCORDION_CONTENT_LIMIT);
12737
12796
  const [template] = accordionItems(root);
@@ -12740,11 +12799,6 @@ function composeAccordion(pn, id, ctx) {
12740
12799
  addBinding(accordionAnswerOf(template), { prop: ACCORDION_BIND_PROP, source: binding, path: picks.answer });
12741
12800
  return root;
12742
12801
  }
12743
- /** The content type a section binding lists, through its data source. */
12744
- function boundContentType(binding, ctx) {
12745
- const source = ctx.plan.dataSources.find(d => d.alias === binding);
12746
- return source && 'contentType' in source ? ctx.plan.contentTypes.find(t => t.name === source.contentType) : undefined;
12747
- }
12748
12802
  const TABS_ROOT_KEY = 'tabs';
12749
12803
  const TABS_HOLDER_KEYS = new Set(['list', 'panels']);
12750
12804
  /**
@@ -12783,11 +12837,9 @@ function composeTabs(pn, id, ctx) {
12783
12837
  }
12784
12838
  return root;
12785
12839
  }
12786
- const type = boundContentType(binding, ctx);
12787
- const picks = pickTabsAutoBinds((type?.fields ?? []).map(f => ({ name: f.name, type: f.type, controller: f.controller })));
12788
- if (!picks.label || !picks.text) {
12789
- throw new ComposeError(`${id}: "${binding}" has no short-text field for the tab and long-text field for its panel`);
12790
- }
12840
+ const { picks, gap } = recipeListBinds(pn.recipe, fieldsOf(boundContentType(binding, ctx)));
12841
+ if (gap)
12842
+ throw new ComposeError(`${id}: "${binding}" has no ${gap}`);
12791
12843
  for (const holder of [tabsListOf(tree), tabsPanelsOf(tree)]) {
12792
12844
  addBinding(holder, { prop: 'source', source: binding, path: '' });
12793
12845
  setProp(holder, 'limit', TABS_CONTENT_LIMIT);
@@ -13380,6 +13432,85 @@ function buildStepFacts(plan, actions = {}) {
13380
13432
  };
13381
13433
  }
13382
13434
 
13435
+ /**
13436
+ * What a plan reads but no model is asked to write (vectoriox/iox-cms#49): the data sources that follow from the pages
13437
+ * alone. A per-item page whose sections read the NEXT item (`project.next`) needs a `$siblings.next` source of its route
13438
+ * source — a fact of the pages, while data sources are the plan's STRUCTURE, asked first and fixed once accepted. Asking
13439
+ * the structure for it made the pages' answer unfixable by construction (a re-ask of the pages cannot add a source), so
13440
+ * it is derived, the same way for the plan wire's expansion (`expandPlanWire`) and for a sentence's page edits
13441
+ * (`applyPlanEdits`). Pure and idempotent: a plan that already holds the source gains nothing; a source is derived only
13442
+ * where it is valid by construction (its subject a single cms source of a type of the plan), so it never adds an issue
13443
+ * at a structure path. Its siblings are walked within an unlimited list of the same type — the structure's first, else a
13444
+ * derived one.
13445
+ */
13446
+ const NEXT_SUFFIX = 'Next';
13447
+ const LIST_SUFFIX = 'All';
13448
+ /** The sources by alias, as the validator reads them (the first of an alias wins). */
13449
+ function sourceViews(list) {
13450
+ const views = new Map();
13451
+ for (const raw of list) {
13452
+ if (isPlainObject(raw) && isNonEmptyString(raw['alias']) && !views.has(raw['alias']))
13453
+ views.set(raw['alias'], raw);
13454
+ }
13455
+ return views;
13456
+ }
13457
+ const readsNext = (page) => Array.isArray(page['sections']) && page['sections'].some(s => {
13458
+ const pattern = isPlainObject(s) && typeof s['pattern'] === 'string' ? patternById(s['pattern']) : undefined;
13459
+ return !!pattern && patternScopes(pattern).has(BindScope.Next);
13460
+ });
13461
+ /** The alias itself, else the first of `<alias>2`, `<alias>3`… no source has. */
13462
+ function freeAlias(base, taken) {
13463
+ let alias = base;
13464
+ for (let n = 2; taken.has(alias); n++)
13465
+ alias = `${base}${n}`;
13466
+ return alias;
13467
+ }
13468
+ /** The unlimited list of `contentType` the plan holds (its first), or null. */
13469
+ function fullListOf(views, contentType) {
13470
+ for (const [alias, s] of views) {
13471
+ if (s.type === PlanDataSourceType.Cms && s.mode === PlanDataSourceMode.Query && s.contentType === contentType && s.limit === undefined)
13472
+ return alias;
13473
+ }
13474
+ return null;
13475
+ }
13476
+ /**
13477
+ * The data sources `plan` lacks and its pages need, in page order (appended after the plan's own). Tolerates any
13478
+ * value: what is not a well-formed plan gains nothing, for the validator to judge as written.
13479
+ */
13480
+ function derivedDataSources(plan) {
13481
+ if (!isPlainObject(plan) || !Array.isArray(plan['dataSources']) || !Array.isArray(plan['pages']) || !Array.isArray(plan['contentTypes']))
13482
+ return [];
13483
+ const types = new Set(plan['contentTypes'].map(t => (isPlainObject(t) ? t['name'] : undefined)).filter(isNonEmptyString));
13484
+ const views = sourceViews(plan['dataSources']);
13485
+ const derived = [];
13486
+ const add = (source) => { derived.push(source); views.set(source.alias, source); };
13487
+ for (const page of plan['pages']) {
13488
+ if (!isPlainObject(page) || !isNonEmptyString(page['routeSource']) || !readsNext(page))
13489
+ continue;
13490
+ const subject = page['routeSource'];
13491
+ const route = views.get(subject);
13492
+ if (!route || route.type !== PlanDataSourceType.Cms || route.mode !== PlanDataSourceMode.Single)
13493
+ continue;
13494
+ if (!isNonEmptyString(route.contentType) || !types.has(route.contentType) || nextSourceOf(views, subject))
13495
+ continue;
13496
+ let within = fullListOf(views, route.contentType);
13497
+ if (!within) {
13498
+ within = freeAlias(`${subject}${LIST_SUFFIX}`, views);
13499
+ add({ alias: within, type: PlanDataSourceType.Cms, mode: PlanDataSourceMode.Query, contentType: route.contentType });
13500
+ }
13501
+ add({ alias: freeAlias(`${subject}${NEXT_SUFFIX}`, views), type: PlanDataSourceType.Relative, of: subject, op: RelativeOp.SiblingsNext, within });
13502
+ }
13503
+ return derived;
13504
+ }
13505
+ /** `plan` with the data sources its pages need appended (`derivedDataSources`); the same object when it lacks none. */
13506
+ function withDerivedSources(plan) {
13507
+ const derived = derivedDataSources(plan);
13508
+ if (!derived.length)
13509
+ return plan;
13510
+ const own = plan;
13511
+ return { ...own, dataSources: [...own['dataSources'], ...derived] };
13512
+ }
13513
+
13383
13514
  /**
13384
13515
  * The plan WIRE (AI plan M1-3, vectoriox/iox-cms#45, #47): what a model writes when it plans a site — a compact format
13385
13516
  * inside the Claude API's structured-output limits (≤ 24 optional parameters and ≤ 16 unions per request, and a compiled
@@ -13400,6 +13531,10 @@ function buildStepFacts(plan, actions = {}) {
13400
13531
  * - **What the model does not decide is not on the wire** (`PLAN_WIRE_DERIVED`): the look (the owner's pick), the run,
13401
13532
  * the language (the reading's), the facts (the owner's own, verbatim), the version, the home page (the page at `/`).
13402
13533
  * So the format is the same for every studio and every run — one compiled grammar, cached by the provider.
13534
+ * - **What follows from the pages is derived, never asked** (`plan-derived.ts`, vectoriox/iox-cms#49): a per-item page
13535
+ * reading the next item gets its `$siblings.next` source — the structure, asked first and then fixed, cannot know the
13536
+ * pages need it, and the pages cannot add a source. `planToWire` writes every source the plan holds, a derived one
13537
+ * too (it is the plan's: a change's brief shows it, and a plan → wire → plan round trip stays exact).
13403
13538
  *
13404
13539
  * ONE source: every wire key names the plan keys it carries (`refs`) and is described in the plan table's own words, and
13405
13540
  * every plan key is either carried or derived (`planWireGaps()`); a key the plan shape gains breaks the builder until it
@@ -13444,7 +13579,7 @@ const WIRE_KEYS = {
13444
13579
  plan: {
13445
13580
  business: inPart(PlanPart.Structure, key('plan.business')),
13446
13581
  contentTypes: inPart(PlanPart.Structure, key('plan.contentTypes')),
13447
- dataSources: inPart(PlanPart.Structure, { refs: ['plan.dataSources'], text: 'dataSource[] — merged by alias into the org library' }),
13582
+ dataSources: inPart(PlanPart.Structure, { refs: ['plan.dataSources'], text: "dataSource[] — merged by alias into the org library; a per-item page's $siblings.next source (what a section reading the next item needs) is added for you" }),
13448
13583
  pages: inPart(PlanPart.Pages, key('plan.pages')),
13449
13584
  globals: inPart(PlanPart.Pages, key('plan.globals')),
13450
13585
  },
@@ -13774,7 +13909,7 @@ function expandWireBusiness(wire, facts) {
13774
13909
  * A model's plan (its parts, on the wire) + what the generation decides (the look, the run, the language, the owner's
13775
13910
  * facts) → the Site Plan. Pure. Translates, never repairs: whatever an answer got wrong is still wrong, for
13776
13911
  * `validateSitePlan` — a part that is not an object is the result itself, a part not written yet leaves its keys out
13777
- * (the validator then names them, at their own paths).
13912
+ * (the validator then names them, at their own paths). What follows from the pages alone is added (`withDerivedSources`).
13778
13913
  */
13779
13914
  function expandPlanWire(parts, context) {
13780
13915
  const written = PLAN_PARTS.filter(part => parts[part] !== undefined).map(part => parts[part]);
@@ -13783,7 +13918,7 @@ function expandPlanWire(parts, context) {
13783
13918
  return odd;
13784
13919
  const wire = Object.assign({}, ...written);
13785
13920
  const { business, contentTypes, dataSources, pages, ...rest } = wire;
13786
- return {
13921
+ return withDerivedSources({
13787
13922
  ...rest,
13788
13923
  version: SITE_PLAN_VERSION,
13789
13924
  source: { ...context.source },
@@ -13793,7 +13928,7 @@ function expandPlanWire(parts, context) {
13793
13928
  contentTypes: each(contentTypes, expandWireContentType),
13794
13929
  dataSources: each(dataSources, expandWireDataSource),
13795
13930
  pages: each(pages, expandWirePage),
13796
- };
13931
+ });
13797
13932
  }
13798
13933
  // ── The plan → the wire ────────────────────────────────────────────────────────────────────────────────────────────
13799
13934
  function fieldToWire(field) {
@@ -14491,8 +14626,9 @@ const APPLY = {
14491
14626
  };
14492
14627
  /**
14493
14628
  * Apply a set of edits to a plan — all of them, or none (any issue refuses the set and returns the plan unchanged). The
14494
- * sources only removed pages and sections read are dropped with them; sources nothing read before are kept.
14495
- * The result still has to pass `validateSitePlan`.
14629
+ * sources only removed pages and sections read are dropped with them; sources nothing read before are kept; the sources
14630
+ * the pages now need and no model writes are derived (`derivedDataSources` — a page edit adding `project.next`, as the
14631
+ * plan wire's expansion does, vectoriox/iox-cms#49). The result still has to pass `validateSitePlan`.
14496
14632
  */
14497
14633
  function applyPlanEdits(plan, edits) {
14498
14634
  const structure = validatePlanEdits(edits);
@@ -14511,7 +14647,11 @@ function applyPlanEdits(plan, edits) {
14511
14647
  for (const s of draft.dataSources)
14512
14648
  if (!dataSources.includes(s))
14513
14649
  changed.push({ kind: PlanChangeKind.DataSource, action: PlanChangeAction.Removed, name: s.alias });
14514
- return { plan: { ...plan, pages: draft.pages, contentTypes: draft.contentTypes, dataSources }, changed, issues: [] };
14650
+ const edited = { ...plan, pages: draft.pages, contentTypes: draft.contentTypes, dataSources };
14651
+ const derived = derivedDataSources(edited);
14652
+ for (const s of derived)
14653
+ changed.push({ kind: PlanChangeKind.DataSource, action: PlanChangeAction.Added, name: s.alias });
14654
+ return { plan: derived.length ? { ...edited, dataSources: [...dataSources, ...derived] } : edited, changed, issues: [] };
14515
14655
  }
14516
14656
  // ── The format a model writes edits in ─────────────────────────────────────────────────────────────────
14517
14657
  /** Each part's answer format name. */
@@ -15297,5 +15437,5 @@ function addStockCredits(footer, credits) {
15297
15437
  * Generated bundle index. Do not edit.
15298
15438
  */
15299
15439
 
15300
- export { ACTIVE_BUILD_STATUSES, AI_REFUSAL_NEXT, AI_REFUSAL_STATUS, AI_STEPS, AI_STEP_LABELS, AI_STEP_NEEDS, AI_STEP_PART_TOKENS, AI_STEP_TYPICAL_TOKENS, AiCallStatus, AiCapability, AiFeature, AiGenerationStage, AiKeyMode, AiLimit, AiNextKind, AiProviderId, AiRefusalCode, AiStep, BAR_ITEMS_MAX, BAR_ITEMS_MIN, BASELINE_ROLES, BUILD_STEPS, BaselineRole, BindScope, BuildNoticeCode, BuildStep, CONTENT_ROLE_MEANINGS, CREDITS_PER_USD, CardBindTarget, CheckSeverity, ComposeError, ComposeIssueCode, ContentRole, ContentTypeAction, CreatedKind, DANGLING_LINK_CODES, DEFAULT_PHONE_MENU_BREAKPOINT, DEFAULT_SITE_LOCALE, ENTRANCE_EASING, ENTRANCE_ORIGIN_LOOK, ENTRANCE_TRIGGER, EntrancePart, EntranceSide, FACT_KEYS, FINAL_GENERATION_STAGES, FINDING_SEVERITY, GRAMMAR_TOO_LARGE_MESSAGE, GlobalSlot, HEBREW_PREFIXES, JSON_SCHEMA_BANNED_KEYWORDS, JsonSchemaShapeError, KeptReason, NAV_PART_LABELS, NAV_SHAPE_COPY, NEUTRAL_STYLE_PROPS, NODE_BASELINES, NO_TOKENS, NavPagesSource, NavRole, NavShape, NavShapeError, PAGE_ARCHETYPES, PLAN_EDITS_ANSWER_SHAPES, PLAN_EDITS_PART_SCHEMA_NAMES, PLAN_EDIT_OP_PART, PLAN_EDIT_SHAPE, PLAN_EDIT_WIRE_KEYS, PLAN_PARTS, PLAN_PART_KEYS, PLAN_WIRE_DERIVED, PLAN_WIRE_PART_SCHEMA_NAMES, PLAN_WIRE_SHAPE, PRICING_PATTERN, PUBLISH_BLOCKING_PLACEHOLDERS, PageDataKind, PathIssueCollector, PatternDirective, PatternGlobalPosition, PatternOpens, PatternPlaceholder, PatternRecipe, PhoneMenuBreakpoint, PlaceholderKind, PlanChangeAction, PlanChangeKind, PlanDataSourceMode, PlanDataSourceType, PlanEditIssueCode, PlanEditOp, PlanEditsOutcome, PlanPart, PlanWireEntry, PlanWireShapeError, PlanWordIssueCode, PreflightRefusalCode, PublishRefusalCode, REQUIRED_CONTENT_ROLES, RelativeOp, SAMPLE_IMAGE_REF, SITE_CHECKS, SITE_FACT_LABELS, SITE_HEADER_PATTERN, SITE_LIBRARY, SITE_LOCALES, SITE_NEED_IDS, SITE_PATTERNS, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_ARCHETYPES, SPEC_SITE_COPY_LENGTHS, SPEC_SITE_DENSITY_COPY, SPEC_SITE_FACTS, SPEC_SITE_LIBRARY, SPEC_SITE_LOCALES, SPEC_SITE_NEED_IDS, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_REQUIRED_ROLES, SPEC_SITE_ROLES, SPEC_SITE_TEMPLATES, STOCK_FAMILIES, STOCK_PROVIDER_TERMS, STRICT_SCHEMA_BUDGET, STRICT_SCHEMA_LIMITS, STUDIO_TEMPLATE, SiteBuildKind, SiteBuildStatus, SiteCheckId, SiteFindingCode, SiteLocale, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, StepState, StockProviderId, addBarItem, addStockCredits, aiStepParts, applyPlanEdits, applyStockPhotos, archetypeIssues, assertStyleSlots, assertValidSitePlan, auditSite, barIconForRoute, blocksPublish, buildKeys, buildStepFacts, builtHomePageId, composeNavHeader, composeSection, composeSite, contentTypePreviewFields, contentTypeToWire, copyLengthOf, costUsd, createSerializedNode, creditsOfUsd, dataSourceToWire, describePlanData, describeShapeKey, entranceAction, entranceInteraction, estimateStepCredits, expandPlanWire, expandWireContentType, expandWireDataSource, expandWireEdit, expandWireField, expandWirePage, expandWireSection, failedBuildStep, fieldToWire, findNavRoot, isActiveBuild, isNonEmptyString, isPlainObject, isRtlLocale, jsonAnyOf, jsonBoolean, jsonClosedObject, jsonInteger, jsonListOf, jsonOneOf, jsonSchemaKeywords, jsonSchemaObjects, jsonShapedObject, jsonText, modelCanDo, moveBarItem, navPages, navShapeBreakpoint, navShapeOf, nextSourceOf, nodeBindPaths, pageToWire, parsePatternPath, parseSampleImageRef, pathAt, patternById, patternHasForm, patternLinkSlots, patternManifest, patternNeedsBinding, patternNodes, patternOptionalParts, patternRoleNeeds, patternScopes, patternSlotBudgets, patternSlots, patternTakesBinding, phoneMenuPages, planEditOps, planEditToWire, planEditWireGaps, planEditWireKeys, planEditWireOps, planEditWireShape, planEditsFromWire, planEditsJsonSchema, planEditsToWire, planEntrances, planFromTemplate, planNavShape, planPartOfPath, planPartToWire, planStockPhotos, planToWire, planWireGaps, planWirePartJsonSchema, planWirePartShape, planWordIssues, publishRefusal, publishVerdict, releasedLayoutDefaults, removeBarItem, requiredAcknowledgements, requiredShapeKeys, roleField, routeMatches, routeParams, sampleImageRef, sectionEntrances, sectionPatternIds, sectionToWire, setStyleProps, shapeKeys, showcaseGlobals, showcasePages, showcasePlans, siteNavPages, slotBudget, stockCopyKey, stockCreditLine, stockCreditLines, stockCreditNode, stockCreditNodeId, stockSlots, strictSchemaComplexity, strictSchemaRefusal, typicalStepTokens, validatePlanEdits, validateSitePlan, walkSerialized, wireContentTypeJsonSchema, wireDataSourceJsonSchema, wireFieldJsonSchema, wirePageJsonSchema, wireSectionJsonSchema, wordPattern, writtenTexts };
15440
+ export { ACTIVE_BUILD_STATUSES, AI_REFUSAL_NEXT, AI_REFUSAL_STATUS, AI_STEPS, AI_STEP_LABELS, AI_STEP_NEEDS, AI_STEP_PART_TOKENS, AI_STEP_TYPICAL_TOKENS, AiCallStatus, AiCapability, AiFeature, AiGenerationStage, AiKeyMode, AiLimit, AiNextKind, AiProviderId, AiRefusalCode, AiStep, BAR_ITEMS_MAX, BAR_ITEMS_MIN, BASELINE_ROLES, BUILD_STEPS, BaselineRole, BindScope, BuildNoticeCode, BuildStep, CONTENT_ROLE_MEANINGS, CREDITS_PER_USD, CardBindTarget, CheckSeverity, ComposeError, ComposeIssueCode, ContentRole, ContentTypeAction, CreatedKind, DANGLING_LINK_CODES, DEFAULT_PHONE_MENU_BREAKPOINT, DEFAULT_SITE_LOCALE, ENTRANCE_EASING, ENTRANCE_ORIGIN_LOOK, ENTRANCE_TRIGGER, EntrancePart, EntranceSide, FACT_KEYS, FINAL_GENERATION_STAGES, FINDING_SEVERITY, GRAMMAR_TOO_LARGE_MESSAGE, GlobalSlot, HEBREW_PREFIXES, JSON_SCHEMA_BANNED_KEYWORDS, JsonSchemaShapeError, KeptReason, NAV_PART_LABELS, NAV_SHAPE_COPY, NEUTRAL_STYLE_PROPS, NODE_BASELINES, NO_TOKENS, NavPagesSource, NavRole, NavShape, NavShapeError, PAGE_ARCHETYPES, PLAN_EDITS_ANSWER_SHAPES, PLAN_EDITS_PART_SCHEMA_NAMES, PLAN_EDIT_OP_PART, PLAN_EDIT_SHAPE, PLAN_EDIT_WIRE_KEYS, PLAN_PARTS, PLAN_PART_KEYS, PLAN_WIRE_DERIVED, PLAN_WIRE_PART_SCHEMA_NAMES, PLAN_WIRE_SHAPE, PRICING_PATTERN, PUBLISH_BLOCKING_PLACEHOLDERS, PageDataKind, PathIssueCollector, PatternDirective, PatternGlobalPosition, PatternOpens, PatternPlaceholder, PatternRecipe, PhoneMenuBreakpoint, PlaceholderKind, PlanChangeAction, PlanChangeKind, PlanDataSourceMode, PlanDataSourceType, PlanEditIssueCode, PlanEditOp, PlanEditsOutcome, PlanPart, PlanWireEntry, PlanWireShapeError, PlanWordIssueCode, PreflightRefusalCode, PublishRefusalCode, REQUIRED_CONTENT_ROLES, RelativeOp, SAMPLE_IMAGE_REF, SITE_CHECKS, SITE_FACT_LABELS, SITE_HEADER_PATTERN, SITE_LIBRARY, SITE_LOCALES, SITE_NEED_IDS, SITE_PATTERNS, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_ARCHETYPES, SPEC_SITE_COPY_LENGTHS, SPEC_SITE_DENSITY_COPY, SPEC_SITE_FACTS, SPEC_SITE_LIBRARY, SPEC_SITE_LOCALES, SPEC_SITE_NEED_IDS, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_REQUIRED_ROLES, SPEC_SITE_ROLES, SPEC_SITE_TEMPLATES, STOCK_FAMILIES, STOCK_PROVIDER_TERMS, STRICT_SCHEMA_BUDGET, STRICT_SCHEMA_LIMITS, STUDIO_TEMPLATE, SiteBuildKind, SiteBuildStatus, SiteCheckId, SiteFindingCode, SiteLocale, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, StepState, StockProviderId, addBarItem, addStockCredits, aiStepParts, applyPlanEdits, applyStockPhotos, archetypeIssues, assertStyleSlots, assertValidSitePlan, auditSite, barIconForRoute, blocksPublish, buildKeys, buildStepFacts, builtHomePageId, composeNavHeader, composeSection, composeSite, contentTypePreviewFields, contentTypeToWire, copyLengthOf, costUsd, createSerializedNode, creditsOfUsd, dataSourceToWire, derivedDataSources, describePlanData, describeShapeKey, entranceAction, entranceInteraction, estimateStepCredits, expandPlanWire, expandWireContentType, expandWireDataSource, expandWireEdit, expandWireField, expandWirePage, expandWireSection, failedBuildStep, fieldToWire, findNavRoot, isActiveBuild, isNonEmptyString, isPlainObject, isRtlLocale, jsonAnyOf, jsonBoolean, jsonClosedObject, jsonInteger, jsonListOf, jsonOneOf, jsonSchemaKeywords, jsonSchemaObjects, jsonShapedObject, jsonText, modelCanDo, moveBarItem, navPages, navShapeBreakpoint, navShapeOf, nextSourceOf, nodeBindPaths, pageToWire, parsePatternPath, parseSampleImageRef, pathAt, patternById, patternHasForm, patternLinkSlots, patternListGap, patternManifest, patternNeedsBinding, patternNodes, patternOptionalParts, patternRoleNeeds, patternScopes, patternSlotBudgets, patternSlots, patternTakesBinding, phoneMenuPages, planEditOps, planEditToWire, planEditWireGaps, planEditWireKeys, planEditWireOps, planEditWireShape, planEditsFromWire, planEditsJsonSchema, planEditsToWire, planEntrances, planFromTemplate, planNavShape, planPartOfPath, planPartToWire, planStockPhotos, planToWire, planWireGaps, planWirePartJsonSchema, planWirePartShape, planWordIssues, publishRefusal, publishVerdict, recipeListBinds, releasedLayoutDefaults, removeBarItem, requiredAcknowledgements, requiredShapeKeys, roleField, routeMatches, routeParams, sampleImageRef, sectionEntrances, sectionPatternIds, sectionToWire, setStyleProps, shapeKeys, showcaseGlobals, showcasePages, showcasePlans, siteNavPages, slotBudget, stockCopyKey, stockCreditLine, stockCreditLines, stockCreditNode, stockCreditNodeId, stockSlots, strictSchemaComplexity, strictSchemaRefusal, typicalStepTokens, validatePlanEdits, validateSitePlan, walkSerialized, wireContentTypeJsonSchema, wireDataSourceJsonSchema, wireFieldJsonSchema, wirePageJsonSchema, wireSectionJsonSchema, withDerivedSources, wordPattern, writtenTexts };
15301
15441
  //# sourceMappingURL=vectoriox-iox-ui-generation-core.mjs.map