@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.
@@ -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: "image";
3628
+ readonly controller: "media";
3628
3629
  }, {
3629
3630
  readonly name: "gallery";
3630
3631
  readonly type: "array";
3631
- readonly controller: "mediaList";
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: "image";
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: "image";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.67.0",
3
+ "version": "5.69.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -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
- function composeItems(type: ContentTypePlan): ComposedItem[] {
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) if (field.name in seed) item[field.name] = expandSeedValue(seed[field.name], n);
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
- type: key('field.type'),
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(), type: jsonText(), controller: jsonText(), required: jsonBoolean(), note: 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
- return { ...rest, ...optional('required', required, v => v === undefined || v === false), ...optional('note', note) };
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 map = listToMap(values ?? [], 'field', 'value');
415
- return { ...out, seed: isPlainObject(map) ? { ...fixed, ...map } : { ...fixed, values } };
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, type: field.type, controller: field.controller, required: field.required ?? false, note: field.note ?? NONE };
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
- for (const [field, value] of Object.entries(type.seed ?? {})) {
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": "image"
4252
+ "controller": "media"
4252
4253
  },
4253
4254
  {
4254
4255
  "name": "gallery",
4255
4256
  "type": "array",
4256
- "controller": "mediaList"
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": "image"
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": "image"
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, 'type', fp);
212
- c.string(f, 'controller', fp);
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
  }
@@ -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: 'image' },
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: 'image' },
105
+ controller: { type: 'media' },
106
106
  },
107
107
  },
108
108
  },