@vectoriox/iox-ui 5.63.0 → 5.64.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/fesm2022/vectoriox-iox-ui-generation-core.mjs +143 -26
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/generation-core/index.d.ts +29 -5
- package/package.json +1 -1
- package/src/generation-core/compose-site.ts +21 -23
- package/src/generation-core/plan-derived.ts +84 -0
- package/src/generation-core/plan-edits.ts +8 -3
- package/src/generation-core/plan-wire.ts +9 -4
- package/src/generation-core/public-api.ts +2 -0
- package/src/generation-core/recipe-fields.ts +49 -0
- package/src/generation-core/validate-site-plan.ts +15 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AnimateId, DesignChoices, DesignTokenLike } from '@vectoriox/iox-ui/design-core';
|
|
2
2
|
import { SeoRawDoc } from '@vectoriox/iox-ui/seo-core';
|
|
3
|
+
import { ContentFieldLike } from '@vectoriox/iox-ui/composite-core';
|
|
3
4
|
|
|
4
5
|
declare const SPEC_SITE_PLAN_SHAPE: {
|
|
5
6
|
readonly plan: {
|
|
@@ -5744,6 +5745,10 @@ declare function buildStepFacts(plan: SitePlan, actions?: Readonly<Record<string
|
|
|
5744
5745
|
* - **What the model does not decide is not on the wire** (`PLAN_WIRE_DERIVED`): the look (the owner's pick), the run,
|
|
5745
5746
|
* the language (the reading's), the facts (the owner's own, verbatim), the version, the home page (the page at `/`).
|
|
5746
5747
|
* So the format is the same for every studio and every run — one compiled grammar, cached by the provider.
|
|
5748
|
+
* - **What follows from the pages is derived, never asked** (`plan-derived.ts`, vectoriox/iox-cms#49): a per-item page
|
|
5749
|
+
* reading the next item gets its `$siblings.next` source — the structure, asked first and then fixed, cannot know the
|
|
5750
|
+
* pages need it, and the pages cannot add a source. `planToWire` writes every source the plan holds, a derived one
|
|
5751
|
+
* too (it is the plan's: a change's brief shows it, and a plan → wire → plan round trip stays exact).
|
|
5747
5752
|
*
|
|
5748
5753
|
* ONE source: every wire key names the plan keys it carries (`refs`) and is described in the plan table's own words, and
|
|
5749
5754
|
* every plan key is either carried or derived (`planWireGaps()`); a key the plan shape gains breaks the builder until it
|
|
@@ -5827,7 +5832,7 @@ type PlanWireParts = Partial<Record<PlanPart, unknown>>;
|
|
|
5827
5832
|
* A model's plan (its parts, on the wire) + what the generation decides (the look, the run, the language, the owner's
|
|
5828
5833
|
* facts) → the Site Plan. Pure. Translates, never repairs: whatever an answer got wrong is still wrong, for
|
|
5829
5834
|
* `validateSitePlan` — a part that is not an object is the result itself, a part not written yet leaves its keys out
|
|
5830
|
-
* (the validator then names them, at their own paths).
|
|
5835
|
+
* (the validator then names them, at their own paths). What follows from the pages alone is added (`withDerivedSources`).
|
|
5831
5836
|
*/
|
|
5832
5837
|
declare function expandPlanWire(parts: PlanWireParts, context: PlanWireContext): SitePlan | unknown;
|
|
5833
5838
|
declare function fieldToWire(field: ContentFieldPlan): PlainObject;
|
|
@@ -6025,6 +6030,24 @@ declare enum AiCallStatus {
|
|
|
6025
6030
|
Interrupted = "interrupted"
|
|
6026
6031
|
}
|
|
6027
6032
|
|
|
6033
|
+
interface RecipeListBinds<T> {
|
|
6034
|
+
picks: T;
|
|
6035
|
+
/** What the fields lack (the recipe's `needs`), or null when they fill it. */
|
|
6036
|
+
gap: string | null;
|
|
6037
|
+
}
|
|
6038
|
+
/** The fields `recipe` binds from a listed type's `fields`, and what they lack. A recipe that lists nothing lacks nothing. */
|
|
6039
|
+
declare function recipeListBinds<T = Readonly<Record<string, string | null>>>(recipe: string | undefined, fields: ReadonlyArray<ContentFieldLike>): RecipeListBinds<T>;
|
|
6040
|
+
/** What a listed type with `fields` lacks for the pattern's recipes that repeat the section's binding — the first gap, or null. */
|
|
6041
|
+
declare function patternListGap(pattern: SitePattern, fields: ReadonlyArray<ContentFieldLike>): string | null;
|
|
6042
|
+
|
|
6043
|
+
/**
|
|
6044
|
+
* The data sources `plan` lacks and its pages need, in page order (appended after the plan's own). Tolerates any
|
|
6045
|
+
* value: what is not a well-formed plan gains nothing, for the validator to judge as written.
|
|
6046
|
+
*/
|
|
6047
|
+
declare function derivedDataSources(plan: unknown): DataSourcePlan[];
|
|
6048
|
+
/** `plan` with the data sources its pages need appended (`derivedDataSources`); the same object when it lacks none. */
|
|
6049
|
+
declare function withDerivedSources<T>(plan: T): T;
|
|
6050
|
+
|
|
6028
6051
|
/**
|
|
6029
6052
|
* What a structured-output request costs the provider's grammar compiler, counted the way the Claude API counts it
|
|
6030
6053
|
* (platform.claude.com/docs/en/build-with-claude/structured-outputs, "Schema complexity limits", read 2026-10-27):
|
|
@@ -6234,8 +6257,9 @@ declare function validatePlanEdits(edits: unknown): {
|
|
|
6234
6257
|
};
|
|
6235
6258
|
/**
|
|
6236
6259
|
* Apply a set of edits to a plan — all of them, or none (any issue refuses the set and returns the plan unchanged). The
|
|
6237
|
-
* sources only removed pages and sections read are dropped with them; sources nothing read before are kept
|
|
6238
|
-
*
|
|
6260
|
+
* sources only removed pages and sections read are dropped with them; sources nothing read before are kept; the sources
|
|
6261
|
+
* the pages now need and no model writes are derived (`derivedDataSources` — a page edit adding `project.next`, as the
|
|
6262
|
+
* plan wire's expansion does, vectoriox/iox-cms#49). The result still has to pass `validateSitePlan`.
|
|
6239
6263
|
*/
|
|
6240
6264
|
declare function applyPlanEdits(plan: SitePlan, edits: ReadonlyArray<PlanEdit>): PlanEditResult;
|
|
6241
6265
|
/** Each part's answer format name. */
|
|
@@ -6554,5 +6578,5 @@ declare function stockCreditNode(index: number, credit: StockLink): SerializedNo
|
|
|
6554
6578
|
/** Append the credit lines to the footer root (once each). Returns how many it added. */
|
|
6555
6579
|
declare function addStockCredits(footer: SerializedNode, credits: ReadonlyArray<StockLink>): number;
|
|
6556
6580
|
|
|
6557
|
-
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 };
|
|
6558
|
-
export type { AddBarItemResult, AiEntitlementsView, AiMeter, AiModelEntry, AiRefusal, AiTokenCounts, AiTokenPrice, AuditPage, AuditPlaceholder, BarItemsEditResult, BuildNotice, BuildStepError, BuildStepFacts, BuildStepRecord, CmsDataSourcePlan, ComposeContext, ComposeIssue, ComposedDataSource, ComposedItem, ComposedNavHeader, ComposedPage, ComposedPlaceholder, ComposedRoute, ComposedSection, ComposedSite, ContentFieldPlan, ContentRoles, ContentSeedPlan, ContentTypePlan, CreateNodeOptions, CreatedRecord, DataSourcePlan, DynamicKeysAsList, EntranceAction, EntranceActionOptions, EntranceInteraction, EntranceMark, EntranceSpec, EntranceStamp, FactKey, JsonSchema, NavHeaderInput, NavHeaderOptions, NavPage, NavShapeOptions, NavShapeResult, NodeBaseline, PageArchetype, PageArchetypeId, PageArchetypeSlot, PageData, PageDataLine, PagePlan, PageSectionPlan, PageSeoPlan, PathIssue, PatternManifestEntry, PatternNode, PatternPath, PatternRoleNeed, PlainObject, PlanChange, PlanEdit, PlanEditIssue, PlanEditResult, PlanEditsAnswer, PlanEntrancesInput, PlanWireContext, PlanWireParts, PlanWordContext, PlanWordIssue, PreflightRefusal, PublishRefusal, RelativeDataSourcePlan, SectionEntrances, SectionInput, SectionOptions, SerializedBinding, SerializedNode, SerializedResponsive, ShapeEntry, ShapeIssueCodes, ShowcaseInput, SiteAudit, SiteAuditOptions, SiteAuditSnapshot, SiteBuildRun, SiteBusiness, SiteCheck, SiteFinding, SiteNeedId, SitePageRef, SitePattern, SitePlan, SitePlanIssue, SitePlanSource, SitePlanValidation, SiteTemplate, StockCredit, StockLink, StockPhoto, StockPlacement, StockProviderTerms, StockSlot, StrictSchemaComplexity, StrictSchemaRefusal, TemplatePlanInput, UndoKept, UndoPreview, UndoRecord, WireOptions, WrittenText };
|
|
6581
|
+
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 };
|
|
6582
|
+
export type { AddBarItemResult, AiEntitlementsView, AiMeter, AiModelEntry, AiRefusal, AiTokenCounts, AiTokenPrice, AuditPage, AuditPlaceholder, BarItemsEditResult, BuildNotice, BuildStepError, BuildStepFacts, BuildStepRecord, CmsDataSourcePlan, ComposeContext, ComposeIssue, ComposedDataSource, ComposedItem, ComposedNavHeader, ComposedPage, ComposedPlaceholder, ComposedRoute, ComposedSection, ComposedSite, ContentFieldPlan, ContentRoles, ContentSeedPlan, ContentTypePlan, CreateNodeOptions, CreatedRecord, DataSourcePlan, DynamicKeysAsList, EntranceAction, EntranceActionOptions, EntranceInteraction, EntranceMark, EntranceSpec, EntranceStamp, FactKey, JsonSchema, NavHeaderInput, NavHeaderOptions, NavPage, NavShapeOptions, NavShapeResult, NodeBaseline, PageArchetype, PageArchetypeId, PageArchetypeSlot, PageData, PageDataLine, PagePlan, PageSectionPlan, PageSeoPlan, PathIssue, PatternManifestEntry, PatternNode, PatternPath, PatternRoleNeed, PlainObject, PlanChange, PlanEdit, PlanEditIssue, PlanEditResult, PlanEditsAnswer, PlanEntrancesInput, PlanWireContext, PlanWireParts, PlanWordContext, PlanWordIssue, PreflightRefusal, PublishRefusal, RecipeListBinds, RelativeDataSourcePlan, SectionEntrances, SectionInput, SectionOptions, SerializedBinding, SerializedNode, SerializedResponsive, ShapeEntry, ShapeIssueCodes, ShowcaseInput, SiteAudit, SiteAuditOptions, SiteAuditSnapshot, SiteBuildRun, SiteBusiness, SiteCheck, SiteFinding, SiteNeedId, SitePageRef, SitePattern, SitePlan, SitePlanIssue, SitePlanSource, SitePlanValidation, SiteTemplate, StockCredit, StockLink, StockPhoto, StockPlacement, StockProviderTerms, StockSlot, StrictSchemaComplexity, StrictSchemaRefusal, TemplatePlanInput, UndoKept, UndoPreview, UndoRecord, WireOptions, WrittenText };
|
package/package.json
CHANGED
|
@@ -4,9 +4,9 @@ import { FormPresetDef, FormPresetField, formPresetById } from '@vectoriox/iox-u
|
|
|
4
4
|
import {
|
|
5
5
|
ACCORDION_BIND_PROP, ACCORDION_CONTENT_LIMIT, ACCORDION_WORDS, accordionAnswerOf, accordionItems, accordionQuestionOf, AccordionRole,
|
|
6
6
|
buildAccordionRecipe, buildCardRecipe, buildCarouselRecipe, buildLightboxRecipe, CardLayout, CardPart, CardRole, CARD_LAYOUTS, CARD_PARTS, CompositeNode,
|
|
7
|
-
CompositeNodeSpec, findByRole, LIGHTBOX_LABEL,
|
|
7
|
+
AccordionAutoBinds, CompositeNodeSpec, findByRole, LIGHTBOX_LABEL, SLIDE_TO_ROW, SLIDE_TYPE, buildTabsRecipe, tabLabelNodeOf,
|
|
8
8
|
tabPanelImageOf, tabPanelTextOf, tabsListOf, tabsPanels, tabsPanelsOf, tabsTriggers, TABS_CONTENT_LIMIT, TABS_IMAGE_BIND_PROP, TABS_LABEL_BIND_PROP,
|
|
9
|
-
TABS_TEXT_BIND_PROP, TABS_WORDS, BadgeTone, BADGE_TONES, buildSegmentedRecipe, CardWords, ITEM_OPEN_STATE, segmentedOptions,
|
|
9
|
+
TABS_TEXT_BIND_PROP, TABS_WORDS, TabsAutoBinds, BadgeTone, BADGE_TONES, buildSegmentedRecipe, CardWords, ITEM_OPEN_STATE, segmentedOptions,
|
|
10
10
|
} from '@vectoriox/iox-ui/composite-core';
|
|
11
11
|
|
|
12
12
|
import { EntranceMark, EntrancePart, planEntrances } from './entrances';
|
|
@@ -14,6 +14,7 @@ import { NODE_BASELINES } from './node-baselines.generated';
|
|
|
14
14
|
import {
|
|
15
15
|
BindScope, CardBindTarget, parsePatternPath, PatternDirective, PatternNode, PatternOpens, PatternPlaceholder, PatternRecipe, SitePattern, patternById,
|
|
16
16
|
} from './patterns';
|
|
17
|
+
import { recipeListBinds } from './recipe-fields';
|
|
17
18
|
import { assertStyleSlots, ComposeError, createSerializedNode, SerializedBinding, SerializedNode, setStyleProps, walkSerialized } from './serialized-node';
|
|
18
19
|
import {
|
|
19
20
|
ContentRole, ContentTypePlan, DataSourcePlan, DEFAULT_SITE_LOCALE, FactKey, FACT_KEYS, GlobalSlot, isRtlLocale, PagePlan, PageSectionPlan, PlanDataSourceMode,
|
|
@@ -838,6 +839,15 @@ function composeSegmented(pn: PatternNode, id: string): SerializedNode {
|
|
|
838
839
|
return root;
|
|
839
840
|
}
|
|
840
841
|
|
|
842
|
+
/** The content type a section binding lists, through its data source. */
|
|
843
|
+
function boundContentType(binding: string, ctx: Ctx): ContentTypePlan | undefined {
|
|
844
|
+
const source = ctx.plan.dataSources.find(d => d.alias === binding);
|
|
845
|
+
return source && 'contentType' in source ? ctx.plan.contentTypes.find(t => t.name === source.contentType) : undefined;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** A type's fields as composite-core's pickers read them. */
|
|
849
|
+
const fieldsOf = (type: ContentTypePlan | undefined) => (type?.fields ?? []).map(f => ({ name: f.name, type: f.type, controller: f.controller }));
|
|
850
|
+
|
|
841
851
|
/**
|
|
842
852
|
* An Accordion (CC2) — the composite-core recipe stamped with this composer's factory. Manual (no section
|
|
843
853
|
* binding): three items whose questions and answers are placeholders the studio rewrites (`ph: 'copy'`), never
|
|
@@ -864,26 +874,17 @@ function composeAccordion(pn: PatternNode, id: string, ctx: Ctx): SerializedNode
|
|
|
864
874
|
}
|
|
865
875
|
return root;
|
|
866
876
|
}
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
const picks = pickAccordionAutoBinds((type?.fields ?? []).map(f => ({ name: f.name, type: f.type, controller: f.controller })));
|
|
870
|
-
if (!picks.question || !picks.answer) {
|
|
871
|
-
throw new ComposeError(`${id}: "${binding}" has no short-text field for the question and long-text field for the answer`);
|
|
872
|
-
}
|
|
877
|
+
const { picks, gap } = recipeListBinds<AccordionAutoBinds>(pn.recipe, fieldsOf(boundContentType(binding, ctx)));
|
|
878
|
+
if (gap) throw new ComposeError(`${id}: "${binding}" has no ${gap}`);
|
|
873
879
|
addBinding(root, { prop: 'source', source: binding, path: '' });
|
|
874
880
|
setProp(root, 'limit', ACCORDION_CONTENT_LIMIT);
|
|
875
881
|
const [template] = accordionItems(root as CompositeNode);
|
|
876
882
|
delete template.itemKey;
|
|
877
|
-
addBinding(accordionQuestionOf(template) as SerializedNode, { prop: ACCORDION_BIND_PROP, source: binding, path: picks.question });
|
|
878
|
-
addBinding(accordionAnswerOf(template) as SerializedNode, { prop: ACCORDION_BIND_PROP, source: binding, path: picks.answer });
|
|
883
|
+
addBinding(accordionQuestionOf(template) as SerializedNode, { prop: ACCORDION_BIND_PROP, source: binding, path: picks.question! });
|
|
884
|
+
addBinding(accordionAnswerOf(template) as SerializedNode, { prop: ACCORDION_BIND_PROP, source: binding, path: picks.answer! });
|
|
879
885
|
return root;
|
|
880
886
|
}
|
|
881
887
|
|
|
882
|
-
/** The content type a section binding lists, through its data source. */
|
|
883
|
-
function boundContentType(binding: string, ctx: Ctx): ContentTypePlan | undefined {
|
|
884
|
-
const source = ctx.plan.dataSources.find(d => d.alias === binding);
|
|
885
|
-
return source && 'contentType' in source ? ctx.plan.contentTypes.find(t => t.name === source.contentType) : undefined;
|
|
886
|
-
}
|
|
887
888
|
|
|
888
889
|
const TABS_ROOT_KEY = 'tabs';
|
|
889
890
|
const TABS_HOLDER_KEYS: ReadonlySet<string> = new Set(['list', 'panels']);
|
|
@@ -920,11 +921,8 @@ function composeTabs(pn: PatternNode, id: string, ctx: Ctx): SerializedNode {
|
|
|
920
921
|
}
|
|
921
922
|
return root;
|
|
922
923
|
}
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
if (!picks.label || !picks.text) {
|
|
926
|
-
throw new ComposeError(`${id}: "${binding}" has no short-text field for the tab and long-text field for its panel`);
|
|
927
|
-
}
|
|
924
|
+
const { picks, gap } = recipeListBinds<TabsAutoBinds>(pn.recipe, fieldsOf(boundContentType(binding, ctx)));
|
|
925
|
+
if (gap) throw new ComposeError(`${id}: "${binding}" has no ${gap}`);
|
|
928
926
|
for (const holder of [tabsListOf(tree), tabsPanelsOf(tree)] as SerializedNode[]) {
|
|
929
927
|
addBinding(holder, { prop: 'source', source: binding, path: '' });
|
|
930
928
|
setProp(holder, 'limit', TABS_CONTENT_LIMIT);
|
|
@@ -933,12 +931,12 @@ function composeTabs(pn: PatternNode, id: string, ctx: Ctx): SerializedNode {
|
|
|
933
931
|
const [panel] = tabsPanels(tree);
|
|
934
932
|
delete tab.itemKey;
|
|
935
933
|
delete panel.itemKey;
|
|
936
|
-
addBinding(tabLabelNodeOf(tab) as SerializedNode, { prop: TABS_LABEL_BIND_PROP, source: binding, path: picks.label });
|
|
937
|
-
addBinding(tabPanelTextOf(panel) as SerializedNode, { prop: TABS_TEXT_BIND_PROP, source: binding, path: picks.text });
|
|
934
|
+
addBinding(tabLabelNodeOf(tab) as SerializedNode, { prop: TABS_LABEL_BIND_PROP, source: binding, path: picks.label! });
|
|
935
|
+
addBinding(tabPanelTextOf(panel) as SerializedNode, { prop: TABS_TEXT_BIND_PROP, source: binding, path: picks.text! });
|
|
938
936
|
const image = tabPanelImageOf(panel) as SerializedNode | undefined;
|
|
939
937
|
if (image && picks.image) {
|
|
940
938
|
addBinding(image, { prop: TABS_IMAGE_BIND_PROP, source: binding, path: picks.image });
|
|
941
|
-
addBinding(image, { prop: 'alt', source: binding, path: picks.label });
|
|
939
|
+
addBinding(image, { prop: 'alt', source: binding, path: picks.label! });
|
|
942
940
|
}
|
|
943
941
|
return root;
|
|
944
942
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { BindScope, patternById, patternScopes } from './patterns';
|
|
2
|
+
import { DataSourcePlan, PlanDataSourceMode, PlanDataSourceType, RelativeOp } from './site-plan.model';
|
|
3
|
+
import { isNonEmptyString, isPlainObject, PlainObject } from './shape-check';
|
|
4
|
+
import { nextSourceOf } from './validate-site-plan';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What a plan reads but no model is asked to write (vectoriox/iox-cms#49): the data sources that follow from the pages
|
|
8
|
+
* alone. A per-item page whose sections read the NEXT item (`project.next`) needs a `$siblings.next` source of its route
|
|
9
|
+
* source — a fact of the pages, while data sources are the plan's STRUCTURE, asked first and fixed once accepted. Asking
|
|
10
|
+
* the structure for it made the pages' answer unfixable by construction (a re-ask of the pages cannot add a source), so
|
|
11
|
+
* it is derived, the same way for the plan wire's expansion (`expandPlanWire`) and for a sentence's page edits
|
|
12
|
+
* (`applyPlanEdits`). Pure and idempotent: a plan that already holds the source gains nothing; a source is derived only
|
|
13
|
+
* where it is valid by construction (its subject a single cms source of a type of the plan), so it never adds an issue
|
|
14
|
+
* at a structure path. Its siblings are walked within an unlimited list of the same type — the structure's first, else a
|
|
15
|
+
* derived one.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const NEXT_SUFFIX = 'Next';
|
|
19
|
+
const LIST_SUFFIX = 'All';
|
|
20
|
+
|
|
21
|
+
interface SourceView { type: string; mode?: string; contentType?: string; limit?: unknown; of?: string; op?: string; }
|
|
22
|
+
|
|
23
|
+
/** The sources by alias, as the validator reads them (the first of an alias wins). */
|
|
24
|
+
function sourceViews(list: ReadonlyArray<unknown>): Map<string, SourceView> {
|
|
25
|
+
const views = new Map<string, SourceView>();
|
|
26
|
+
for (const raw of list) {
|
|
27
|
+
if (isPlainObject(raw) && isNonEmptyString(raw['alias']) && !views.has(raw['alias'])) views.set(raw['alias'], raw as unknown as SourceView);
|
|
28
|
+
}
|
|
29
|
+
return views;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const readsNext = (page: PlainObject) => Array.isArray(page['sections']) && page['sections'].some(s => {
|
|
33
|
+
const pattern = isPlainObject(s) && typeof s['pattern'] === 'string' ? patternById(s['pattern']) : undefined;
|
|
34
|
+
return !!pattern && patternScopes(pattern).has(BindScope.Next);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/** The alias itself, else the first of `<alias>2`, `<alias>3`… no source has. */
|
|
38
|
+
function freeAlias(base: string, taken: ReadonlyMap<string, unknown>): string {
|
|
39
|
+
let alias = base;
|
|
40
|
+
for (let n = 2; taken.has(alias); n++) alias = `${base}${n}`;
|
|
41
|
+
return alias;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The unlimited list of `contentType` the plan holds (its first), or null. */
|
|
45
|
+
function fullListOf(views: ReadonlyMap<string, SourceView>, contentType: string): string | null {
|
|
46
|
+
for (const [alias, s] of views) {
|
|
47
|
+
if (s.type === PlanDataSourceType.Cms && s.mode === PlanDataSourceMode.Query && s.contentType === contentType && s.limit === undefined) return alias;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The data sources `plan` lacks and its pages need, in page order (appended after the plan's own). Tolerates any
|
|
54
|
+
* value: what is not a well-formed plan gains nothing, for the validator to judge as written.
|
|
55
|
+
*/
|
|
56
|
+
export function derivedDataSources(plan: unknown): DataSourcePlan[] {
|
|
57
|
+
if (!isPlainObject(plan) || !Array.isArray(plan['dataSources']) || !Array.isArray(plan['pages']) || !Array.isArray(plan['contentTypes'])) return [];
|
|
58
|
+
const types = new Set(plan['contentTypes'].map(t => (isPlainObject(t) ? t['name'] : undefined)).filter(isNonEmptyString));
|
|
59
|
+
const views = sourceViews(plan['dataSources']);
|
|
60
|
+
const derived: DataSourcePlan[] = [];
|
|
61
|
+
const add = (source: DataSourcePlan) => { derived.push(source); views.set(source.alias, source); };
|
|
62
|
+
for (const page of plan['pages']) {
|
|
63
|
+
if (!isPlainObject(page) || !isNonEmptyString(page['routeSource']) || !readsNext(page)) continue;
|
|
64
|
+
const subject = page['routeSource'];
|
|
65
|
+
const route = views.get(subject);
|
|
66
|
+
if (!route || route.type !== PlanDataSourceType.Cms || route.mode !== PlanDataSourceMode.Single) continue;
|
|
67
|
+
if (!isNonEmptyString(route.contentType) || !types.has(route.contentType) || nextSourceOf(views, subject)) continue;
|
|
68
|
+
let within = fullListOf(views, route.contentType);
|
|
69
|
+
if (!within) {
|
|
70
|
+
within = freeAlias(`${subject}${LIST_SUFFIX}`, views);
|
|
71
|
+
add({ alias: within, type: PlanDataSourceType.Cms, mode: PlanDataSourceMode.Query, contentType: route.contentType });
|
|
72
|
+
}
|
|
73
|
+
add({ alias: freeAlias(`${subject}${NEXT_SUFFIX}`, views), type: PlanDataSourceType.Relative, of: subject, op: RelativeOp.SiblingsNext, within });
|
|
74
|
+
}
|
|
75
|
+
return derived;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** `plan` with the data sources its pages need appended (`derivedDataSources`); the same object when it lacks none. */
|
|
79
|
+
export function withDerivedSources<T>(plan: T): T {
|
|
80
|
+
const derived = derivedDataSources(plan);
|
|
81
|
+
if (!derived.length) return plan;
|
|
82
|
+
const own = plan as unknown as PlainObject;
|
|
83
|
+
return { ...own, dataSources: [...(own['dataSources'] as unknown[]), ...derived] } as T;
|
|
84
|
+
}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
jsonAnyOf, jsonInteger, jsonListOf, jsonOneOf, JsonSchema, jsonShapedObject, jsonText,
|
|
3
3
|
} from './json-schema';
|
|
4
4
|
import { patternById, patternOptionalParts, patternSlots, patternTakesBinding } from './patterns';
|
|
5
|
+
import { derivedDataSources } from './plan-derived';
|
|
5
6
|
import {
|
|
6
7
|
contentTypeToWire, dataSourceToWire, expandWireContentType, expandWireDataSource, expandWireField, expandWirePage, expandWireSection, fieldToWire,
|
|
7
8
|
pageToWire, PLAN_PARTS, PlanPart, PlanWireShapeError, sectionPatternIds, sectionToWire, wireContentTypeJsonSchema, wireDataSourceJsonSchema,
|
|
@@ -367,8 +368,9 @@ const APPLY: Readonly<Record<`${PlanEditOp}`, Apply>> = {
|
|
|
367
368
|
|
|
368
369
|
/**
|
|
369
370
|
* Apply a set of edits to a plan — all of them, or none (any issue refuses the set and returns the plan unchanged). The
|
|
370
|
-
* sources only removed pages and sections read are dropped with them; sources nothing read before are kept
|
|
371
|
-
*
|
|
371
|
+
* sources only removed pages and sections read are dropped with them; sources nothing read before are kept; the sources
|
|
372
|
+
* the pages now need and no model writes are derived (`derivedDataSources` — a page edit adding `project.next`, as the
|
|
373
|
+
* plan wire's expansion does, vectoriox/iox-cms#49). The result still has to pass `validateSitePlan`.
|
|
372
374
|
*/
|
|
373
375
|
export function applyPlanEdits(plan: SitePlan, edits: ReadonlyArray<PlanEdit>): PlanEditResult {
|
|
374
376
|
const structure = validatePlanEdits(edits);
|
|
@@ -384,8 +386,11 @@ export function applyPlanEdits(plan: SitePlan, edits: ReadonlyArray<PlanEdit>):
|
|
|
384
386
|
const added = new Set(changed.filter(ch => ch.kind === PlanChangeKind.DataSource).map(ch => ch.name));
|
|
385
387
|
const dataSources = draft.dataSources.filter(s => after.has(s.alias) || !before.has(s.alias) || added.has(s.alias));
|
|
386
388
|
for (const s of draft.dataSources) if (!dataSources.includes(s)) changed.push({ kind: PlanChangeKind.DataSource, action: PlanChangeAction.Removed, name: s.alias });
|
|
389
|
+
const edited = { ...plan, pages: draft.pages, contentTypes: draft.contentTypes, dataSources };
|
|
390
|
+
const derived = derivedDataSources(edited);
|
|
391
|
+
for (const s of derived) changed.push({ kind: PlanChangeKind.DataSource, action: PlanChangeAction.Added, name: s.alias });
|
|
387
392
|
|
|
388
|
-
return { plan: { ...
|
|
393
|
+
return { plan: derived.length ? { ...edited, dataSources: [...dataSources, ...derived] } : edited, changed, issues: [] };
|
|
389
394
|
}
|
|
390
395
|
|
|
391
396
|
// ── The format a model writes edits in ─────────────────────────────────────────────────────────────────
|
|
@@ -3,6 +3,7 @@ import { FORM_PRESETS } from '@vectoriox/iox-ui/forms-core';
|
|
|
3
3
|
|
|
4
4
|
import { describeShapeKey, jsonAnyOf, jsonBoolean, jsonInteger, jsonListOf, jsonOneOf, JsonSchema, jsonShapedObject, jsonText } from './json-schema';
|
|
5
5
|
import { patternLinkSlots, patternOptionalParts, patternSlots, PatternGlobalPosition, SITE_PATTERNS } from './patterns';
|
|
6
|
+
import { withDerivedSources } from './plan-derived';
|
|
6
7
|
import {
|
|
7
8
|
ContentFieldPlan, ContentTypePlan, DataSourcePlan, FactKey, GlobalSlot, PAGE_ARCHETYPES, PagePlan, PageSectionPlan, PlanDataSourceMode, PlanDataSourceType,
|
|
8
9
|
RelativeOp, SITE_NEED_IDS, SITE_PLAN_VERSION, SitePlan, SitePlanSource, SiteLocale,
|
|
@@ -30,6 +31,10 @@ import { isPlainObject, PlainObject, ShapeEntry, shapeKeys } from './shape-check
|
|
|
30
31
|
* - **What the model does not decide is not on the wire** (`PLAN_WIRE_DERIVED`): the look (the owner's pick), the run,
|
|
31
32
|
* the language (the reading's), the facts (the owner's own, verbatim), the version, the home page (the page at `/`).
|
|
32
33
|
* So the format is the same for every studio and every run — one compiled grammar, cached by the provider.
|
|
34
|
+
* - **What follows from the pages is derived, never asked** (`plan-derived.ts`, vectoriox/iox-cms#49): a per-item page
|
|
35
|
+
* reading the next item gets its `$siblings.next` source — the structure, asked first and then fixed, cannot know the
|
|
36
|
+
* pages need it, and the pages cannot add a source. `planToWire` writes every source the plan holds, a derived one
|
|
37
|
+
* too (it is the plan's: a change's brief shows it, and a plan → wire → plan round trip stays exact).
|
|
33
38
|
*
|
|
34
39
|
* ONE source: every wire key names the plan keys it carries (`refs`) and is described in the plan table's own words, and
|
|
35
40
|
* every plan key is either carried or derived (`planWireGaps()`); a key the plan shape gains breaks the builder until it
|
|
@@ -91,7 +96,7 @@ const WIRE_KEYS: Readonly<Record<`${PlanWireEntry}`, Readonly<Record<string, Wir
|
|
|
91
96
|
plan: {
|
|
92
97
|
business: inPart(PlanPart.Structure, key('plan.business')),
|
|
93
98
|
contentTypes: inPart(PlanPart.Structure, key('plan.contentTypes')),
|
|
94
|
-
dataSources: inPart(PlanPart.Structure, { refs: ['plan.dataSources'], text:
|
|
99
|
+
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" }),
|
|
95
100
|
pages: inPart(PlanPart.Pages, key('plan.pages')),
|
|
96
101
|
globals: inPart(PlanPart.Pages, key('plan.globals')),
|
|
97
102
|
},
|
|
@@ -465,7 +470,7 @@ export type PlanWireParts = Partial<Record<PlanPart, unknown>>;
|
|
|
465
470
|
* A model's plan (its parts, on the wire) + what the generation decides (the look, the run, the language, the owner's
|
|
466
471
|
* facts) → the Site Plan. Pure. Translates, never repairs: whatever an answer got wrong is still wrong, for
|
|
467
472
|
* `validateSitePlan` — a part that is not an object is the result itself, a part not written yet leaves its keys out
|
|
468
|
-
* (the validator then names them, at their own paths).
|
|
473
|
+
* (the validator then names them, at their own paths). What follows from the pages alone is added (`withDerivedSources`).
|
|
469
474
|
*/
|
|
470
475
|
export function expandPlanWire(parts: PlanWireParts, context: PlanWireContext): SitePlan | unknown {
|
|
471
476
|
const written = PLAN_PARTS.filter(part => parts[part] !== undefined).map(part => parts[part]);
|
|
@@ -473,7 +478,7 @@ export function expandPlanWire(parts: PlanWireParts, context: PlanWireContext):
|
|
|
473
478
|
if (odd !== undefined) return odd;
|
|
474
479
|
const wire: PlainObject = Object.assign({}, ...(written as PlainObject[]));
|
|
475
480
|
const { business, contentTypes, dataSources, pages, ...rest } = wire;
|
|
476
|
-
return {
|
|
481
|
+
return withDerivedSources({
|
|
477
482
|
...rest,
|
|
478
483
|
version: SITE_PLAN_VERSION,
|
|
479
484
|
source: { ...context.source },
|
|
@@ -483,7 +488,7 @@ export function expandPlanWire(parts: PlanWireParts, context: PlanWireContext):
|
|
|
483
488
|
contentTypes: each(contentTypes, expandWireContentType),
|
|
484
489
|
dataSources: each(dataSources, expandWireDataSource),
|
|
485
490
|
pages: each(pages, expandWirePage),
|
|
486
|
-
};
|
|
491
|
+
});
|
|
487
492
|
}
|
|
488
493
|
|
|
489
494
|
// ── The plan → the wire ────────────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -27,6 +27,8 @@ export * from './plan-data';
|
|
|
27
27
|
export * from './showcase';
|
|
28
28
|
export * from './site-build-run';
|
|
29
29
|
export * from './ai-run';
|
|
30
|
+
export * from './recipe-fields';
|
|
31
|
+
export * from './plan-derived';
|
|
30
32
|
export * from './plan-wire';
|
|
31
33
|
export * from './strict-schema';
|
|
32
34
|
export * from './plan-edits';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ContentFieldLike, pickAccordionAutoBinds, pickTabsAutoBinds } from '@vectoriox/iox-ui/composite-core';
|
|
2
|
+
|
|
3
|
+
import { PatternDirective, patternNodes, PatternRecipe, SitePattern } from './patterns';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The fields a recipe repeating a section's list binds (Accordion, Tabs — R3's From content): composite-core's pickers,
|
|
7
|
+
* the builder switch's rule, decide them. ONE rule for the composer (which binds them) and the gate (which refuses,
|
|
8
|
+
* at the section's `binding`, a list whose type cannot fill the recipe — vectoriox/iox-cms#49: it used to surface only
|
|
9
|
+
* as the composer's error at the plan's root, where the pages part could not tell which section to change).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface ListRecipe {
|
|
13
|
+
picks(fields: ReadonlyArray<ContentFieldLike>): Readonly<Record<string, string | null>>;
|
|
14
|
+
/** The picks the recipe cannot go without. */
|
|
15
|
+
required: ReadonlyArray<string>;
|
|
16
|
+
/** What the listed type must have, in words. */
|
|
17
|
+
needs: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const LIST_RECIPES: Readonly<Partial<Record<`${PatternRecipe}`, ListRecipe>>> = {
|
|
21
|
+
[PatternRecipe.Accordion]: { picks: f => ({ ...pickAccordionAutoBinds(f) }), required: ['question', 'answer'], needs: 'short-text field for the question and long-text field for the answer' },
|
|
22
|
+
[PatternRecipe.Tabs]: { picks: f => ({ ...pickTabsAutoBinds(f) }), required: ['label', 'text'], needs: 'short-text field for the tab and long-text field for its panel' },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface RecipeListBinds<T> {
|
|
26
|
+
picks: T;
|
|
27
|
+
/** What the fields lack (the recipe's `needs`), or null when they fill it. */
|
|
28
|
+
gap: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The fields `recipe` binds from a listed type's `fields`, and what they lack. A recipe that lists nothing lacks nothing. */
|
|
32
|
+
export function recipeListBinds<T = Readonly<Record<string, string | null>>>(recipe: string | undefined, fields: ReadonlyArray<ContentFieldLike>): RecipeListBinds<T> {
|
|
33
|
+
const spec = recipe ? LIST_RECIPES[recipe as `${PatternRecipe}`] : undefined;
|
|
34
|
+
if (!spec) return { picks: {} as T, gap: null };
|
|
35
|
+
const picks = spec.picks(fields);
|
|
36
|
+
return { picks: picks as unknown as T, gap: spec.required.every(key => picks[key]) ? null : spec.needs };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const listsBinding = (src: string | undefined) => src === PatternDirective.Binding || src === PatternDirective.OptionalBinding;
|
|
40
|
+
|
|
41
|
+
/** What a listed type with `fields` lacks for the pattern's recipes that repeat the section's binding — the first gap, or null. */
|
|
42
|
+
export function patternListGap(pattern: SitePattern, fields: ReadonlyArray<ContentFieldLike>): string | null {
|
|
43
|
+
for (const node of patternNodes(pattern)) {
|
|
44
|
+
if (!listsBinding(node.src)) continue;
|
|
45
|
+
const { gap } = recipeListBinds(node.recipe, fields);
|
|
46
|
+
if (gap) return gap;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
FACT_KEYS, GlobalSlot, PAGE_ARCHETYPES, SITE_NEED_IDS, PlanDataSourceMode, PlanDataSourceType, RelativeOp, REQUIRED_CONTENT_ROLES, SitePlan, SitePlanSourceKind,
|
|
11
11
|
SITE_LOCALES, SITE_PLAN_VERSION,
|
|
12
12
|
} from './site-plan.model';
|
|
13
|
+
import { patternListGap } from './recipe-fields';
|
|
13
14
|
import { SPEC_SITE_PLAN_SHAPE } from './site-spec.generated';
|
|
14
15
|
import {
|
|
15
16
|
isNonEmptyString, isPlainObject as isObject, PathIssue, PathIssueCollector, pathAt as at, PlainObject as Obj, requiredShapeKeys, ShapeEntry, shapeKeys,
|
|
@@ -187,7 +188,12 @@ function validateDesign(c: Collector, design: unknown): void {
|
|
|
187
188
|
if (design['fonts'] !== undefined && !isObject(design['fonts'])) bad('fonts', 'must be an object');
|
|
188
189
|
}
|
|
189
190
|
|
|
190
|
-
interface ContentTypeInfo {
|
|
191
|
+
interface ContentTypeInfo {
|
|
192
|
+
fields: Set<string>;
|
|
193
|
+
/** The fields as a recipe's pickers read them (`recipe-fields`). */
|
|
194
|
+
described: Array<{ name: string; type?: string; controller?: string }>;
|
|
195
|
+
roles: Record<string, string>;
|
|
196
|
+
}
|
|
191
197
|
|
|
192
198
|
function validateContentTypes(c: Collector, list: unknown[]): Map<string, ContentTypeInfo> {
|
|
193
199
|
const types = new Map<string, ContentTypeInfo>();
|
|
@@ -223,11 +229,13 @@ function validateContentTypes(c: Collector, list: unknown[]): Map<string, Conten
|
|
|
223
229
|
}
|
|
224
230
|
}
|
|
225
231
|
if (name && types.has(name)) c.add(SitePlanIssueCode.Duplicate, `${path}.name`, `content type "${name}" is used twice`);
|
|
226
|
-
else if (name) types.set(name, { fields: fieldSet, roles });
|
|
232
|
+
else if (name) types.set(name, { fields: fieldSet, described: fields.filter(isObject).map(describeField), roles });
|
|
227
233
|
});
|
|
228
234
|
return types;
|
|
229
235
|
}
|
|
230
236
|
|
|
237
|
+
const describeField = (f: Obj) => ({ name: String(f['name']), type: typeof f['type'] === 'string' ? f['type'] : undefined, controller: typeof f['controller'] === 'string' ? f['controller'] : undefined });
|
|
238
|
+
|
|
231
239
|
/** A content type's roles (L0-3): known roles only, each played by a field of the type; `title` always; `gallery` a list. */
|
|
232
240
|
function validateRoles(c: Collector, roles: unknown, path: string, typeName: string | null, fields: Set<string>, fieldTypes: Map<unknown, unknown>): Record<string, string> {
|
|
233
241
|
if (!isObject(roles)) { c.add(SitePlanIssueCode.Shape, path, 'must be an object (which field plays each role)'); return {}; }
|
|
@@ -396,6 +404,11 @@ function validatePages(c: Collector, list: unknown[], types: Map<string, Content
|
|
|
396
404
|
const src = sources.get(binding);
|
|
397
405
|
if (!src) c.add(SitePlanIssueCode.UnknownDataSource, `${sp}.binding`, `no data source "${binding}"`);
|
|
398
406
|
else if (src.type !== PlanDataSourceType.Cms || src.mode !== PlanDataSourceMode.Query) c.add(SitePlanIssueCode.Binding, `${sp}.binding`, `"${binding}" is not a list source`);
|
|
407
|
+
else {
|
|
408
|
+
const listed = src.contentType ? types.get(src.contentType) : undefined;
|
|
409
|
+
const gap = listed ? patternListGap(pattern, listed.described) : null;
|
|
410
|
+
if (gap) 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'}`);
|
|
411
|
+
}
|
|
399
412
|
}
|
|
400
413
|
} else if (binding) c.add(SitePlanIssueCode.Binding, `${sp}.binding`, `"${patternId}" repeats nothing — it takes no binding`);
|
|
401
414
|
|