@vectoriox/iox-ui 5.67.0 → 5.69.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 +149 -24
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-seo-core.mjs +2 -2
- package/fesm2022/vectoriox-iox-ui-seo-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +2 -2
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/generation-core/index.d.ts +44 -6
- package/package.json +1 -1
- package/src/generation-core/compose-site.ts +23 -4
- package/src/generation-core/plan-wire.ts +22 -12
- package/src/generation-core/plan-words.ts +3 -1
- package/src/generation-core/site-plan.model.ts +35 -0
- package/src/generation-core/site-spec.generated.ts +5 -4
- package/src/generation-core/stock-photos.ts +37 -0
- package/src/generation-core/validate-site-plan.ts +17 -3
- package/src/seo-core/schema.ts +2 -2
|
@@ -69,6 +69,7 @@ declare const SPEC_SITE_PLAN_SHAPE: {
|
|
|
69
69
|
readonly count: "integer ≥ 0";
|
|
70
70
|
readonly sample: "boolean — items are flagged isSample";
|
|
71
71
|
readonly isPublished: "boolean";
|
|
72
|
+
readonly "perItem?": "per field, one string per sample: real content that wins over the template";
|
|
72
73
|
readonly "[field]?": "the field's value — strings are templates: {n} = the item's 1-based number, {sampleImage:n} / {sampleImage:n+k} = a bundled sample image";
|
|
73
74
|
};
|
|
74
75
|
readonly cmsDataSource: {
|
|
@@ -3624,11 +3625,11 @@ declare const SPEC_SITE_TEMPLATES: {
|
|
|
3624
3625
|
}, {
|
|
3625
3626
|
readonly name: "cover";
|
|
3626
3627
|
readonly type: "string";
|
|
3627
|
-
readonly controller: "
|
|
3628
|
+
readonly controller: "media";
|
|
3628
3629
|
}, {
|
|
3629
3630
|
readonly name: "gallery";
|
|
3630
3631
|
readonly type: "array";
|
|
3631
|
-
readonly controller: "
|
|
3632
|
+
readonly controller: "gallery";
|
|
3632
3633
|
}, {
|
|
3633
3634
|
readonly name: "year";
|
|
3634
3635
|
readonly type: "string";
|
|
@@ -3793,7 +3794,7 @@ declare const SPEC_SITE_TEMPLATES: {
|
|
|
3793
3794
|
}, {
|
|
3794
3795
|
readonly name: "photo";
|
|
3795
3796
|
readonly type: "string";
|
|
3796
|
-
readonly controller: "
|
|
3797
|
+
readonly controller: "media";
|
|
3797
3798
|
}, {
|
|
3798
3799
|
readonly name: "price";
|
|
3799
3800
|
readonly type: "string";
|
|
@@ -3985,7 +3986,7 @@ declare const SPEC_SITE_TEMPLATES: {
|
|
|
3985
3986
|
}, {
|
|
3986
3987
|
readonly name: "photo";
|
|
3987
3988
|
readonly type: "string";
|
|
3988
|
-
readonly controller: "
|
|
3989
|
+
readonly controller: "media";
|
|
3989
3990
|
}];
|
|
3990
3991
|
readonly seed: {
|
|
3991
3992
|
readonly count: 6;
|
|
@@ -4336,6 +4337,28 @@ interface SiteBusiness {
|
|
|
4336
4337
|
readonly tagline?: string;
|
|
4337
4338
|
readonly facts?: Readonly<Partial<Record<FactKey, string>>>;
|
|
4338
4339
|
}
|
|
4340
|
+
/** How a content field is edited: the closed list a plan chooses from. */
|
|
4341
|
+
declare enum ContentFieldControl {
|
|
4342
|
+
Text = "text",
|
|
4343
|
+
Textarea = "textarea",
|
|
4344
|
+
/** One picture. `image` is the deprecated name of this control. */
|
|
4345
|
+
Media = "media",
|
|
4346
|
+
/** A list of pictures. */
|
|
4347
|
+
Gallery = "gallery",
|
|
4348
|
+
/** A list of words. */
|
|
4349
|
+
List = "list",
|
|
4350
|
+
Number = "number",
|
|
4351
|
+
Switch = "switch"
|
|
4352
|
+
}
|
|
4353
|
+
/** How a field is stored. The schema editor's and the item editor's own list: anything else renders no input. */
|
|
4354
|
+
declare enum ContentFieldType {
|
|
4355
|
+
String = "string",
|
|
4356
|
+
Number = "number",
|
|
4357
|
+
Boolean = "boolean",
|
|
4358
|
+
Array = "array"
|
|
4359
|
+
}
|
|
4360
|
+
/** ONE rule: how a field is stored follows from how it is edited, so a plan never has to say. */
|
|
4361
|
+
declare const FIELD_TYPE_OF_CONTROL: Readonly<Record<ContentFieldControl, ContentFieldType>>;
|
|
4339
4362
|
interface ContentFieldPlan {
|
|
4340
4363
|
readonly name: string;
|
|
4341
4364
|
readonly type: string;
|
|
@@ -4349,6 +4372,8 @@ interface ContentSeedPlan {
|
|
|
4349
4372
|
readonly count: number;
|
|
4350
4373
|
readonly sample: boolean;
|
|
4351
4374
|
readonly isPublished: boolean;
|
|
4375
|
+
/** Per field, one value per sample: real content where the items are about a subject anyone can know. Wins over the field's template. */
|
|
4376
|
+
readonly perItem?: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
4352
4377
|
readonly [field: string]: unknown;
|
|
4353
4378
|
}
|
|
4354
4379
|
/** The part a field plays (contract `site.roles`, AI plan L0-3): what a pattern binds instead of a field name, so one
|
|
@@ -6677,6 +6702,19 @@ interface StockItemSlot extends StockNeed {
|
|
|
6677
6702
|
field: string;
|
|
6678
6703
|
}
|
|
6679
6704
|
type StockTarget = StockSlot | StockItemSlot;
|
|
6705
|
+
/** Where the owner's photo goes: a slot of a composed section or the image field of a sample item. */
|
|
6706
|
+
type OwnPhotoPlacement = (Pick<StockSlot, 'nodeId' | 'owner'> | {
|
|
6707
|
+
item: Pick<StockItemSlot, 'contentType' | 'index' | 'field'>;
|
|
6708
|
+
}) & {
|
|
6709
|
+
url: string;
|
|
6710
|
+
};
|
|
6711
|
+
/** The owner's photos, in order, come before any stock or sample picture: sections never repeat one, and sample items go round again. */
|
|
6712
|
+
declare function planOwnPhotos(slots: ReadonlyArray<StockTarget>, urls: ReadonlyArray<string>): {
|
|
6713
|
+
placements: OwnPhotoPlacement[];
|
|
6714
|
+
rest: StockTarget[];
|
|
6715
|
+
};
|
|
6716
|
+
/** Each marked sample's gallery shows the owner's photos instead of bundled samples, each list starting where the last one stopped. */
|
|
6717
|
+
declare function applyOwnPhotoGalleries(plan: Pick<SitePlan, 'contentTypes'>, items: Readonly<Record<string, ReadonlyArray<Record<string, unknown>>>>, urls: ReadonlyArray<string>): number;
|
|
6680
6718
|
/** Where a photo may not appear twice: one page; one content type's samples (a list shows them together). */
|
|
6681
6719
|
declare function stockGroupOf(target: StockTarget): string;
|
|
6682
6720
|
/**
|
|
@@ -6735,5 +6773,5 @@ declare function stockCreditNode(index: number, credit: StockLink): SerializedNo
|
|
|
6735
6773
|
/** Append the credit lines to the footer root (once each). Returns how many it added. */
|
|
6736
6774
|
declare function addStockCredits(footer: SerializedNode, credits: ReadonlyArray<StockLink>): number;
|
|
6737
6775
|
|
|
6738
|
-
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_FAMILY_INTENTS, STOCK_INTENT_ORDER, STOCK_INTENT_SUBJECTS, STOCK_ITEM_ASPECT, STOCK_MIN_KEPT, STOCK_PROVIDER_TERMS, STOCK_SUBJECT_WORDS, STRICT_SCHEMA_BUDGET, STRICT_SCHEMA_LIMITS, STUDIO_TEMPLATE, SiteBuildKind, SiteBuildStatus, SiteCheckId, SiteFindingCode, SiteLocale, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, StepState, StockIntent, StockOrientation, StockProviderId, StockReviewFlag, StockReviewVerdict, StockSlotKind, StockSubject, addBarItem, addStockCredits, aiStepParts, applyPlanEdits, applyStockItemPhotos, applyStockPhotos, archetypeIssues, aspectRatioOf, 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, orientationOfRatio, 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, scoreStockPhoto, sectionEntrances, sectionPatternIds, sectionToWire, setStyleProps, shapeKeys, showcaseGlobals, showcasePages, showcasePlans, siteNavPages, slotBudget, stockCopyKey, stockCreditLine, stockCreditLines, stockCreditNode, stockCreditNodeId, stockFits, stockGroupOf, stockItemSlots, stockKept, stockPhotoRatio, stockPhotoSubject, stockPhotoUsable, stockPhotoWords, stockSlots, stockWord, stockWords, strictSchemaComplexity, strictSchemaRefusal, typicalStepTokens, validatePlanEdits, validateSitePlan, walkSerialized, wireContentTypeJsonSchema, wireDataSourceJsonSchema, wireFieldJsonSchema, wirePageJsonSchema, wireSectionJsonSchema, withDerivedSources, wordPattern, writtenTexts };
|
|
6739
|
-
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, StockItemSlot, StockLink, StockNeed, StockPhoto, StockPhotoTraits, StockPlacement, StockPlanOptions, StockProviderTerms, StockReview, StockSlot, StockTarget, StrictSchemaComplexity, StrictSchemaRefusal, TemplatePlanInput, UndoKept, UndoPreview, UndoRecord, WireOptions, WrittenText };
|
|
6776
|
+
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, ContentFieldControl, ContentFieldType, ContentRole, ContentTypeAction, CreatedKind, DANGLING_LINK_CODES, DEFAULT_PHONE_MENU_BREAKPOINT, DEFAULT_SITE_LOCALE, ENTRANCE_EASING, ENTRANCE_ORIGIN_LOOK, ENTRANCE_TRIGGER, EntrancePart, EntranceSide, FACT_KEYS, FIELD_TYPE_OF_CONTROL, 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_FAMILY_INTENTS, STOCK_INTENT_ORDER, STOCK_INTENT_SUBJECTS, STOCK_ITEM_ASPECT, STOCK_MIN_KEPT, STOCK_PROVIDER_TERMS, STOCK_SUBJECT_WORDS, STRICT_SCHEMA_BUDGET, STRICT_SCHEMA_LIMITS, STUDIO_TEMPLATE, SiteBuildKind, SiteBuildStatus, SiteCheckId, SiteFindingCode, SiteLocale, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, StepState, StockIntent, StockOrientation, StockProviderId, StockReviewFlag, StockReviewVerdict, StockSlotKind, StockSubject, addBarItem, addStockCredits, aiStepParts, applyOwnPhotoGalleries, applyPlanEdits, applyStockItemPhotos, applyStockPhotos, archetypeIssues, aspectRatioOf, 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, orientationOfRatio, 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, planOwnPhotos, planPartOfPath, planPartToWire, planStockPhotos, planToWire, planWireGaps, planWirePartJsonSchema, planWirePartShape, planWordIssues, publishRefusal, publishVerdict, recipeListBinds, releasedLayoutDefaults, removeBarItem, requiredAcknowledgements, requiredShapeKeys, roleField, routeMatches, routeParams, sampleImageRef, scoreStockPhoto, sectionEntrances, sectionPatternIds, sectionToWire, setStyleProps, shapeKeys, showcaseGlobals, showcasePages, showcasePlans, siteNavPages, slotBudget, stockCopyKey, stockCreditLine, stockCreditLines, stockCreditNode, stockCreditNodeId, stockFits, stockGroupOf, stockItemSlots, stockKept, stockPhotoRatio, stockPhotoSubject, stockPhotoUsable, stockPhotoWords, stockSlots, stockWord, stockWords, strictSchemaComplexity, strictSchemaRefusal, typicalStepTokens, validatePlanEdits, validateSitePlan, walkSerialized, wireContentTypeJsonSchema, wireDataSourceJsonSchema, wireFieldJsonSchema, wirePageJsonSchema, wireSectionJsonSchema, withDerivedSources, wordPattern, writtenTexts };
|
|
6777
|
+
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, OwnPhotoPlacement, 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, StockItemSlot, StockLink, StockNeed, StockPhoto, StockPhotoTraits, StockPlacement, StockPlanOptions, StockProviderTerms, StockReview, StockSlot, StockTarget, StrictSchemaComplexity, StrictSchemaRefusal, TemplatePlanInput, UndoKept, UndoPreview, UndoRecord, WireOptions, WrittenText };
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from './patterns';
|
|
17
17
|
import { recipeListBinds } from './recipe-fields';
|
|
18
18
|
import { assertStyleSlots, ComposeError, createSerializedNode, SerializedBinding, SerializedNode, setStyleProps, walkSerialized } from './serialized-node';
|
|
19
|
-
import {
|
|
19
|
+
import { SitePlanSourceKind, ContentFieldControl, ContentFieldPlan,
|
|
20
20
|
ContentRole, ContentTypePlan, DataSourcePlan, DEFAULT_SITE_LOCALE, FactKey, FACT_KEYS, GlobalSlot, isRtlLocale, PagePlan, PageSectionPlan, PlanDataSourceMode,
|
|
21
21
|
PlanDataSourceType, RelativeOp, roleField, SiteLocale, SitePlan, SitePlanSource, SITE_FACT_LABELS, SiteBusiness,
|
|
22
22
|
} from './site-plan.model';
|
|
@@ -1087,13 +1087,32 @@ function expandSeedValue(value: unknown, n: number): unknown {
|
|
|
1087
1087
|
.replace(SAMPLE_IMAGE_TEMPLATE, (_m, plus?: string) => sampleImageRef(n + (plus ? Number(plus) : 0)));
|
|
1088
1088
|
}
|
|
1089
1089
|
|
|
1090
|
-
|
|
1090
|
+
/** An AI plan's sample always has something to edit: a field it left empty gets a marked placeholder, never an invented fact. A template's blanks are deliberate. */
|
|
1091
|
+
function defaultSeed(type: ContentTypePlan, field: ContentFieldPlan): unknown {
|
|
1092
|
+
const word = field.name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ').toLowerCase();
|
|
1093
|
+
const defaults: Partial<Record<ContentFieldControl, unknown>> = {
|
|
1094
|
+
[ContentFieldControl.Text]: word === 'slug' ? `sample-${type.name}-{n}`.toLowerCase() : `Sample ${word} {n}`,
|
|
1095
|
+
[ContentFieldControl.Textarea]: `Sample ${word}: two lines the studio replaces.`,
|
|
1096
|
+
[ContentFieldControl.Media]: '{sampleImage:n}',
|
|
1097
|
+
[ContentFieldControl.Gallery]: ['{sampleImage:n}', '{sampleImage:n+1}', '{sampleImage:n+2}'],
|
|
1098
|
+
[ContentFieldControl.List]: [`Sample ${word} {n}`],
|
|
1099
|
+
};
|
|
1100
|
+
return defaults[field.controller as ContentFieldControl];
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function composeItems(type: ContentTypePlan, fillEmpty: boolean): ComposedItem[] {
|
|
1091
1104
|
const seed = type.seed;
|
|
1092
1105
|
if (!seed) return [];
|
|
1093
1106
|
const items: ComposedItem[] = [];
|
|
1094
1107
|
for (let n = 1; n <= seed.count; n++) {
|
|
1095
1108
|
const item: Record<string, unknown> = {};
|
|
1096
|
-
for (const field of type.fields)
|
|
1109
|
+
for (const field of type.fields) {
|
|
1110
|
+
const given = seed[field.name];
|
|
1111
|
+
const empty = !(field.name in seed) || given === '' || (Array.isArray(given) && !given.length);
|
|
1112
|
+
const own = seed.perItem?.[field.name]?.[n - 1];
|
|
1113
|
+
const value = own || (empty && fillEmpty ? defaultSeed(type, field) : given);
|
|
1114
|
+
if (value !== undefined) item[field.name] = expandSeedValue(value, n);
|
|
1115
|
+
}
|
|
1097
1116
|
items.push({ ...item, isPublished: seed.isPublished, isSample: seed.sample });
|
|
1098
1117
|
}
|
|
1099
1118
|
return items;
|
|
@@ -1155,7 +1174,7 @@ export function composeSite(plan: SitePlan, context: ComposeContext): ComposedSi
|
|
|
1155
1174
|
business: clone(plan.business),
|
|
1156
1175
|
design,
|
|
1157
1176
|
schemas: clone(plan.contentTypes) as ContentTypePlan[],
|
|
1158
|
-
items: Object.fromEntries(plan.contentTypes.map(t => [t.name, composeItems(t)])),
|
|
1177
|
+
items: Object.fromEntries(plan.contentTypes.map(t => [t.name, composeItems(t, plan.source.kind === SitePlanSourceKind.Ai)])),
|
|
1159
1178
|
dataSources: plan.dataSources.map(composeDataSource),
|
|
1160
1179
|
pages,
|
|
1161
1180
|
globals,
|
|
@@ -4,7 +4,7 @@ import { FORM_PRESETS } from '@vectoriox/iox-ui/forms-core';
|
|
|
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
6
|
import { withDerivedSources } from './plan-derived';
|
|
7
|
-
import {
|
|
7
|
+
import { ContentFieldControl, FIELD_TYPE_OF_CONTROL,
|
|
8
8
|
ContentFieldPlan, ContentTypePlan, DataSourcePlan, FactKey, GlobalSlot, PAGE_ARCHETYPES, PagePlan, PageSectionPlan, PlanDataSourceMode, PlanDataSourceType,
|
|
9
9
|
RelativeOp, SITE_NEED_IDS, SITE_PLAN_VERSION, SitePlan, SitePlanSource, SiteLocale,
|
|
10
10
|
} from './site-plan.model';
|
|
@@ -117,8 +117,7 @@ const WIRE_KEYS: Readonly<Record<`${PlanWireEntry}`, Readonly<Record<string, Wir
|
|
|
117
117
|
},
|
|
118
118
|
field: {
|
|
119
119
|
name: key('field.name'),
|
|
120
|
-
|
|
121
|
-
controller: key('field.controller'),
|
|
120
|
+
controller: { refs: ['field.controller'], text: `one of ${Object.values(ContentFieldControl).join(', ')} — how the field is edited (gallery is a list of pictures, list a list of words); how it is stored follows from this` },
|
|
122
121
|
required: { refs: ['field.required'], text: 'boolean — the field must be filled', none: 'false' },
|
|
123
122
|
note: key('field.note', "''"),
|
|
124
123
|
},
|
|
@@ -126,11 +125,12 @@ const WIRE_KEYS: Readonly<Record<`${PlanWireEntry}`, Readonly<Record<string, Wir
|
|
|
126
125
|
count: key('seed.count'),
|
|
127
126
|
sample: key('seed.sample'),
|
|
128
127
|
isPublished: key('seed.isPublished'),
|
|
129
|
-
values: { refs: ['seed.[field]'], text: "seedValue[] — one per field: the samples' value for it" },
|
|
128
|
+
values: { refs: ['seed.[field]'], text: "seedValue[] — one per field, for EVERY field: the samples' value for it, with {n} for the sample's number and {sampleImage:n} for a picture, words a person replaces" },
|
|
130
129
|
},
|
|
131
130
|
seedValue: {
|
|
132
131
|
field: { refs: ['seed.[field]'], text: 'string — a field of the type' },
|
|
133
132
|
value: { refs: ['seed.[field]'], text: `string | string[] — ${describeShapeKey(PLAN_SHAPE['seed'], '[field]')} (a list for an array field)` },
|
|
133
|
+
each: { refs: ['seed.perItem'], text: 'string[] — one value per sample, in order, when each sample has its own content', none: '[]' },
|
|
134
134
|
},
|
|
135
135
|
dataSource: {
|
|
136
136
|
alias: { refs: ['cmsDataSource.alias', 'relativeDataSource.alias'] },
|
|
@@ -185,6 +185,7 @@ const WIRE_KEYS: Readonly<Record<`${PlanWireEntry}`, Readonly<Record<string, Wir
|
|
|
185
185
|
|
|
186
186
|
/** The plan keys the model does not write, and where the expander takes each from. */
|
|
187
187
|
export const PLAN_WIRE_DERIVED: Readonly<Record<string, string>> = {
|
|
188
|
+
'field.type': 'follows from the field\'s control (FIELD_TYPE_OF_CONTROL)',
|
|
188
189
|
'plan.version': `always '${SITE_PLAN_VERSION}'`,
|
|
189
190
|
'plan.source': 'this run (the context)',
|
|
190
191
|
'plan.locale': "the reading's language (the context)",
|
|
@@ -260,7 +261,7 @@ const sectionPatterns = () => sectionPatternIds().map(id => SITE_PATTERNS[id]);
|
|
|
260
261
|
const globalIds = (position: `${PatternGlobalPosition}`) => Object.entries(SITE_PATTERNS).filter(([, p]) => p.global === position).map(([id]) => id);
|
|
261
262
|
|
|
262
263
|
export function wireFieldJsonSchema(): JsonSchema {
|
|
263
|
-
return shaped(PlanWireEntry.Field, { name: jsonText(),
|
|
264
|
+
return shaped(PlanWireEntry.Field, { name: jsonText(), controller: jsonOneOf(Object.values(ContentFieldControl)), required: jsonBoolean(), note: jsonText() });
|
|
264
265
|
}
|
|
265
266
|
|
|
266
267
|
/** What an object of the wire travels without when its children are written as their own entries (a plan edit's). */
|
|
@@ -284,7 +285,7 @@ const shapedAs = (entry: `${PlanWireEntry}`, options: WireOptions, properties: R
|
|
|
284
285
|
};
|
|
285
286
|
|
|
286
287
|
export function wireContentTypeJsonSchema(options: WireOptions = {}): JsonSchema {
|
|
287
|
-
const value = shaped(PlanWireEntry.SeedValue, { field: jsonText(), value: jsonAnyOf([jsonText(), jsonListOf(jsonText())]) });
|
|
288
|
+
const value = shaped(PlanWireEntry.SeedValue, { field: jsonText(), value: jsonAnyOf([jsonText(), jsonListOf(jsonText())]), each: jsonListOf(jsonText()) });
|
|
288
289
|
return shapedAs(PlanWireEntry.ContentType, options, {
|
|
289
290
|
name: jsonText(),
|
|
290
291
|
label: jsonText(),
|
|
@@ -400,7 +401,8 @@ const each = (list: unknown, expand: (v: unknown) => unknown) => (Array.isArray(
|
|
|
400
401
|
export function expandWireField(wire: unknown): ContentFieldPlan | unknown {
|
|
401
402
|
if (!isPlainObject(wire)) return wire;
|
|
402
403
|
const { required, note, ...rest } = wire;
|
|
403
|
-
|
|
404
|
+
const type = FIELD_TYPE_OF_CONTROL[rest['controller'] as ContentFieldControl];
|
|
405
|
+
return { ...rest, ...(type ? { type } : {}), ...optional('required', required, v => v === undefined || v === false), ...optional('note', note) };
|
|
404
406
|
}
|
|
405
407
|
|
|
406
408
|
export function expandWireContentType(wire: unknown, options: WireOptions = {}): ContentTypePlan | unknown {
|
|
@@ -411,8 +413,16 @@ export function expandWireContentType(wire: unknown, options: WireOptions = {}):
|
|
|
411
413
|
if (!isPlainObject(seed)) return seed === undefined ? out : { ...out, seed };
|
|
412
414
|
const { values, ...fixed } = seed;
|
|
413
415
|
if (fixed['count'] === 0 && emptyList(values)) return out;
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
+
const perItem: Record<string, unknown> = {};
|
|
417
|
+
const plain = Array.isArray(values) ? values.map(row => {
|
|
418
|
+
if (!isPlainObject(row)) return row;
|
|
419
|
+
const { each, ...rest } = row;
|
|
420
|
+
if (Array.isArray(each) && each.length) perItem[String(rest['field'])] = each;
|
|
421
|
+
return rest;
|
|
422
|
+
}) : values;
|
|
423
|
+
const map = listToMap(plain ?? [], 'field', 'value');
|
|
424
|
+
const items = Object.keys(perItem).length ? { perItem } : {};
|
|
425
|
+
return { ...out, seed: isPlainObject(map) ? { ...fixed, ...items, ...map } : { ...fixed, values } };
|
|
416
426
|
}
|
|
417
427
|
|
|
418
428
|
export function expandWireDataSource(wire: unknown): DataSourcePlan | unknown {
|
|
@@ -494,16 +504,16 @@ export function expandPlanWire(parts: PlanWireParts, context: PlanWireContext):
|
|
|
494
504
|
// ── The plan → the wire ────────────────────────────────────────────────────────────────────────────────────────────
|
|
495
505
|
|
|
496
506
|
export function fieldToWire(field: ContentFieldPlan): PlainObject {
|
|
497
|
-
return { name: field.name,
|
|
507
|
+
return { name: field.name, controller: field.controller, required: field.required ?? false, note: field.note ?? NONE };
|
|
498
508
|
}
|
|
499
509
|
|
|
500
510
|
export function contentTypeToWire(type: ContentTypePlan, options: WireOptions = {}): PlainObject {
|
|
501
511
|
const played = type.roles as Readonly<Record<string, string | undefined>>;
|
|
502
512
|
const roles = ROLE_KEYS.filter(role => played[role]).map(role => ({ role, field: played[role] }));
|
|
503
|
-
const { count = 0, sample = false, isPublished = false, ...values } = type.seed ?? {};
|
|
513
|
+
const { count = 0, sample = false, isPublished = false, perItem = {}, ...values } = type.seed ?? {};
|
|
504
514
|
return {
|
|
505
515
|
name: type.name, label: type.label, roles, ...(options.children === false ? {} : { fields: type.fields.map(fieldToWire) }),
|
|
506
|
-
seed: { count, sample, isPublished, values: mapToList(values, 'field', 'value') },
|
|
516
|
+
seed: { count, sample, isPublished, values: mapToList(values, 'field', 'value').map(row => ({ ...row, each: perItem[String(row['field'])] ?? [] })) },
|
|
507
517
|
};
|
|
508
518
|
}
|
|
509
519
|
|
|
@@ -45,7 +45,9 @@ export function writtenTexts(plan: SitePlan): WrittenText[] {
|
|
|
45
45
|
});
|
|
46
46
|
plan.contentTypes?.forEach((type, t) => {
|
|
47
47
|
add(`contentTypes[${t}].label`, type.label);
|
|
48
|
-
|
|
48
|
+
const { perItem, ...seed }: Record<string, unknown> & { perItem?: Readonly<Record<string, ReadonlyArray<string>>> } = type.seed ?? {};
|
|
49
|
+
for (const [field, list] of Object.entries(perItem ?? {})) list.forEach((v, k) => add(`contentTypes[${t}].seed.perItem.${field}[${k}]`, v));
|
|
50
|
+
for (const [field, value] of Object.entries(seed)) {
|
|
49
51
|
const values = Array.isArray(value) ? value : [value];
|
|
50
52
|
values.forEach((v, k) => add(`contentTypes[${t}].seed.${field}${Array.isArray(value) ? `[${k}]` : ''}`, v));
|
|
51
53
|
}
|
|
@@ -32,6 +32,39 @@ export interface SiteBusiness {
|
|
|
32
32
|
readonly facts?: Readonly<Partial<Record<FactKey, string>>>;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** How a content field is edited: the closed list a plan chooses from. */
|
|
36
|
+
export enum ContentFieldControl {
|
|
37
|
+
Text = 'text',
|
|
38
|
+
Textarea = 'textarea',
|
|
39
|
+
/** One picture. `image` is the deprecated name of this control. */
|
|
40
|
+
Media = 'media',
|
|
41
|
+
/** A list of pictures. */
|
|
42
|
+
Gallery = 'gallery',
|
|
43
|
+
/** A list of words. */
|
|
44
|
+
List = 'list',
|
|
45
|
+
Number = 'number',
|
|
46
|
+
Switch = 'switch',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** How a field is stored. The schema editor's and the item editor's own list: anything else renders no input. */
|
|
50
|
+
export enum ContentFieldType {
|
|
51
|
+
String = 'string',
|
|
52
|
+
Number = 'number',
|
|
53
|
+
Boolean = 'boolean',
|
|
54
|
+
Array = 'array',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** ONE rule: how a field is stored follows from how it is edited, so a plan never has to say. */
|
|
58
|
+
export const FIELD_TYPE_OF_CONTROL: Readonly<Record<ContentFieldControl, ContentFieldType>> = {
|
|
59
|
+
[ContentFieldControl.Text]: ContentFieldType.String,
|
|
60
|
+
[ContentFieldControl.Textarea]: ContentFieldType.String,
|
|
61
|
+
[ContentFieldControl.Media]: ContentFieldType.String,
|
|
62
|
+
[ContentFieldControl.Gallery]: ContentFieldType.Array,
|
|
63
|
+
[ContentFieldControl.List]: ContentFieldType.Array,
|
|
64
|
+
[ContentFieldControl.Number]: ContentFieldType.Number,
|
|
65
|
+
[ContentFieldControl.Switch]: ContentFieldType.Boolean,
|
|
66
|
+
};
|
|
67
|
+
|
|
35
68
|
export interface ContentFieldPlan {
|
|
36
69
|
readonly name: string;
|
|
37
70
|
readonly type: string;
|
|
@@ -46,6 +79,8 @@ export interface ContentSeedPlan {
|
|
|
46
79
|
readonly count: number;
|
|
47
80
|
readonly sample: boolean;
|
|
48
81
|
readonly isPublished: boolean;
|
|
82
|
+
/** Per field, one value per sample: real content where the items are about a subject anyone can know. Wins over the field's template. */
|
|
83
|
+
readonly perItem?: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
49
84
|
readonly [field: string]: unknown;
|
|
50
85
|
}
|
|
51
86
|
|
|
@@ -69,6 +69,7 @@ export const SPEC_SITE_PLAN_SHAPE = {
|
|
|
69
69
|
"count": "integer ≥ 0",
|
|
70
70
|
"sample": "boolean — items are flagged isSample",
|
|
71
71
|
"isPublished": "boolean",
|
|
72
|
+
"perItem?": "per field, one string per sample: real content that wins over the template",
|
|
72
73
|
"[field]?": "the field's value — strings are templates: {n} = the item's 1-based number, {sampleImage:n} / {sampleImage:n+k} = a bundled sample image"
|
|
73
74
|
},
|
|
74
75
|
"cmsDataSource": {
|
|
@@ -4248,12 +4249,12 @@ export const SPEC_SITE_TEMPLATES = {
|
|
|
4248
4249
|
{
|
|
4249
4250
|
"name": "cover",
|
|
4250
4251
|
"type": "string",
|
|
4251
|
-
"controller": "
|
|
4252
|
+
"controller": "media"
|
|
4252
4253
|
},
|
|
4253
4254
|
{
|
|
4254
4255
|
"name": "gallery",
|
|
4255
4256
|
"type": "array",
|
|
4256
|
-
"controller": "
|
|
4257
|
+
"controller": "gallery"
|
|
4257
4258
|
},
|
|
4258
4259
|
{
|
|
4259
4260
|
"name": "year",
|
|
@@ -4466,7 +4467,7 @@ export const SPEC_SITE_TEMPLATES = {
|
|
|
4466
4467
|
{
|
|
4467
4468
|
"name": "photo",
|
|
4468
4469
|
"type": "string",
|
|
4469
|
-
"controller": "
|
|
4470
|
+
"controller": "media"
|
|
4470
4471
|
},
|
|
4471
4472
|
{
|
|
4472
4473
|
"name": "price",
|
|
@@ -4702,7 +4703,7 @@ export const SPEC_SITE_TEMPLATES = {
|
|
|
4702
4703
|
{
|
|
4703
4704
|
"name": "photo",
|
|
4704
4705
|
"type": "string",
|
|
4705
|
-
"controller": "
|
|
4706
|
+
"controller": "media"
|
|
4706
4707
|
}
|
|
4707
4708
|
],
|
|
4708
4709
|
"seed": {
|
|
@@ -122,6 +122,43 @@ export interface StockItemSlot extends StockNeed {
|
|
|
122
122
|
|
|
123
123
|
export type StockTarget = StockSlot | StockItemSlot;
|
|
124
124
|
|
|
125
|
+
/** Where the owner's photo goes: a slot of a composed section or the image field of a sample item. */
|
|
126
|
+
export type OwnPhotoPlacement = (Pick<StockSlot, 'nodeId' | 'owner'> | { item: Pick<StockItemSlot, 'contentType' | 'index' | 'field'> }) & { url: string };
|
|
127
|
+
|
|
128
|
+
/** The owner's photos, in order, come before any stock or sample picture: sections never repeat one, and sample items go round again. */
|
|
129
|
+
export function planOwnPhotos(slots: ReadonlyArray<StockTarget>, urls: ReadonlyArray<string>): { placements: OwnPhotoPlacement[]; rest: StockTarget[] } {
|
|
130
|
+
if (!urls.length) return { placements: [], rest: [...slots] };
|
|
131
|
+
const placements: OwnPhotoPlacement[] = [];
|
|
132
|
+
const rest: StockTarget[] = [];
|
|
133
|
+
let next = 0;
|
|
134
|
+
for (const slot of slots) {
|
|
135
|
+
if (slot.kind === StockSlotKind.Section) {
|
|
136
|
+
if (next < urls.length) placements.push({ nodeId: slot.nodeId, owner: slot.owner, url: urls[next++] });
|
|
137
|
+
else rest.push(slot);
|
|
138
|
+
} else {
|
|
139
|
+
placements.push({ item: { contentType: slot.contentType, index: slot.index, field: slot.field }, url: urls[next++ % urls.length] });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { placements, rest };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Each marked sample's gallery shows the owner's photos instead of bundled samples, each list starting where the last one stopped. */
|
|
146
|
+
export function applyOwnPhotoGalleries(plan: Pick<SitePlan, 'contentTypes'>, items: Readonly<Record<string, ReadonlyArray<Record<string, unknown>>>>, urls: ReadonlyArray<string>): number {
|
|
147
|
+
if (!urls.length) return 0;
|
|
148
|
+
let changed = 0;
|
|
149
|
+
for (const type of plan.contentTypes) {
|
|
150
|
+
const field = roleField(type, ContentRole.Gallery);
|
|
151
|
+
if (!field) continue;
|
|
152
|
+
(items[type.name] ?? []).forEach((item, index) => {
|
|
153
|
+
const list = item[field];
|
|
154
|
+
if (!item['isSample'] || !Array.isArray(list)) return;
|
|
155
|
+
item[field] = list.map((value, i) => (parseSampleImageRef(value) === null ? value : urls[(index + i) % urls.length]));
|
|
156
|
+
changed++;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return changed;
|
|
160
|
+
}
|
|
161
|
+
|
|
125
162
|
/** Where a photo may not appear twice: one page; one content type's samples (a list shows them together). */
|
|
126
163
|
export function stockGroupOf(target: StockTarget): string {
|
|
127
164
|
return target.kind === StockSlotKind.Section ? `page:${target.owner}` : `items:${target.contentType}`;
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
BindScope, patternById, patternHasForm, patternLinkSlots, patternNeedsBinding, patternOptionalParts, patternRoleNeeds, patternTakesBinding, patternScopes,
|
|
7
7
|
patternSlots, PatternGlobalPosition, slotBudget,
|
|
8
8
|
} from './patterns';
|
|
9
|
-
import {
|
|
9
|
+
import { ContentFieldControl, FIELD_TYPE_OF_CONTROL,
|
|
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';
|
|
@@ -103,6 +103,7 @@ const ROLE_KEYS = shapeKeys(SHAPE['roles']);
|
|
|
103
103
|
const PAGE_SEO_KEYS = shapeKeys(SHAPE['pageSeo']);
|
|
104
104
|
const ARRAY_TYPE = 'array';
|
|
105
105
|
const FIELD_KEYS = shapeKeys(SHAPE['field']);
|
|
106
|
+
const SLUG_FIELD = 'slug';
|
|
106
107
|
const SEED_FIXED_KEYS = shapeKeys(SHAPE['seed']);
|
|
107
108
|
const CMS_SOURCE_KEYS = shapeKeys(SHAPE['cmsDataSource']);
|
|
108
109
|
const RELATIVE_SOURCE_KEYS = shapeKeys(SHAPE['relativeDataSource']);
|
|
@@ -208,8 +209,11 @@ function validateContentTypes(c: Collector, list: unknown[]): Map<string, Conten
|
|
|
208
209
|
const fp = `${path}.fields[${j}]`;
|
|
209
210
|
if (!isObject(f)) { c.add(SitePlanIssueCode.Shape, fp, 'must be an object'); return null; }
|
|
210
211
|
c.keys(f, FIELD_KEYS, fp);
|
|
211
|
-
c.string(f, '
|
|
212
|
-
c.string(f, '
|
|
212
|
+
const control = c.string(f, 'controller', fp);
|
|
213
|
+
const stored = c.string(f, 'type', fp);
|
|
214
|
+
const expected = control ? FIELD_TYPE_OF_CONTROL[control as ContentFieldControl] : undefined;
|
|
215
|
+
if (control && !expected) c.add(SitePlanIssueCode.Shape, `${fp}.controller`, `"${control}" is not a field control (${Object.values(ContentFieldControl).join(', ')})`);
|
|
216
|
+
if (expected && stored && stored !== expected) c.add(SitePlanIssueCode.Shape, `${fp}.type`, `a ${control} field is stored as ${expected}, not "${stored}"`);
|
|
213
217
|
c.boolean(f, 'required', fp);
|
|
214
218
|
return c.string(f, 'name', fp);
|
|
215
219
|
});
|
|
@@ -223,6 +227,16 @@ function validateContentTypes(c: Collector, list: unknown[]): Map<string, Conten
|
|
|
223
227
|
else {
|
|
224
228
|
if (!Number.isInteger(seed['count']) || (seed['count'] as number) < 0) c.add(SitePlanIssueCode.Shape, `${path}.seed.count`, 'must be a whole number');
|
|
225
229
|
for (const key of ['sample', 'isPublished']) if (typeof seed[key] !== 'boolean') c.add(SitePlanIssueCode.Shape, `${path}.seed.${key}`, 'must be a boolean');
|
|
230
|
+
const perItem = seed['perItem'];
|
|
231
|
+
if (perItem !== undefined) {
|
|
232
|
+
if (!isObject(perItem)) c.add(SitePlanIssueCode.Shape, `${path}.seed.perItem`, 'must be an object');
|
|
233
|
+
else for (const [field, list] of Object.entries(perItem)) {
|
|
234
|
+
const at = `${path}.seed.perItem.${field}`;
|
|
235
|
+
if (!fieldSet.has(field)) c.add(SitePlanIssueCode.UnknownField, at, `"${field}" is not a field of ${name}`);
|
|
236
|
+
else if (!Array.isArray(list) || list.length !== seed['count'] || list.some(v => typeof v !== 'string')) c.add(SitePlanIssueCode.Shape, at, `must be ${String(seed['count'])} strings, one per sample`);
|
|
237
|
+
else if (field === SLUG_FIELD && new Set(list).size !== list.length) c.add(SitePlanIssueCode.Shape, at, 'each sample needs its own slug');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
226
240
|
for (const key of Object.keys(seed)) {
|
|
227
241
|
if (!SEED_FIXED_KEYS.includes(key) && !fieldSet.has(key)) c.add(SitePlanIssueCode.UnknownField, `${path}.seed.${key}`, `"${key}" is not a field of ${name}`);
|
|
228
242
|
}
|
package/src/seo-core/schema.ts
CHANGED
|
@@ -72,7 +72,7 @@ export function buildSeoSchemaProperties(): Record<string, unknown> {
|
|
|
72
72
|
image: {
|
|
73
73
|
type: 'string',
|
|
74
74
|
label: 'OG Image',
|
|
75
|
-
controller: { type: '
|
|
75
|
+
controller: { type: 'media' },
|
|
76
76
|
},
|
|
77
77
|
},
|
|
78
78
|
},
|
|
@@ -102,7 +102,7 @@ export function buildSeoSchemaProperties(): Record<string, unknown> {
|
|
|
102
102
|
image: {
|
|
103
103
|
type: 'string',
|
|
104
104
|
label: 'Twitter Image',
|
|
105
|
-
controller: { type: '
|
|
105
|
+
controller: { type: 'media' },
|
|
106
106
|
},
|
|
107
107
|
},
|
|
108
108
|
},
|