riverbankcms 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { A as parseFooterCompositionV1, Ht as ok, K as parseUtcIsoDateTimeString, M as isCanonicalRichTextDoc, P as isPresent, Vt as err, X as parseBookingTimeZone, b as stableJsonStringify, k as footerNavigationLayouts, l as EVENT_PRESET_VALUES, lt as assertNever, n as NAVIGATION_LOCATIONS, s as resolveSiteVerticalBookingConfig, u as ORDINARY_EVENT_PRESET_VALUES } from "./src-DbmhanBI.mjs";
2
+ import { A as parseFooterCompositionV1, Ht as err, M as isCanonicalRichTextDoc, P as isPresent, Ut as ok, Z as parseBookingTimeZone, b as stableJsonStringify, k as footerNavigationLayouts, l as EVENT_PRESET_VALUES, n as NAVIGATION_LOCATIONS, q as parseUtcIsoDateTimeString, s as resolveSiteVerticalBookingConfig, u as ORDINARY_EVENT_PRESET_VALUES, ut as assertNever } from "./src-DIAVNRwo.mjs";
3
3
  import "node:module";
4
4
  import { z } from "zod";
5
5
 
@@ -61,41 +61,6 @@ function siteCommandResourceResult(ref, resourceId, effect) {
61
61
  };
62
62
  }
63
63
 
64
- //#endregion
65
- //#region ../site-commands/src/capabilityGaps.ts
66
- const staticExecutionGapCodes = [
67
- "event_occurrence_required",
68
- "event_occurrence_invalid",
69
- "event_occurrence_venue_mapping_unsupported",
70
- "event_booking_offer_invalid",
71
- "event_ticket_single_occurrence_scope_unsupported",
72
- "event_manual_payment_unsupported",
73
- "event_external_payment_unsupported",
74
- "event_waitlist_requires_waitlist_adapter",
75
- "event_media_content_requires_media_adapter",
76
- "event_linked_content_requires_content_adapter",
77
- "event_delivery_mode_requires_booking_surface_adapter",
78
- "event_whole_series_page_fields_unsupported",
79
- "event_whole_series_passes_unsupported",
80
- "event_whole_series_pricing_unsupported",
81
- "event_whole_series_schedule_update_unsupported",
82
- "event_whole_series_staff_update_unsupported"
83
- ];
84
- const plannerCapabilityGapCodes = ["command_metadata_support_gap"];
85
- const adapterCapabilityGapCodes = [
86
- "demo_recipe_adapter_missing",
87
- "media_manifest_storage_resolution_missing",
88
- "navigation_content_entry_refs_unsupported",
89
- "staff_site_user_requires_dashboard_user",
90
- "team_member_profile_requires_dashboard_user",
91
- "generated_navigation_command_unsupported"
92
- ];
93
- const siteCommandCapabilityGapCodes = [
94
- ...staticExecutionGapCodes,
95
- ...plannerCapabilityGapCodes,
96
- ...adapterCapabilityGapCodes
97
- ];
98
-
99
64
  //#endregion
100
65
  //#region ../site-commands/src/refs.ts
101
66
  const resourceKinds = [
@@ -123,6 +88,7 @@ const resourceKinds = [
123
88
  "pass-redemption",
124
89
  "order",
125
90
  "appointment-service",
91
+ "appointment-service-category",
126
92
  "appointment-resource",
127
93
  "availability"
128
94
  ];
@@ -167,6 +133,9 @@ function createVenueRef(parts) {
167
133
  function createEventCategoryRef(parts) {
168
134
  return `event-category:${joinRefSlugParts(parts)}`;
169
135
  }
136
+ function createAppointmentServiceCategoryRef(parts) {
137
+ return `appointment-service-category:${joinRefSlugParts(parts)}`;
138
+ }
170
139
  function createEventSeriesRef(parts) {
171
140
  return `event:${joinRefSlugParts(parts)}`;
172
141
  }
@@ -245,6 +214,118 @@ function isValidIanaTimezone(value) {
245
214
  }
246
215
  }
247
216
 
217
+ //#endregion
218
+ //#region ../site-commands/src/appointmentServiceCategoryCommands.ts
219
+ const appointmentServiceCategoryRefSchema = resourceRefSchema("appointment-service-category");
220
+ /**
221
+ * Creates or updates one appointment-service category (issue 1403).
222
+ *
223
+ * The category's `identifier` is its stable key: it never changes after the
224
+ * category is created, so a rename in the dashboard doesn't break a rerun.
225
+ * When a command leaves it out, the identifier is the ref's slug, which keeps
226
+ * reruns of the same command pointed at the same category.
227
+ *
228
+ * `description` and `sortOrder` describe the wanted state. An omitted
229
+ * description means no description. An omitted `sortOrder` puts a new
230
+ * category last and leaves an existing one where it is.
231
+ */
232
+ const upsertAppointmentServiceCategoryCommandSchema = z.object({
233
+ type: z.literal("upsertAppointmentServiceCategory"),
234
+ ref: appointmentServiceCategoryRefSchema,
235
+ input: z.object({
236
+ identifier: recipeIdentifierSchema.optional(),
237
+ name: z.string().trim().min(1),
238
+ description: z.string().trim().nullable().optional(),
239
+ sortOrder: nonNegativeIntegerSchema.optional()
240
+ }).strict()
241
+ }).strict();
242
+ /**
243
+ * The three ways a service can be placed among the categories. Each variant
244
+ * forbids the other key, so a command naming both a `categoryRef` and a
245
+ * `categoryIdentifier` can't be represented.
246
+ */
247
+ const noAppointmentServiceCategoryFields = {
248
+ categoryRef: z.undefined().optional(),
249
+ categoryIdentifier: z.undefined().optional()
250
+ };
251
+ const appointmentServiceCategoryRefFields = {
252
+ categoryRef: appointmentServiceCategoryRefSchema,
253
+ categoryIdentifier: z.undefined().optional()
254
+ };
255
+ const appointmentServiceCategoryIdentifierFields = {
256
+ categoryRef: z.undefined().optional(),
257
+ categoryIdentifier: recipeIdentifierSchema
258
+ };
259
+ /**
260
+ * Rejects a command that names both category keys with one clear message,
261
+ * before the union reports that no variant matched.
262
+ */
263
+ function rejectBothAppointmentServiceCategoryKeys(value, ctx) {
264
+ if (typeof value !== "object" || value === null || !("categoryRef" in value) || !("categoryIdentifier" in value) || value.categoryRef === undefined || value.categoryIdentifier === undefined) {
265
+ return;
266
+ }
267
+ ctx.addIssue({
268
+ code: "custom",
269
+ path: ["categoryIdentifier"],
270
+ message: "Use categoryRef for a category upserted in this batch, or categoryIdentifier for one that already exists, not both."
271
+ });
272
+ }
273
+ /** Where an `upsertAppointmentService` command puts the service. */
274
+ function appointmentServiceCategoryPlacement(input) {
275
+ if (input.categoryRef !== undefined) {
276
+ return {
277
+ kind: "batch_ref",
278
+ categoryRef: input.categoryRef
279
+ };
280
+ }
281
+ if (input.categoryIdentifier !== undefined) {
282
+ return {
283
+ kind: "identifier",
284
+ identifier: input.categoryIdentifier
285
+ };
286
+ }
287
+ return { kind: "uncategorized" };
288
+ }
289
+ /** The stable key an `upsertAppointmentServiceCategory` command writes to. */
290
+ function appointmentServiceCategoryIdentifierForCommand(command) {
291
+ return command.input.identifier ?? refSlug(command.ref);
292
+ }
293
+
294
+ //#endregion
295
+ //#region ../site-commands/src/capabilityGaps.ts
296
+ const staticExecutionGapCodes = [
297
+ "event_occurrence_required",
298
+ "event_occurrence_invalid",
299
+ "event_occurrence_venue_mapping_unsupported",
300
+ "event_booking_offer_invalid",
301
+ "event_ticket_single_occurrence_scope_unsupported",
302
+ "event_manual_payment_unsupported",
303
+ "event_external_payment_unsupported",
304
+ "event_waitlist_requires_waitlist_adapter",
305
+ "event_media_content_requires_media_adapter",
306
+ "event_linked_content_requires_content_adapter",
307
+ "event_delivery_mode_requires_booking_surface_adapter",
308
+ "event_whole_series_page_fields_unsupported",
309
+ "event_whole_series_passes_unsupported",
310
+ "event_whole_series_pricing_unsupported",
311
+ "event_whole_series_schedule_update_unsupported",
312
+ "event_whole_series_staff_update_unsupported"
313
+ ];
314
+ const plannerCapabilityGapCodes = ["command_metadata_support_gap"];
315
+ const adapterCapabilityGapCodes = [
316
+ "demo_recipe_adapter_missing",
317
+ "media_manifest_storage_resolution_missing",
318
+ "navigation_content_entry_refs_unsupported",
319
+ "staff_site_user_requires_dashboard_user",
320
+ "team_member_profile_requires_dashboard_user",
321
+ "generated_navigation_command_unsupported"
322
+ ];
323
+ const siteCommandCapabilityGapCodes = [
324
+ ...staticExecutionGapCodes,
325
+ ...plannerCapabilityGapCodes,
326
+ ...adapterCapabilityGapCodes
327
+ ];
328
+
248
329
  //#endregion
249
330
  //#region ../site-commands/src/bookingCommandValues.ts
250
331
  const bookingPassProductUseCases = ["event_pass", "appointment_package"];
@@ -2449,8 +2530,8 @@ const BACKGROUND_ROLE_NAMES = new Set([
2449
2530
  "muted",
2450
2531
  "neutral"
2451
2532
  ]);
2452
- function onColorTokenName(backgroundToken) {
2453
- return `on${backgroundToken.split("-").filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}`;
2533
+ function onColorTokenName(backgroundToken$1) {
2534
+ return `on${backgroundToken$1.split("-").filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}`;
2454
2535
  }
2455
2536
  function isOnColorBackgroundCandidateToken(token) {
2456
2537
  const role = token.match(/^(.*?)(?:-\d{2,3})?$/)?.[1] ?? token;
@@ -2584,8 +2665,8 @@ function resolveDeterministicForeground(backgroundReference, candidates, palette
2584
2665
  if (!backgroundHex) {
2585
2666
  return resolveTrustedTreatmentForeground(candidates, palette);
2586
2667
  }
2587
- const backgroundToken = paletteToken(backgroundReference);
2588
- const derivedOnToken = backgroundToken ? onColorTokenName(backgroundToken) : null;
2668
+ const backgroundToken$1 = paletteToken(backgroundReference);
2669
+ const derivedOnToken = backgroundToken$1 ? onColorTokenName(backgroundToken$1) : null;
2589
2670
  const text = firstSafeToken([
2590
2671
  candidates.text,
2591
2672
  derivedOnToken,
@@ -4616,16 +4697,18 @@ const themeV2RuntimeStyleFieldNames = [
4616
4697
  "_frameStyles",
4617
4698
  "_sectionStyles",
4618
4699
  "_containerStyles",
4619
- "_cardStyles",
4620
- "_transitionStyles",
4621
- "styleGroup"
4700
+ "_cardStyles"
4622
4701
  ];
4623
4702
  const themeV2BlockSpecificRawStyleFieldNames = [
4624
4703
  "_contentCardStyles",
4625
4704
  "_heroLayoutStyles",
4626
4705
  "_textCardStyles"
4627
4706
  ];
4628
- const themeV2NormalEditorRawStyleFieldNames = [...themeV2RuntimeStyleFieldNames, ...themeV2BlockSpecificRawStyleFieldNames];
4707
+ const themeV2NormalEditorRawStyleFieldNames = [
4708
+ ...themeV2RuntimeStyleFieldNames,
4709
+ ...themeV2BlockSpecificRawStyleFieldNames,
4710
+ "styleGroup"
4711
+ ];
4629
4712
  const themeV2NormalEditorRawStyleFieldNameSet = new Set(themeV2NormalEditorRawStyleFieldNames);
4630
4713
  const themeV2CompilerOwnedFieldNames = [...themeV2RuntimeStyleFieldNames, ...themeV2BlockSpecificRawStyleFieldNames];
4631
4714
  const pageDesignEditorBackgroundOverrideOwner = "page-design-editor";
@@ -4637,6 +4720,21 @@ function isThemeV2NormalEditorRawStylePath(fieldPath) {
4637
4720
  const rootField = fieldPath.split(".")[0] ?? "";
4638
4721
  return isThemeV2NormalEditorRawStyleFieldName(rootField);
4639
4722
  }
4723
+ const staleTransitionStyleGroupPrefix = "site-style-transition-";
4724
+ /**
4725
+ * A `styleGroup` in the retired `site-style-transition-*` namespace is residue
4726
+ * from the old per-block transition path (issue 1422). Any other value is
4727
+ * authored and must survive.
4728
+ */
4729
+ function isStaleTransitionStyleGroup(value) {
4730
+ return typeof value === "string" && value.startsWith(staleTransitionStyleGroupPrefix);
4731
+ }
4732
+ /** Drops a stale transition `styleGroup`, leaving authored values untouched. */
4733
+ function stripStaleTransitionStyleGroup(content) {
4734
+ if (!isStaleTransitionStyleGroup(content.styleGroup)) return { ...content };
4735
+ const { styleGroup: _stale, ...rest } = content;
4736
+ return rest;
4737
+ }
4640
4738
 
4641
4739
  //#endregion
4642
4740
  //#region ../theme-core/src/utils/tokenOrColor.ts
@@ -6700,10 +6798,10 @@ function resolveFocusIndicator(colour) {
6700
6798
  };
6701
6799
  }
6702
6800
  }
6703
- function resolveSolid(input, backgroundToken) {
6704
- const backgroundHex = paletteHex(input, backgroundToken);
6801
+ function resolveSolid(input, backgroundToken$1) {
6802
+ const backgroundHex = paletteHex(input, backgroundToken$1);
6705
6803
  if (!backgroundHex.ok) return backgroundHex;
6706
- const foregroundToken = unsafeAsOpaqueButtonColourToken(onColorTokenName(backgroundToken));
6804
+ const foregroundToken = unsafeAsOpaqueButtonColourToken(onColorTokenName(backgroundToken$1));
6707
6805
  const foregroundHex = input.palette[foregroundToken];
6708
6806
  if (typeof foregroundHex !== "string") {
6709
6807
  return {
@@ -6712,14 +6810,14 @@ function resolveSolid(input, backgroundToken) {
6712
6810
  tag: "missing_on_colour",
6713
6811
  variantId: input.variantId,
6714
6812
  state: input.state,
6715
- backgroundToken,
6813
+ backgroundToken: backgroundToken$1,
6716
6814
  foregroundToken
6717
6815
  }
6718
6816
  };
6719
6817
  }
6720
6818
  const contrast = validateButtonTextContrast({
6721
6819
  ...input,
6722
- background: backgroundToken,
6820
+ background: backgroundToken$1,
6723
6821
  backgroundHex: backgroundHex.hex,
6724
6822
  foreground: foregroundToken,
6725
6823
  foregroundHex
@@ -6737,9 +6835,9 @@ function resolveSolid(input, backgroundToken) {
6737
6835
  ok: true,
6738
6836
  colour: {
6739
6837
  kind: "solid",
6740
- backgroundToken,
6838
+ backgroundToken: backgroundToken$1,
6741
6839
  foregroundToken,
6742
- backgroundCss: tokenCss(backgroundToken),
6840
+ backgroundCss: tokenCss(backgroundToken$1),
6743
6841
  foregroundCss: tokenCss(foregroundToken)
6744
6842
  }
6745
6843
  };
@@ -13438,11 +13536,11 @@ const TEXT_CONTRAST_THRESHOLD = 3;
13438
13536
  * @param palette - Palette with color tokens (either ExpandedPalette or simple Record<string, string>)
13439
13537
  * @returns Array of text color token names that meet contrast threshold (3:1)
13440
13538
  */
13441
- function getLegibleTextColors(backgroundToken, palette) {
13442
- if (!backgroundToken) {
13539
+ function getLegibleTextColors(backgroundToken$1, palette) {
13540
+ if (!backgroundToken$1) {
13443
13541
  return getAllColorTokens(palette);
13444
13542
  }
13445
- const bgHex = palette[backgroundToken];
13543
+ const bgHex = palette[backgroundToken$1];
13446
13544
  if (!bgHex || typeof bgHex !== "string") {
13447
13545
  return getAllColorTokens(palette);
13448
13546
  }
@@ -15331,12 +15429,12 @@ function resolveAccessibleSurface(input) {
15331
15429
  const rejectedCandidates = [];
15332
15430
  const recipe = surfaceRecipes[input.surface];
15333
15431
  const threshold = contrastThresholdByPolicy[input.policy ?? "body-aa"];
15334
- for (const backgroundToken of recipe.backgroundCandidates) {
15335
- const backgroundHex = readPaletteHex(input.palette, backgroundToken);
15432
+ for (const backgroundToken$1 of recipe.backgroundCandidates) {
15433
+ const backgroundHex = readPaletteHex(input.palette, backgroundToken$1);
15336
15434
  if (!backgroundHex) {
15337
15435
  rejectedCandidates.push({
15338
15436
  kind: "background-candidate",
15339
- token: backgroundToken,
15437
+ token: backgroundToken$1,
15340
15438
  reason: "missing-token"
15341
15439
  });
15342
15440
  continue;
@@ -15344,7 +15442,7 @@ function resolveAccessibleSurface(input) {
15344
15442
  const heading = resolveRequiredForeground({
15345
15443
  role: "heading",
15346
15444
  candidates: recipe.headingCandidates,
15347
- backgroundToken,
15445
+ backgroundToken: backgroundToken$1,
15348
15446
  backgroundHex,
15349
15447
  palette: input.palette,
15350
15448
  threshold,
@@ -15353,7 +15451,7 @@ function resolveAccessibleSurface(input) {
15353
15451
  const text = resolveRequiredForeground({
15354
15452
  role: "text",
15355
15453
  candidates: recipe.textCandidates,
15356
- backgroundToken,
15454
+ backgroundToken: backgroundToken$1,
15357
15455
  backgroundHex,
15358
15456
  palette: input.palette,
15359
15457
  threshold,
@@ -15362,7 +15460,7 @@ function resolveAccessibleSurface(input) {
15362
15460
  const link = resolveRequiredForeground({
15363
15461
  role: "link",
15364
15462
  candidates: recipe.linkCandidates,
15365
- backgroundToken,
15463
+ backgroundToken: backgroundToken$1,
15366
15464
  backgroundHex,
15367
15465
  palette: input.palette,
15368
15466
  threshold,
@@ -15371,7 +15469,7 @@ function resolveAccessibleSurface(input) {
15371
15469
  if (!heading || !text || !link) continue;
15372
15470
  const supporting = resolveSupportingForeground({
15373
15471
  candidates: recipe.supportingCandidates,
15374
- backgroundToken,
15472
+ backgroundToken: backgroundToken$1,
15375
15473
  backgroundHex,
15376
15474
  text,
15377
15475
  palette: input.palette,
@@ -15382,7 +15480,7 @@ function resolveAccessibleSurface(input) {
15382
15480
  kind: "resolved-accessible-surface",
15383
15481
  surface: input.surface,
15384
15482
  background: {
15385
- token: asAccessibleSurfaceToken(backgroundToken),
15483
+ token: asAccessibleSurfaceToken(backgroundToken$1),
15386
15484
  hex: backgroundHex,
15387
15485
  inherited: recipe.inheritedBackground
15388
15486
  },
@@ -15481,23 +15579,23 @@ function resolveSafeToken(input) {
15481
15579
  }
15482
15580
  return null;
15483
15581
  }
15484
- function expandForegroundCandidates(candidates, backgroundToken) {
15582
+ function expandForegroundCandidates(candidates, backgroundToken$1) {
15485
15583
  const seen = new Set();
15486
15584
  const expanded = [];
15487
15585
  for (const candidate$1 of candidates) {
15488
- const token = expandForegroundCandidate(candidate$1, backgroundToken);
15586
+ const token = expandForegroundCandidate(candidate$1, backgroundToken$1);
15489
15587
  if (seen.has(token)) continue;
15490
15588
  seen.add(token);
15491
15589
  expanded.push(token);
15492
15590
  }
15493
15591
  return expanded;
15494
15592
  }
15495
- function expandForegroundCandidate(candidate$1, backgroundToken) {
15593
+ function expandForegroundCandidate(candidate$1, backgroundToken$1) {
15496
15594
  if (typeof candidate$1 === "string") {
15497
15595
  return candidate$1;
15498
15596
  }
15499
15597
  switch (candidate$1.kind) {
15500
- case "on-background": return onColorTokenName(backgroundToken);
15598
+ case "on-background": return onColorTokenName(backgroundToken$1);
15501
15599
  }
15502
15600
  }
15503
15601
  function readPaletteHex(palette, token) {
@@ -15631,7 +15729,6 @@ function blockDesignPlanFromAppearancePreset(preset, input) {
15631
15729
  return {
15632
15730
  ...planInput,
15633
15731
  appearancePresetId: asAppearancePresetId(preset.id),
15634
- transitionAfter: "none",
15635
15732
  emphasis: "medium",
15636
15733
  ...resolveBlockAppearancePresetDesign(preset, layoutProfile ?? null)
15637
15734
  };
@@ -16506,235 +16603,339 @@ function cardRoleFromLegacyVocabulary(value) {
16506
16603
  const cardRoleWithLegacyAliasSchema = z.preprocess((value) => cardRoleFromLegacyVocabulary(value) ?? value, cardRoleSchema);
16507
16604
 
16508
16605
  //#endregion
16509
- //#region ../theme-core/src/site-styles/compositionBudget.ts
16510
- const repairActionsByRatioIssueKind = {
16511
- "accent-density-exceeded": ["reduce-accent-density"],
16512
- "panel-density-exceeded": ["reduce-panel-density"],
16513
- "transition-mix-exceeded": ["reduce-transition-mix"],
16514
- "media-density-exceeded": ["reduce-media-density"],
16515
- "decorative-repetition-exceeded": ["reduce-decorative-repetition"]
16516
- };
16517
- function issueMetadata(policy, allowedRepairActions) {
16518
- return {
16519
- policy,
16520
- severity: designIssueSeverityForPolicy(policy),
16521
- allowedRepairActions
16606
+ //#region ../theme-core/src/site-styles/runtimeBackgroundResolution.ts
16607
+ function parseCanonicalRuntimeBackground(content) {
16608
+ const sectionStyles = readRecord(content._sectionStyles);
16609
+ const background = readRecord(sectionStyles?.background);
16610
+ if (!background) return { kind: "none" };
16611
+ const parsed = boxBackgroundSchema.safeParse(background);
16612
+ if (!parsed.success) return { kind: "none" };
16613
+ return parsed.data.contentOwner === pageDesignEditorBackgroundOverrideOwner ? {
16614
+ kind: "page-design-override",
16615
+ background: parsed.data
16616
+ } : {
16617
+ kind: "block-authored",
16618
+ background: parsed.data
16522
16619
  };
16523
16620
  }
16524
- /** Returns the fraction of page blocks selected by a budget check. */
16525
- function ratio(count, total) {
16526
- return total === 0 ? 0 : count / total;
16527
- }
16528
- function maxCompositionItemsWithinRatio(totalItems, allowedRatio) {
16529
- return Math.floor((allowedRatio + Number.EPSILON) * totalItems);
16530
- }
16531
- function isRatioExceeded(actualCount, totalItems, allowedRatio) {
16532
- return actualCount > maxCompositionItemsWithinRatio(totalItems, allowedRatio);
16533
- }
16534
- function issueWhenRatioExceeded(input) {
16535
- const actualRatio = ratio(input.blockIds.length, input.totalBlocks);
16536
- if (!isRatioExceeded(input.blockIds.length, input.totalBlocks, input.allowedRatio)) {
16537
- return null;
16621
+ function resolveSectionBackground(source, lookDefault) {
16622
+ if (source.kind === "page-design-override") {
16623
+ return {
16624
+ kind: "page-design-override",
16625
+ background: source.background
16626
+ };
16538
16627
  }
16539
- return {
16540
- kind: input.kind,
16541
- budget: input.budget,
16542
- actualRatio,
16543
- allowedRatio: input.allowedRatio,
16544
- blockIds: input.blockIds,
16545
- ...issueMetadata(input.policy, repairActionsByRatioIssueKind[input.kind])
16546
- };
16628
+ if (lookDefault) {
16629
+ if (source.kind === "block-authored" && (source.background.type === "image" || source.background.type === "video")) {
16630
+ return {
16631
+ kind: "block-authored",
16632
+ background: mergeLookForegroundWithAuthoredMedia(lookDefault, source.background)
16633
+ };
16634
+ }
16635
+ return {
16636
+ kind: "look-default",
16637
+ background: lookDefault
16638
+ };
16639
+ }
16640
+ if (source.kind === "block-authored" && (source.background.type === "image" || source.background.type === "video")) {
16641
+ return {
16642
+ kind: "block-authored",
16643
+ background: source.background
16644
+ };
16645
+ }
16646
+ return source.kind === "block-authored" ? {
16647
+ kind: "block-authored",
16648
+ background: source.background
16649
+ } : { kind: "none" };
16547
16650
  }
16548
- /** Checks whether block framing or a card surface contributes panel mass. */
16549
- function hasPanelFrameOrCardSurface(block) {
16550
- return block.appliedContentFrame.kind !== "plain" || block.appliedCardRole != null && block.appliedCardRole !== "none";
16651
+ function resolveSectionForeground(background, palette) {
16652
+ return resolveSurfaceForeground(background.kind === "none" ? null : background.background, palette, {}, { compositeBase: "background" });
16551
16653
  }
16552
- /** Checks explicit decorative annotations; transitions are budgeted separately. */
16553
- function hasDecorativeAnnotation(context) {
16554
- return context?.decorativeIntent === "supporting" || context?.decorativeIntent === "feature";
16654
+ /**
16655
+ * Read foreground authority from post-compiler block content. The preview
16656
+ * compiler has already materialized any Look-owned background into
16657
+ * `_sectionStyles`; raw authored content must be composed with its Look through
16658
+ * `resolveSectionBackground` instead.
16659
+ */
16660
+ function resolveCompiledSectionForegroundFromContent(content, palette) {
16661
+ if (!content) return resolveSurfaceForeground(null, palette);
16662
+ return resolveSectionForeground(resolveSectionBackground(parseCanonicalRuntimeBackground(content), null), palette);
16555
16663
  }
16556
- /** Checks whether a block annotation says media contributes visual weight. */
16557
- function hasMediaAnnotation(context) {
16558
- return context?.mediaIntent === "supporting" || context?.mediaIntent === "dominant";
16664
+ function mergeLookForegroundWithAuthoredMedia(lookDefault, authored) {
16665
+ const merged = boxBackgroundSchema.safeParse({
16666
+ ...lookDefault,
16667
+ type: authored.type,
16668
+ image: authored.image,
16669
+ objectFit: authored.objectFit,
16670
+ scale: authored.scale,
16671
+ position: authored.position
16672
+ });
16673
+ return merged.success ? merged.data : authored;
16559
16674
  }
16560
- function validateCompositionBudget(input) {
16561
- const { budget, policy, resolved } = input;
16562
- const totalBlocks = resolved.blocks.length;
16563
- const knownBlockIds = new Set(resolved.blocks.map((block) => block.blockId));
16564
- const blockContexts = input.blockContexts ?? [];
16565
- const unknownContextBlockIds = blockContexts.filter((context) => !knownBlockIds.has(context.blockId)).map((context) => context.blockId);
16566
- const knownBlockContexts = blockContexts.filter((context) => knownBlockIds.has(context.blockId));
16567
- const contextsByBlockId = new Map(knownBlockContexts.map((context) => [context.blockId, context]));
16568
- const issues = [];
16569
- if (unknownContextBlockIds.length > 0) {
16570
- issues.push({
16571
- kind: "unknown-composition-context-block",
16572
- blockIds: unknownContextBlockIds,
16573
- ...issueMetadata(policy, [])
16574
- });
16675
+ function readRecord(value) {
16676
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
16677
+ }
16678
+
16679
+ //#endregion
16680
+ //#region ../theme-core/src/site-styles/transitionCatalogue.ts
16681
+ /**
16682
+ * Family order for pickers. Entries below are listed family by family in this
16683
+ * same order, so the flat option list and the grouped families agree.
16684
+ */
16685
+ const transitionCatalogueFamilies = [
16686
+ {
16687
+ id: "gradient",
16688
+ label: "Gradient"
16689
+ },
16690
+ {
16691
+ id: "wave",
16692
+ label: "Wave"
16693
+ },
16694
+ {
16695
+ id: "angle",
16696
+ label: "Angle"
16575
16697
  }
16576
- const accentBlockIds = resolved.blocks.filter((block) => block.appliedSectionSurface === "accent-band" || block.appliedSectionSurface === "brand-depth-band").map((block) => block.blockId);
16577
- const accentIssue = issueWhenRatioExceeded({
16578
- kind: "accent-density-exceeded",
16579
- policy,
16580
- budget: budget.accentDensity,
16581
- blockIds: accentBlockIds,
16582
- totalBlocks,
16583
- allowedRatio: compositionDensityRatioLimits[budget.accentDensity]
16584
- });
16585
- if (accentIssue) {
16586
- issues.push(accentIssue);
16587
- }
16588
- const panelBlockIds = resolved.blocks.filter(hasPanelFrameOrCardSurface).map((block) => block.blockId);
16589
- const panelIssue = issueWhenRatioExceeded({
16590
- kind: "panel-density-exceeded",
16591
- policy,
16592
- budget: budget.panelDensity,
16593
- blockIds: panelBlockIds,
16594
- totalBlocks,
16595
- allowedRatio: compositionDensityRatioLimits[budget.panelDensity]
16596
- });
16597
- if (panelIssue) {
16598
- issues.push(panelIssue);
16599
- }
16600
- const transitionBlockIds = resolved.blocks.filter((block) => block.appliedTransitionAfter !== "none").map((block) => block.blockId);
16601
- const transitionIssue = issueWhenRatioExceeded({
16602
- kind: "transition-mix-exceeded",
16603
- policy,
16604
- budget: budget.transitionMix,
16605
- blockIds: transitionBlockIds,
16606
- totalBlocks,
16607
- allowedRatio: compositionTransitionMixRatioLimits[budget.transitionMix]
16608
- });
16609
- if (transitionIssue) {
16610
- issues.push(transitionIssue);
16611
- }
16612
- const ctaContexts = knownBlockContexts.filter((context) => (context.primaryCtaCount ?? 0) > 0);
16613
- const actualPrimaryCtaCount = ctaContexts.reduce((sum, context) => sum + (context.primaryCtaCount ?? 0), 0);
16614
- if (actualPrimaryCtaCount > budget.primaryCtaLimit) {
16615
- issues.push({
16616
- kind: "primary-cta-limit-exceeded",
16617
- actualCount: actualPrimaryCtaCount,
16618
- allowedCount: budget.primaryCtaLimit,
16619
- blockIds: ctaContexts.map((context) => context.blockId),
16620
- ...issueMetadata(policy, ["reduce-primary-ctas"])
16621
- });
16622
- }
16623
- const mediaBlockIds = resolved.blocks.filter((block) => hasMediaAnnotation(contextsByBlockId.get(block.blockId))).map((block) => block.blockId);
16624
- const mediaIssue = issueWhenRatioExceeded({
16625
- kind: "media-density-exceeded",
16626
- policy,
16627
- budget: budget.mediaDensity,
16628
- blockIds: mediaBlockIds,
16629
- totalBlocks,
16630
- allowedRatio: compositionDensityRatioLimits[budget.mediaDensity]
16631
- });
16632
- if (mediaIssue) {
16633
- issues.push(mediaIssue);
16634
- }
16635
- const decorativeBlockIds = resolved.blocks.filter((block) => hasDecorativeAnnotation(contextsByBlockId.get(block.blockId))).map((block) => block.blockId);
16636
- const decorativeIssue = issueWhenRatioExceeded({
16637
- kind: "decorative-repetition-exceeded",
16638
- policy,
16639
- budget: budget.decorativeRepetition,
16640
- blockIds: decorativeBlockIds,
16641
- totalBlocks,
16642
- allowedRatio: compositionDensityRatioLimits[budget.decorativeRepetition]
16643
- });
16644
- if (decorativeIssue) {
16645
- issues.push(decorativeIssue);
16646
- }
16647
- return issues;
16648
- }
16649
-
16650
- //#endregion
16651
- //#region ../theme-core/src/site-styles/compilationPlan.ts
16652
- const defaultHeaderBrandPreferences = {
16653
- brandDisplay: "auto",
16654
- logoSize: "auto"
16655
- };
16656
- function headerBrandPreferencesFromTheme(theme) {
16657
- return {
16658
- brandDisplay: theme?.header.logo?.brandDisplay ?? defaultHeaderBrandPreferences.brandDisplay,
16659
- logoSize: theme?.header.logo?.size ?? defaultHeaderBrandPreferences.logoSize
16660
- };
16661
- }
16662
- function replaceSiteStyleCompilationPlan(input) {
16663
- return {
16664
- kind: "replace-site-style",
16665
- buttons: {
16666
- kind: "recompile",
16667
- selection: input.buttonStyleSelection ?? null,
16668
- reason: "site-style-replacement"
16669
- },
16670
- header: {
16671
- kind: "recompile",
16672
- selection: input.headerSelection,
16673
- brandPresentation: input.headerLogo === undefined ? { kind: "use-compiled-defaults" } : {
16674
- kind: "apply-logo-context",
16675
- logo: input.headerLogo
16676
- }
16677
- },
16678
- footerLookId: input.footerLookId ?? null
16679
- };
16680
- }
16698
+ ];
16699
+ const mediumHeight = "6rem + 2px";
16700
+ const compactHeight = "4.5rem + 2px";
16701
+ const largeHeight = "7rem + 2px";
16681
16702
  /**
16682
- * Writes the brand display / logo size preference onto the header's logo style.
16703
+ * The single source of transition vocabulary. Order is significant: it is the
16704
+ * order of `boundaryOptions` (after `none`) and of each family's variants,
16705
+ * which persisted pins, page-flow presets and the editor picker all rely on.
16683
16706
  *
16684
- * Moved here from preview-next in #1318 Phase 4a so the header preference
16685
- * pipeline can be composed once, in theme-core, rather than half in the client.
16707
+ * Shapes match what `generateLayoutCss` draws today. Structured
16708
+ * amplitude/frequency treatments (issue 826) extend this list later.
16686
16709
  */
16687
- function withHeaderBrandThemePreferences(theme, preferences) {
16688
- const currentLogo = theme.header.logo;
16689
- if (!currentLogo && preferences.brandDisplay === defaultHeaderBrandPreferences.brandDisplay && preferences.logoSize === defaultHeaderBrandPreferences.logoSize) {
16690
- return theme;
16691
- }
16692
- return {
16693
- ...theme,
16694
- header: {
16695
- ...theme.header,
16696
- logo: {
16697
- fontFamily: currentLogo?.fontFamily ?? "heading",
16698
- letterSpacing: currentLogo?.letterSpacing ?? "normal",
16699
- gradient: currentLogo?.gradient ?? false,
16700
- ...currentLogo ?? {},
16701
- brandDisplay: preferences.brandDisplay,
16702
- size: preferences.logoSize
16710
+ const transitionCatalogueEntries = defineExactCatalog()([
16711
+ {
16712
+ intent: "soft-fade-short",
16713
+ optionId: asBoundaryOptionId("break:soft"),
16714
+ family: "gradient",
16715
+ variantId: "soft",
16716
+ label: "Soft",
16717
+ variantLabel: "Soft",
16718
+ treatment: {
16719
+ kind: "soft-fade",
16720
+ height: "medium",
16721
+ curve: "soft"
16722
+ },
16723
+ size: "medium",
16724
+ heightExpression: mediumHeight,
16725
+ shape: { kind: "fade" }
16726
+ },
16727
+ {
16728
+ intent: "soft-fade-long",
16729
+ optionId: asBoundaryOptionId("break:spacious"),
16730
+ family: "gradient",
16731
+ variantId: "spacious",
16732
+ label: "Spacious",
16733
+ variantLabel: "Spacious",
16734
+ treatment: {
16735
+ kind: "soft-fade",
16736
+ height: "large",
16737
+ curve: "spacious"
16738
+ },
16739
+ size: "spacious",
16740
+ heightExpression: "10rem",
16741
+ shape: { kind: "fade" }
16742
+ },
16743
+ {
16744
+ intent: "soft-fade-expansive",
16745
+ optionId: asBoundaryOptionId("break:expansive"),
16746
+ family: "gradient",
16747
+ variantId: "expansive",
16748
+ label: "Expansive",
16749
+ variantLabel: "Expansive",
16750
+ treatment: {
16751
+ kind: "soft-fade",
16752
+ height: "expansive",
16753
+ curve: "expansive"
16754
+ },
16755
+ size: "expansive",
16756
+ heightExpression: "14rem",
16757
+ shape: { kind: "fade" }
16758
+ },
16759
+ {
16760
+ intent: "wave-edge-balanced",
16761
+ optionId: asBoundaryOptionId("break:wave-balanced"),
16762
+ family: "wave",
16763
+ variantId: "balanced",
16764
+ label: "Balanced wave",
16765
+ variantLabel: "Balanced",
16766
+ treatment: {
16767
+ kind: "wave-edge",
16768
+ height: "medium",
16769
+ amplitude: "balanced",
16770
+ rhythm: "double",
16771
+ symmetry: "symmetrical"
16772
+ },
16773
+ size: "medium",
16774
+ heightExpression: mediumHeight,
16775
+ shape: {
16776
+ kind: "wave",
16777
+ mask: "balanced"
16778
+ }
16779
+ },
16780
+ {
16781
+ intent: "wave-edge",
16782
+ optionId: asBoundaryOptionId("break:wave"),
16783
+ family: "wave",
16784
+ variantId: "classic",
16785
+ label: "Classic wave",
16786
+ variantLabel: "Classic",
16787
+ treatment: {
16788
+ kind: "wave-edge",
16789
+ height: "medium",
16790
+ amplitude: "balanced",
16791
+ rhythm: "double",
16792
+ symmetry: "asymmetrical"
16793
+ },
16794
+ size: "medium",
16795
+ heightExpression: mediumHeight,
16796
+ shape: {
16797
+ kind: "wave",
16798
+ mask: "classic"
16799
+ }
16800
+ },
16801
+ {
16802
+ intent: "wave-edge-gentle",
16803
+ optionId: asBoundaryOptionId("break:wave-gentle"),
16804
+ family: "wave",
16805
+ variantId: "gentle",
16806
+ label: "Gentle wave",
16807
+ variantLabel: "Gentle",
16808
+ treatment: {
16809
+ kind: "wave-edge",
16810
+ height: "compact",
16811
+ amplitude: "subtle",
16812
+ rhythm: "single",
16813
+ symmetry: "symmetrical"
16814
+ },
16815
+ size: "compact",
16816
+ heightExpression: compactHeight,
16817
+ shape: {
16818
+ kind: "wave",
16819
+ mask: "gentle"
16820
+ }
16821
+ },
16822
+ {
16823
+ intent: "wave-edge-bold",
16824
+ optionId: asBoundaryOptionId("break:wave-bold"),
16825
+ family: "wave",
16826
+ variantId: "bold",
16827
+ label: "Bold wave",
16828
+ variantLabel: "Bold",
16829
+ treatment: {
16830
+ kind: "wave-edge",
16831
+ height: "large",
16832
+ amplitude: "bold",
16833
+ rhythm: "double",
16834
+ symmetry: "symmetrical"
16835
+ },
16836
+ size: "large",
16837
+ heightExpression: largeHeight,
16838
+ shape: {
16839
+ kind: "wave",
16840
+ mask: "bold"
16841
+ }
16842
+ },
16843
+ {
16844
+ intent: "wave-edge-drift",
16845
+ optionId: asBoundaryOptionId("break:wave-drift"),
16846
+ family: "wave",
16847
+ variantId: "drift",
16848
+ label: "Drift wave",
16849
+ variantLabel: "Drift",
16850
+ treatment: {
16851
+ kind: "wave-edge",
16852
+ height: "medium",
16853
+ amplitude: "balanced",
16854
+ rhythm: "double",
16855
+ symmetry: "drift"
16856
+ },
16857
+ size: "medium",
16858
+ heightExpression: mediumHeight,
16859
+ shape: {
16860
+ kind: "wave",
16861
+ mask: "drift"
16862
+ }
16863
+ },
16864
+ {
16865
+ intent: "angle-edge-subtle",
16866
+ optionId: asBoundaryOptionId("break:angle-subtle"),
16867
+ family: "angle",
16868
+ variantId: "subtle",
16869
+ label: "Subtle angle",
16870
+ variantLabel: "Subtle",
16871
+ treatment: {
16872
+ kind: "angle-edge",
16873
+ height: "compact",
16874
+ slope: "subtle",
16875
+ direction: "forward"
16876
+ },
16877
+ size: "compact",
16878
+ heightExpression: compactHeight,
16879
+ shape: {
16880
+ kind: "angle",
16881
+ polygon: {
16882
+ startTopPercent: 34,
16883
+ endTopPercent: 18
16703
16884
  }
16704
16885
  }
16705
- };
16706
- }
16707
-
16708
- //#endregion
16709
- //#region ../theme-core/src/site-styles/schemaVersionedJson.ts
16710
- function parseSchemaVersionedJson(input) {
16711
- if (input.value == null) {
16712
- return { kind: "absent" };
16713
- }
16714
- if (typeof input.value === "object" && input.value !== null && "schemaVersion" in input.value && input.value.schemaVersion !== input.expectedSchemaVersion) {
16715
- return {
16716
- kind: "invalid",
16717
- issue: {
16718
- kind: "unsupported-schema-version",
16719
- message: input.unsupportedSchemaVersionMessage
16886
+ },
16887
+ {
16888
+ intent: "angle-edge",
16889
+ optionId: asBoundaryOptionId("break:angle-balanced"),
16890
+ family: "angle",
16891
+ variantId: "balanced",
16892
+ label: "Balanced angle",
16893
+ variantLabel: "Balanced",
16894
+ treatment: {
16895
+ kind: "angle-edge",
16896
+ height: "medium",
16897
+ slope: "balanced",
16898
+ direction: "forward"
16899
+ },
16900
+ size: "medium",
16901
+ heightExpression: mediumHeight,
16902
+ shape: {
16903
+ kind: "angle",
16904
+ polygon: {
16905
+ startTopPercent: 42,
16906
+ endTopPercent: 8
16720
16907
  }
16721
- };
16722
- }
16723
- const parsed = input.schema.safeParse(input.value);
16724
- if (!parsed.success) {
16725
- const firstIssue = parsed.error.issues[0];
16726
- return {
16727
- kind: "invalid",
16728
- issue: {
16729
- kind: "invalid-json-shape",
16730
- message: firstIssue?.message ?? input.invalidShapeMessage
16908
+ }
16909
+ },
16910
+ {
16911
+ intent: "angle-edge-steep",
16912
+ optionId: asBoundaryOptionId("break:angle-steep"),
16913
+ family: "angle",
16914
+ variantId: "steep",
16915
+ label: "Steep angle",
16916
+ variantLabel: "Steep",
16917
+ treatment: {
16918
+ kind: "angle-edge",
16919
+ height: "large",
16920
+ slope: "steep",
16921
+ direction: "forward"
16922
+ },
16923
+ size: "large",
16924
+ heightExpression: largeHeight,
16925
+ shape: {
16926
+ kind: "angle",
16927
+ polygon: {
16928
+ startTopPercent: 54,
16929
+ endTopPercent: 0
16731
16930
  }
16732
- };
16931
+ }
16733
16932
  }
16734
- return {
16735
- kind: "parsed",
16736
- value: parsed.data
16737
- };
16933
+ ]);
16934
+ const transitionCatalogueCoversEveryIntentOnce = true;
16935
+ void transitionCatalogueCoversEveryIntentOnce;
16936
+ /** CSS `clip-path` value for an angle shape, as `generateLayoutCss` draws it. */
16937
+ function anglePolygonCss(polygon) {
16938
+ return `polygon(0 ${polygon.startTopPercent}%, 100% ${polygon.endTopPercent}%, 100% 100%, 0 100%)`;
16738
16939
  }
16739
16940
 
16740
16941
  //#endregion
@@ -16747,22 +16948,27 @@ function boundaryRuntimeMetrics(family, size, heightExpression) {
16747
16948
  reserveCss: `calc((${heightExpression}) / 2)`
16748
16949
  };
16749
16950
  }
16750
- const sectionBoundaryRuntimeMetricsByTransition = {
16751
- none: null,
16752
- "soft-fade-short": boundaryRuntimeMetrics("gradient", "medium", "6rem + 2px"),
16753
- "soft-fade-long": boundaryRuntimeMetrics("gradient", "spacious", "10rem"),
16754
- "soft-fade-expansive": boundaryRuntimeMetrics("gradient", "expansive", "14rem"),
16755
- "wave-edge": boundaryRuntimeMetrics("wave", "medium", "6rem + 2px"),
16756
- "wave-edge-balanced": boundaryRuntimeMetrics("wave", "medium", "6rem + 2px"),
16757
- "wave-edge-gentle": boundaryRuntimeMetrics("wave", "compact", "4.5rem + 2px"),
16758
- "wave-edge-bold": boundaryRuntimeMetrics("wave", "large", "7rem + 2px"),
16759
- "wave-edge-drift": boundaryRuntimeMetrics("wave", "medium", "6rem + 2px"),
16760
- "angle-edge": boundaryRuntimeMetrics("angle", "medium", "6rem + 2px"),
16761
- "angle-edge-subtle": boundaryRuntimeMetrics("angle", "compact", "4.5rem + 2px"),
16762
- "angle-edge-steep": boundaryRuntimeMetrics("angle", "large", "7rem + 2px")
16763
- };
16764
- function boundaryRuntimeMetricsForTransition(transition) {
16765
- return sectionBoundaryRuntimeMetricsByTransition[transition];
16951
+ const catalogueEntryByIntent = new Map(transitionCatalogueEntries.map((entry) => [entry.intent, entry]));
16952
+ const metricsByIntent = new Map(transitionCatalogueEntries.map((entry) => [entry.intent, boundaryRuntimeMetrics(entry.family, entry.size, entry.heightExpression)]));
16953
+ /**
16954
+ * The catalogue's compile-time coverage check guarantees an entry per
16955
+ * decorative intent, so a miss here is a broken invariant, not bad input.
16956
+ */
16957
+ function requireCatalogueValue(values, intent) {
16958
+ const value = values.get(intent);
16959
+ if (value === undefined) {
16960
+ throw new Error(`Transition catalogue has no entry for ${intent}`);
16961
+ }
16962
+ return value;
16963
+ }
16964
+ function transitionCatalogueEntryForIntent(intent) {
16965
+ return requireCatalogueValue(catalogueEntryByIntent, intent);
16966
+ }
16967
+ function isDecorativeTransitionIntent(transition) {
16968
+ return transition !== "none";
16969
+ }
16970
+ function boundaryRuntimeMetricsForDecorativeTransition(transition) {
16971
+ return requireCatalogueValue(metricsByIntent, transition);
16766
16972
  }
16767
16973
  const noneBoundaryOption = {
16768
16974
  id: asBoundaryOptionId("break:none"),
@@ -16770,264 +16976,63 @@ const noneBoundaryOption = {
16770
16976
  transitionAfter: "none",
16771
16977
  treatment: { kind: "none" }
16772
16978
  };
16773
- const softBoundaryOption = {
16774
- id: asBoundaryOptionId("break:soft"),
16775
- label: "Soft",
16776
- transitionAfter: "soft-fade-short",
16777
- treatment: {
16778
- kind: "soft-fade",
16779
- height: "medium",
16780
- curve: "soft"
16781
- }
16782
- };
16783
- const spaciousBoundaryOption = {
16784
- id: asBoundaryOptionId("break:spacious"),
16785
- label: "Spacious",
16786
- transitionAfter: "soft-fade-long",
16787
- treatment: {
16788
- kind: "soft-fade",
16789
- height: "large",
16790
- curve: "spacious"
16791
- }
16792
- };
16793
- const expansiveBoundaryOption = {
16794
- id: asBoundaryOptionId("break:expansive"),
16795
- label: "Expansive",
16796
- transitionAfter: "soft-fade-expansive",
16797
- treatment: {
16798
- kind: "soft-fade",
16799
- height: "expansive",
16800
- curve: "expansive"
16801
- }
16802
- };
16803
- const waveBoundaryOption = {
16804
- id: asBoundaryOptionId("break:wave"),
16805
- label: "Classic wave",
16806
- transitionAfter: "wave-edge",
16807
- treatment: {
16808
- kind: "wave-edge",
16809
- height: "medium",
16810
- amplitude: "balanced",
16811
- rhythm: "double",
16812
- symmetry: "asymmetrical"
16813
- }
16814
- };
16815
- const balancedWaveBoundaryOption = {
16816
- id: asBoundaryOptionId("break:wave-balanced"),
16817
- label: "Balanced wave",
16818
- transitionAfter: "wave-edge-balanced",
16819
- treatment: {
16820
- kind: "wave-edge",
16821
- height: "medium",
16822
- amplitude: "balanced",
16823
- rhythm: "double",
16824
- symmetry: "symmetrical"
16825
- }
16826
- };
16827
- const gentleWaveBoundaryOption = {
16828
- id: asBoundaryOptionId("break:wave-gentle"),
16829
- label: "Gentle wave",
16830
- transitionAfter: "wave-edge-gentle",
16831
- treatment: {
16832
- kind: "wave-edge",
16833
- height: "compact",
16834
- amplitude: "subtle",
16835
- rhythm: "single",
16836
- symmetry: "symmetrical"
16837
- }
16838
- };
16839
- const boldWaveBoundaryOption = {
16840
- id: asBoundaryOptionId("break:wave-bold"),
16841
- label: "Bold wave",
16842
- transitionAfter: "wave-edge-bold",
16843
- treatment: {
16844
- kind: "wave-edge",
16845
- height: "large",
16846
- amplitude: "bold",
16847
- rhythm: "double",
16848
- symmetry: "symmetrical"
16849
- }
16850
- };
16851
- const driftWaveBoundaryOption = {
16852
- id: asBoundaryOptionId("break:wave-drift"),
16853
- label: "Drift wave",
16854
- transitionAfter: "wave-edge-drift",
16855
- treatment: {
16856
- kind: "wave-edge",
16857
- height: "medium",
16858
- amplitude: "balanced",
16859
- rhythm: "double",
16860
- symmetry: "drift"
16861
- }
16862
- };
16863
- const subtleAngleBoundaryOption = {
16864
- id: asBoundaryOptionId("break:angle-subtle"),
16865
- label: "Subtle angle",
16866
- transitionAfter: "angle-edge-subtle",
16867
- treatment: {
16868
- kind: "angle-edge",
16869
- height: "compact",
16870
- slope: "subtle",
16871
- direction: "forward"
16872
- }
16873
- };
16874
- const balancedAngleBoundaryOption = {
16875
- id: asBoundaryOptionId("break:angle-balanced"),
16876
- label: "Balanced angle",
16877
- transitionAfter: "angle-edge",
16878
- treatment: {
16879
- kind: "angle-edge",
16880
- height: "medium",
16881
- slope: "balanced",
16882
- direction: "forward"
16883
- }
16884
- };
16885
- const steepAngleBoundaryOption = {
16886
- id: asBoundaryOptionId("break:angle-steep"),
16887
- label: "Steep angle",
16888
- transitionAfter: "angle-edge-steep",
16889
- treatment: {
16890
- kind: "angle-edge",
16891
- height: "large",
16892
- slope: "steep",
16893
- direction: "forward"
16894
- }
16895
- };
16979
+ const boundaryOptionByIntent = new Map(transitionCatalogueEntries.map((entry) => [entry.intent, {
16980
+ id: entry.optionId,
16981
+ label: entry.label,
16982
+ transitionAfter: entry.intent,
16983
+ treatment: entry.treatment
16984
+ }]));
16985
+ function boundaryOptionForTransitionIntent(intent) {
16986
+ return requireCatalogueValue(boundaryOptionByIntent, intent);
16987
+ }
16988
+ const softBoundaryOption = boundaryOptionForTransitionIntent("soft-fade-short");
16989
+ const spaciousBoundaryOption = boundaryOptionForTransitionIntent("soft-fade-long");
16990
+ const expansiveBoundaryOption = boundaryOptionForTransitionIntent("soft-fade-expansive");
16991
+ const waveBoundaryOption = boundaryOptionForTransitionIntent("wave-edge");
16992
+ const balancedWaveBoundaryOption = boundaryOptionForTransitionIntent("wave-edge-balanced");
16993
+ const gentleWaveBoundaryOption = boundaryOptionForTransitionIntent("wave-edge-gentle");
16994
+ const boldWaveBoundaryOption = boundaryOptionForTransitionIntent("wave-edge-bold");
16995
+ const driftWaveBoundaryOption = boundaryOptionForTransitionIntent("wave-edge-drift");
16996
+ const subtleAngleBoundaryOption = boundaryOptionForTransitionIntent("angle-edge-subtle");
16997
+ const balancedAngleBoundaryOption = boundaryOptionForTransitionIntent("angle-edge");
16998
+ const steepAngleBoundaryOption = boundaryOptionForTransitionIntent("angle-edge-steep");
16999
+ /**
17000
+ * Must remain non-empty; downstream code indexes `[0]` as the documented default.
17001
+ */
17002
+ const boundaryOptions = [noneBoundaryOption, ...transitionCatalogueEntries.map((entry) => boundaryOptionForTransitionIntent(entry.intent))];
16896
17003
  const noneTransitionVariant = {
16897
17004
  familyId: "none",
16898
17005
  variantId: "none",
16899
17006
  label: "None",
16900
17007
  boundaryOption: noneBoundaryOption
16901
17008
  };
16902
- const softGradientTransitionVariant = {
16903
- familyId: "gradient",
16904
- variantId: "soft",
16905
- label: "Soft",
16906
- boundaryOption: softBoundaryOption
16907
- };
16908
- const spaciousGradientTransitionVariant = {
16909
- familyId: "gradient",
16910
- variantId: "spacious",
16911
- label: "Spacious",
16912
- boundaryOption: spaciousBoundaryOption
16913
- };
16914
- const expansiveGradientTransitionVariant = {
16915
- familyId: "gradient",
16916
- variantId: "expansive",
16917
- label: "Expansive",
16918
- boundaryOption: expansiveBoundaryOption
16919
- };
16920
- const balancedWaveTransitionVariant = {
16921
- familyId: "wave",
16922
- variantId: "balanced",
16923
- label: "Balanced",
16924
- boundaryOption: balancedWaveBoundaryOption
16925
- };
16926
- const classicWaveTransitionVariant = {
16927
- familyId: "wave",
16928
- variantId: "classic",
16929
- label: "Classic",
16930
- boundaryOption: waveBoundaryOption
16931
- };
16932
- const gentleWaveTransitionVariant = {
16933
- familyId: "wave",
16934
- variantId: "gentle",
16935
- label: "Gentle",
16936
- boundaryOption: gentleWaveBoundaryOption
16937
- };
16938
- const boldWaveTransitionVariant = {
16939
- familyId: "wave",
16940
- variantId: "bold",
16941
- label: "Bold",
16942
- boundaryOption: boldWaveBoundaryOption
16943
- };
16944
- const driftWaveTransitionVariant = {
16945
- familyId: "wave",
16946
- variantId: "drift",
16947
- label: "Drift",
16948
- boundaryOption: driftWaveBoundaryOption
16949
- };
16950
- const subtleAngleTransitionVariant = {
16951
- familyId: "angle",
16952
- variantId: "subtle",
16953
- label: "Subtle",
16954
- boundaryOption: subtleAngleBoundaryOption
16955
- };
16956
- const balancedAngleTransitionVariant = {
16957
- familyId: "angle",
16958
- variantId: "balanced",
16959
- label: "Balanced",
16960
- boundaryOption: balancedAngleBoundaryOption
16961
- };
16962
- const steepAngleTransitionVariant = {
16963
- familyId: "angle",
16964
- variantId: "steep",
16965
- label: "Steep",
16966
- boundaryOption: steepAngleBoundaryOption
16967
- };
16968
- const sectionTransitionFamilies = [
16969
- {
16970
- id: "none",
16971
- label: "None",
16972
- variants: [noneTransitionVariant]
16973
- },
16974
- {
16975
- id: "gradient",
16976
- label: "Gradient",
16977
- variants: [
16978
- softGradientTransitionVariant,
16979
- spaciousGradientTransitionVariant,
16980
- expansiveGradientTransitionVariant
16981
- ]
16982
- },
16983
- {
16984
- id: "wave",
16985
- label: "Wave",
16986
- variants: [
16987
- balancedWaveTransitionVariant,
16988
- classicWaveTransitionVariant,
16989
- gentleWaveTransitionVariant,
16990
- boldWaveTransitionVariant,
16991
- driftWaveTransitionVariant
16992
- ]
16993
- },
16994
- {
16995
- id: "angle",
16996
- label: "Angle",
16997
- variants: [
16998
- subtleAngleTransitionVariant,
16999
- balancedAngleTransitionVariant,
17000
- steepAngleTransitionVariant
17001
- ]
17002
- }
17003
- ];
17004
- /**
17005
- * Must remain non-empty; downstream code indexes `[0]` as the documented default.
17006
- */
17007
- const boundaryOptions = [
17008
- noneBoundaryOption,
17009
- softBoundaryOption,
17010
- spaciousBoundaryOption,
17011
- expansiveBoundaryOption,
17012
- balancedWaveBoundaryOption,
17013
- waveBoundaryOption,
17014
- gentleWaveBoundaryOption,
17015
- boldWaveBoundaryOption,
17016
- driftWaveBoundaryOption,
17017
- subtleAngleBoundaryOption,
17018
- balancedAngleBoundaryOption,
17019
- steepAngleBoundaryOption
17020
- ];
17009
+ function transitionVariantForEntry(entry) {
17010
+ return {
17011
+ familyId: entry.family,
17012
+ variantId: entry.variantId,
17013
+ label: entry.variantLabel,
17014
+ boundaryOption: boundaryOptionForTransitionIntent(entry.intent)
17015
+ };
17016
+ }
17017
+ const sectionTransitionFamilies = [{
17018
+ id: "none",
17019
+ label: "None",
17020
+ variants: [noneTransitionVariant]
17021
+ }, ...transitionCatalogueFamilies.map((family) => ({
17022
+ id: family.id,
17023
+ label: family.label,
17024
+ variants: requireNonEmptyReadonlyArray(transitionCatalogueEntries.filter((entry) => entry.family === family.id).map(transitionVariantForEntry), `Transition family ${family.id} has no catalogue entries.`)
17025
+ }))];
17026
+ const boundaryOptionById = new Map(boundaryOptions.map((option) => [option.id, option]));
17027
+ const boundaryOptionByTreatmentKey = new Map(boundaryOptions.map((option) => [stableTreatmentKey(option.treatment), option]));
17028
+ const transitionVariantByBoundaryOptionId = new Map(sectionTransitionFamilies.flatMap((family) => family.variants.map((variant) => [variant.boundaryOption.id, variant])));
17029
+ const transitionFamilyById = new Map(sectionTransitionFamilies.map((family) => [family.id, family]));
17021
17030
  function sectionTransitionVariantForBoundaryOptionId(boundaryOptionId) {
17022
17031
  if (!boundaryOptionId) return null;
17023
- for (const family of sectionTransitionFamilies) {
17024
- const variant = family.variants.find((candidate$1) => candidate$1.boundaryOption.id === boundaryOptionId);
17025
- if (variant) return variant;
17026
- }
17027
- return null;
17032
+ return transitionVariantByBoundaryOptionId.get(boundaryOptionId) ?? null;
17028
17033
  }
17029
17034
  function boundaryTreatmentForBoundaryOptionId(boundaryOptionId) {
17030
- return boundaryOptions.find((option) => option.id === boundaryOptionId)?.treatment ?? {
17035
+ return boundaryOptionById.get(boundaryOptionId)?.treatment ?? {
17031
17036
  kind: "legacy-unsupported",
17032
17037
  boundaryOptionId
17033
17038
  };
@@ -17036,11 +17041,19 @@ function boundaryOptionForTreatment(treatment) {
17036
17041
  if (treatment.kind === "legacy-unsupported") {
17037
17042
  return null;
17038
17043
  }
17039
- return boundaryOptions.find((option) => sectionBoundaryTreatmentsEqual(option.treatment, treatment)) ?? null;
17044
+ return boundaryOptionByTreatmentKey.get(stableTreatmentKey(treatment)) ?? null;
17040
17045
  }
17041
17046
  function boundaryOptionIdForTreatment(treatment) {
17042
17047
  return boundaryOptionForTreatment(treatment)?.id ?? (treatment.kind === "legacy-unsupported" ? treatment.boundaryOptionId : null);
17043
17048
  }
17049
+ /**
17050
+ * Supported decorative intent for a structured treatment, or null for `none`,
17051
+ * legacy-unsupported pins and structured treatments the catalogue lacks.
17052
+ */
17053
+ function transitionIntentForTreatment(treatment) {
17054
+ const transition = boundaryOptionForTreatment(treatment)?.transitionAfter;
17055
+ return transition && isDecorativeTransitionIntent(transition) ? transition : null;
17056
+ }
17044
17057
  function sectionBoundaryTreatmentsEqual(left, right) {
17045
17058
  return stableTreatmentKey(left) === stableTreatmentKey(right);
17046
17059
  }
@@ -17067,10 +17080,10 @@ function stableTreatmentKey(treatment) {
17067
17080
  function sectionTransitionFamilyForBoundaryOptionId(boundaryOptionId) {
17068
17081
  const variant = sectionTransitionVariantForBoundaryOptionId(boundaryOptionId);
17069
17082
  if (!variant) return null;
17070
- return sectionTransitionFamilies.find((family) => family.id === variant.familyId) ?? null;
17083
+ return transitionFamilyById.get(variant.familyId) ?? null;
17071
17084
  }
17072
17085
  function defaultSectionTransitionVariantForFamily(familyId) {
17073
- const family = sectionTransitionFamilies.find((candidate$1) => candidate$1.id === familyId);
17086
+ const family = transitionFamilyById.get(familyId);
17074
17087
  if (!family) {
17075
17088
  throw new Error(`Unknown section transition family: ${familyId}`);
17076
17089
  }
@@ -17078,389 +17091,787 @@ function defaultSectionTransitionVariantForFamily(familyId) {
17078
17091
  }
17079
17092
 
17080
17093
  //#endregion
17081
- //#region ../theme-core/src/site-styles/pageDesignIntent.ts
17082
- const pageDesignIntentSchemaVersion = 2;
17083
- const legacyPageDesignIntentSchemaVersion = 1;
17084
- const unknownBoundaryOptionId = asBoundaryOptionId("break:unknown");
17085
- /**
17086
- * Site style is site-owned authority. Page design snapshots retain pins and
17087
- * flow choices, but must not keep resolving against a superseded site style.
17088
- */
17089
- function rebasePageDesignIntentToSiteStyle(intent, siteStyleId) {
17090
- return intent.siteStyleId === siteStyleId ? intent : {
17091
- ...intent,
17092
- siteStyleId
17094
+ //#region ../theme-core/src/site-styles/sectionBoundaryComposition.ts
17095
+ const backgroundToken = requireThemeColorToken$1("background");
17096
+ /**
17097
+ * Classifies a section from its final render content. `_sectionStyles`
17098
+ * already holds the effective background (Look default, page-design override
17099
+ * and block-authored media merged by `resolveSectionBackground`), so this only
17100
+ * reads it and never re-decides precedence. A missing or unparseable
17101
+ * background is the page background, as it is for the renderer.
17102
+ */
17103
+ function boundaryEndpointForSectionContent(content) {
17104
+ const background = parseCanonicalRuntimeBackground(content);
17105
+ if (background.kind === "none") {
17106
+ return {
17107
+ kind: "paint",
17108
+ token: backgroundToken
17109
+ };
17110
+ }
17111
+ const effective = background.background;
17112
+ switch (effective.type) {
17113
+ case "color": {
17114
+ if (effective.color === null || effective.color === undefined) {
17115
+ return {
17116
+ kind: "paint",
17117
+ token: backgroundToken
17118
+ };
17119
+ }
17120
+ const token = parseThemeColorToken(effective.color);
17121
+ return token ? {
17122
+ kind: "paint",
17123
+ token
17124
+ } : {
17125
+ kind: "unpainted",
17126
+ reason: "literal-color"
17127
+ };
17128
+ }
17129
+ case "gradient": return {
17130
+ kind: "unpainted",
17131
+ reason: "gradient"
17132
+ };
17133
+ case "image":
17134
+ case "video": return {
17135
+ kind: "unpainted",
17136
+ reason: "media"
17137
+ };
17138
+ default: return assertNever$1(effective, "Unhandled section background type");
17139
+ }
17140
+ }
17141
+ /**
17142
+ * Reads both facts from final render content in one place. `_frameStyles` is
17143
+ * the compiled frame; content without one (no page design, an SDK block)
17144
+ * draws a plain frame.
17145
+ */
17146
+ function sectionFactsForContent(content) {
17147
+ return {
17148
+ endpoint: boundaryEndpointForSectionContent(content),
17149
+ frame: frameKindForContent(content)
17093
17150
  };
17094
17151
  }
17095
- const pageDesignUserPinnedAppearanceChoiceSchema = z.object({
17096
- kind: z.literal("user-pinned"),
17097
- value: z.string().min(1).transform(asAppearancePresetId)
17098
- }).strict();
17099
- const pageDesignUserPinnedBoundaryChoiceSchema = z.object({
17100
- kind: z.literal("user-pinned"),
17101
- value: z.string().min(1).transform(asBoundaryOptionId)
17102
- }).strict();
17103
- const pageDesignBoundaryTreatmentSchema = z.discriminatedUnion("kind", [
17104
- z.object({ kind: z.literal("none") }).strict(),
17105
- z.object({
17106
- kind: z.literal("soft-fade"),
17107
- height: z.enum([
17108
- "compact",
17109
- "medium",
17110
- "large",
17111
- "expansive"
17112
- ]),
17113
- curve: z.enum([
17114
- "soft",
17115
- "spacious",
17116
- "expansive"
17117
- ])
17118
- }).strict(),
17119
- z.object({
17120
- kind: z.literal("wave-edge"),
17121
- height: z.enum([
17122
- "compact",
17123
- "medium",
17124
- "large"
17125
- ]),
17126
- amplitude: z.enum([
17127
- "subtle",
17128
- "balanced",
17129
- "bold"
17130
- ]),
17131
- rhythm: z.enum(["single", "double"]),
17132
- symmetry: z.enum([
17133
- "symmetrical",
17134
- "asymmetrical",
17135
- "drift"
17136
- ])
17137
- }).strict(),
17138
- z.object({
17139
- kind: z.literal("angle-edge"),
17140
- height: z.enum([
17141
- "compact",
17142
- "medium",
17143
- "large"
17144
- ]),
17145
- slope: z.enum([
17146
- "subtle",
17147
- "balanced",
17148
- "steep"
17149
- ]),
17150
- direction: z.enum(["forward", "reverse"])
17151
- }).strict(),
17152
- z.object({
17153
- kind: z.literal("legacy-unsupported"),
17154
- boundaryOptionId: z.string().min(1).transform(asBoundaryOptionId)
17155
- }).strict()
17156
- ]);
17157
- const pageDesignUserPinnedBoundaryTreatmentChoiceSchema = z.object({
17158
- kind: z.literal("user-pinned"),
17159
- value: pageDesignBoundaryTreatmentSchema
17160
- }).strict();
17161
- const pageDesignBlockLookPinSchema = z.object({
17162
- blockId: z.string().min(1).transform(asDesignBlockId),
17163
- blockKind: z.string().min(1),
17164
- choice: pageDesignUserPinnedAppearanceChoiceSchema
17165
- }).strict();
17166
- const pageDesignBoundaryPinSchema = z.object({
17167
- boundaryId: z.string().min(1).transform(asDesignBoundaryId),
17168
- previousBlockId: z.string().min(1).transform(asDesignBlockId),
17169
- nextBlockId: z.string().min(1).transform(asDesignBlockId),
17170
- choice: pageDesignUserPinnedBoundaryTreatmentChoiceSchema
17171
- }).strict().superRefine(validateBoundaryPinIdMatchesBlocks);
17172
- const legacyPageDesignBoundaryPinSchema = z.object({
17173
- boundaryId: z.string().min(1).transform(asDesignBoundaryId),
17174
- previousBlockId: z.string().min(1).transform(asDesignBlockId),
17175
- nextBlockId: z.string().min(1).transform(asDesignBlockId),
17176
- choice: pageDesignUserPinnedBoundaryChoiceSchema
17177
- }).strict().superRefine(validateBoundaryPinIdMatchesBlocks);
17152
+ function frameKindForContent(content) {
17153
+ const frameStyles = content._frameStyles;
17154
+ const kind = typeof frameStyles === "object" && frameStyles !== null && "kind" in frameStyles ? frameStyles.kind : null;
17155
+ return kind === "panel" || kind === "flush-panel" ? kind : "plain";
17156
+ }
17178
17157
  /**
17179
- * Persisted editor intent stores only user-pinned block Looks and boundary choices.
17180
- * Missing pins mean `auto`; block-kind/preset compatibility is enforced by resolution,
17181
- * not by this JSON parse boundary.
17158
+ * The frame rule: a flush panel owns the edge it runs to, so no break is
17159
+ * drawn next to one. The single home for this rule; the generated
17160
+ * pipeline's budget and the site-style lab ask it with their plan frames.
17182
17161
  */
17183
- const pageDesignIntentV1Schema = z.object({
17184
- schemaVersion: z.literal(legacyPageDesignIntentSchemaVersion),
17185
- siteStyleId: z.string().min(1).transform(asSiteStyleId),
17186
- pageFlowPresetId: z.string().min(1).transform(asPageFlowPresetId),
17187
- blockLookPins: z.array(pageDesignBlockLookPinSchema),
17188
- boundaryPins: z.array(legacyPageDesignBoundaryPinSchema)
17189
- }).strict().superRefine(validateUniquePageDesignPins);
17190
- const pageDesignIntentV2Schema = z.object({
17191
- schemaVersion: z.literal(pageDesignIntentSchemaVersion),
17192
- siteStyleId: z.string().min(1).transform(asSiteStyleId),
17193
- pageFlowPresetId: z.string().min(1).transform(asPageFlowPresetId),
17194
- blockLookPins: z.array(pageDesignBlockLookPinSchema),
17195
- boundaryPins: z.array(pageDesignBoundaryPinSchema)
17196
- }).strict().superRefine(validateUniquePageDesignPins);
17197
- function parsePageDesignIntent(value, options = {}) {
17198
- if (isLegacyPageDesignIntentCandidate(value)) {
17199
- const legacyParsed = parseSchemaVersionedJson({
17200
- value,
17201
- schema: pageDesignIntentV1Schema,
17202
- expectedSchemaVersion: legacyPageDesignIntentSchemaVersion,
17203
- unsupportedSchemaVersionMessage: options.unsupportedSchemaVersionMessage ?? "Unsupported Theme V2 page design intent schemaVersion.",
17204
- invalidShapeMessage: options.invalidShapeMessage ?? "Invalid Theme V2 page design intent shape."
17205
- });
17206
- switch (legacyParsed.kind) {
17207
- case "absent": return legacyParsed;
17208
- case "parsed": return {
17209
- kind: "parsed",
17210
- intent: normalizeLegacyPageDesignIntent(legacyParsed.value)
17211
- };
17212
- case "invalid": return legacyParsed;
17213
- default: return assertNever$1(legacyParsed, "Unhandled legacy page design intent JSON parse result");
17214
- }
17162
+ function frameNotShownReason(previous, next) {
17163
+ return previous === "flush-panel" || next === "flush-panel" ? "flush-panel" : null;
17164
+ }
17165
+ /**
17166
+ * Decides whether and how a join draws. The frame rule wins before paint is
17167
+ * considered; two unpainted sides show nothing.
17168
+ */
17169
+ function composeBoundary(previous, next) {
17170
+ const frameReason = frameNotShownReason(previous.frame, next.frame);
17171
+ if (frameReason) {
17172
+ return {
17173
+ kind: "not-shown",
17174
+ reason: frameReason
17175
+ };
17215
17176
  }
17216
- const parsed = parseSchemaVersionedJson({
17217
- value,
17218
- schema: pageDesignIntentV2Schema,
17219
- expectedSchemaVersion: pageDesignIntentSchemaVersion,
17220
- unsupportedSchemaVersionMessage: options.unsupportedSchemaVersionMessage ?? "Unsupported Theme V2 page design intent schemaVersion.",
17221
- invalidShapeMessage: options.invalidShapeMessage ?? "Invalid Theme V2 page design intent shape."
17222
- });
17223
- switch (parsed.kind) {
17224
- case "absent": return parsed;
17225
- case "parsed": return {
17226
- kind: "parsed",
17227
- intent: parsed.value
17177
+ const from = previous.endpoint;
17178
+ const to = next.endpoint;
17179
+ if (from.kind === "paint") {
17180
+ return {
17181
+ kind: "shown",
17182
+ composition: to.kind === "paint" ? {
17183
+ kind: "paint-to-paint",
17184
+ from: from.token,
17185
+ to: to.token
17186
+ } : {
17187
+ kind: "paint-to-unpainted",
17188
+ from: from.token
17189
+ }
17228
17190
  };
17229
- case "invalid": return parsed;
17230
- default: return assertNever$1(parsed, "Unhandled page design intent JSON parse result");
17231
17191
  }
17232
- }
17233
- function isLegacyPageDesignIntentCandidate(value) {
17234
- return typeof value === "object" && value !== null && "schemaVersion" in value && value.schemaVersion === legacyPageDesignIntentSchemaVersion;
17235
- }
17236
- function normalizeLegacyPageDesignIntent(intent) {
17237
- return {
17238
- schemaVersion: pageDesignIntentSchemaVersion,
17239
- siteStyleId: intent.siteStyleId,
17240
- pageFlowPresetId: intent.pageFlowPresetId,
17241
- blockLookPins: intent.blockLookPins,
17242
- boundaryPins: intent.boundaryPins.map((pin) => ({
17243
- boundaryId: pin.boundaryId,
17244
- previousBlockId: pin.previousBlockId,
17245
- nextBlockId: pin.nextBlockId,
17246
- choice: userPinnedDesignChoice(boundaryTreatmentForBoundaryOptionId(pin.choice.value))
17247
- }))
17192
+ return to.kind === "paint" ? {
17193
+ kind: "shown",
17194
+ composition: {
17195
+ kind: "unpainted-to-paint",
17196
+ to: to.token
17197
+ }
17198
+ } : {
17199
+ kind: "not-shown",
17200
+ reason: "both-unpainted"
17248
17201
  };
17249
17202
  }
17250
- function autoDesignChoice() {
17251
- return { kind: "auto" };
17203
+ /**
17204
+ * Placement, reserves and paint for a shown boundary, derived from the
17205
+ * composition and the catalogue's metrics for the treatment.
17206
+ */
17207
+ function boundaryLayout(intent, composition) {
17208
+ const metrics = boundaryRuntimeMetricsForDecorativeTransition(intent);
17209
+ switch (composition.kind) {
17210
+ case "paint-to-paint": return {
17211
+ placement: "centred",
17212
+ reserves: {
17213
+ before: metrics.reserveCss,
17214
+ after: metrics.reserveCss
17215
+ },
17216
+ paint: {
17217
+ band: composition.from,
17218
+ shape: composition.to
17219
+ }
17220
+ };
17221
+ case "unpainted-to-paint": return {
17222
+ placement: "previous",
17223
+ reserves: {
17224
+ before: metrics.heightCss,
17225
+ after: null
17226
+ },
17227
+ paint: {
17228
+ band: null,
17229
+ shape: composition.to
17230
+ }
17231
+ };
17232
+ case "paint-to-unpainted": return {
17233
+ placement: "next",
17234
+ reserves: {
17235
+ before: null,
17236
+ after: metrics.heightCss
17237
+ },
17238
+ paint: {
17239
+ band: null,
17240
+ shape: composition.from
17241
+ }
17242
+ };
17243
+ default: return assertNever$1(composition, "Unhandled boundary composition");
17244
+ }
17252
17245
  }
17253
- function userPinnedDesignChoice(value) {
17246
+ function requireThemeColorToken$1(value) {
17247
+ const token = parseThemeColorToken(value);
17248
+ if (!token) {
17249
+ throw new Error(`Expected a theme color token. Received: ${value}`);
17250
+ }
17251
+ return token;
17252
+ }
17253
+
17254
+ //#endregion
17255
+ //#region ../theme-core/src/site-styles/compositionBudget.ts
17256
+ const repairActionsByRatioIssueKind = {
17257
+ "accent-density-exceeded": ["reduce-accent-density"],
17258
+ "panel-density-exceeded": ["reduce-panel-density"],
17259
+ "transition-mix-exceeded": ["reduce-transition-mix"],
17260
+ "media-density-exceeded": ["reduce-media-density"],
17261
+ "decorative-repetition-exceeded": ["reduce-decorative-repetition"]
17262
+ };
17263
+ function issueMetadata(policy, allowedRepairActions) {
17254
17264
  return {
17255
- kind: "user-pinned",
17256
- value
17265
+ policy,
17266
+ severity: designIssueSeverityForPolicy(policy),
17267
+ allowedRepairActions
17257
17268
  };
17258
17269
  }
17259
- function resolvePageDesignBlockLookChoice(intent, blockId) {
17260
- return intent.blockLookPins.find((pin) => pin.blockId === blockId)?.choice ?? autoDesignChoice();
17270
+ /** Returns the fraction of page blocks selected by a budget check. */
17271
+ function ratio(count, total) {
17272
+ return total === 0 ? 0 : count / total;
17261
17273
  }
17262
- function resolvePageDesignBoundaryChoice(intent, previousBlockId, nextBlockId) {
17263
- return intent.boundaryPins.find((pin) => pin.previousBlockId === previousBlockId && pin.nextBlockId === nextBlockId)?.choice ?? autoDesignChoice();
17274
+ function maxCompositionItemsWithinRatio(totalItems, allowedRatio) {
17275
+ return Math.floor((allowedRatio + Number.EPSILON) * totalItems);
17264
17276
  }
17265
- function buildBoundaryPin(previousBlockId, nextBlockId, choice) {
17277
+ function isRatioExceeded(actualCount, totalItems, allowedRatio) {
17278
+ return actualCount > maxCompositionItemsWithinRatio(totalItems, allowedRatio);
17279
+ }
17280
+ function issueWhenRatioExceeded(input) {
17281
+ const actualRatio = ratio(input.blockIds.length, input.totalBlocks);
17282
+ if (!isRatioExceeded(input.blockIds.length, input.totalBlocks, input.allowedRatio)) {
17283
+ return null;
17284
+ }
17266
17285
  return {
17267
- boundaryId: makeBoundaryId(previousBlockId, nextBlockId),
17268
- previousBlockId,
17269
- nextBlockId,
17270
- choice
17286
+ kind: input.kind,
17287
+ budget: input.budget,
17288
+ actualRatio,
17289
+ allowedRatio: input.allowedRatio,
17290
+ blockIds: input.blockIds,
17291
+ ...issueMetadata(input.policy, repairActionsByRatioIssueKind[input.kind])
17271
17292
  };
17272
17293
  }
17273
- function reducePageDesignIntentForStructureEvent(intent, event) {
17274
- switch (event.kind) {
17275
- case "block-added": return {
17276
- intent,
17277
- issues: []
17278
- };
17279
- case "block-removed": return removeBlockFromPageDesignIntent(intent, event.blockId);
17280
- case "blocks-reordered": return pruneBoundaryPinsForOrder(intent, event.orderedBlockIds);
17281
- case "block-kind-changed": return changeBlockKindInPageDesignIntent(intent, event);
17282
- case "block-duplicated": return duplicateBlockInPageDesignIntent(intent, event);
17283
- default: return assertNever$1(event, "Unhandled page design structure event");
17284
- }
17294
+ /** Checks whether block framing or a card surface contributes panel mass. */
17295
+ function hasPanelFrameOrCardSurface(block) {
17296
+ return block.appliedContentFrame.kind !== "plain" || block.appliedCardRole != null && block.appliedCardRole !== "none";
17285
17297
  }
17286
- function reducePageDesignIntentForUserAction(intent, action) {
17287
- switch (action.kind) {
17288
- case "set-page-flow": return {
17289
- ...intent,
17290
- pageFlowPresetId: action.pageFlowPresetId
17291
- };
17292
- case "set-block-look": return setBlockLookChoice(intent, action.block, action.choice);
17293
- case "set-boundary": return setBoundaryChoice(intent, action.previousBlockId, action.nextBlockId, action.choice);
17294
- default: return assertNever$1(action, "Unhandled page design user action");
17295
- }
17298
+ /** Checks explicit decorative annotations; transitions are budgeted separately. */
17299
+ function hasDecorativeAnnotation(context) {
17300
+ return context?.decorativeIntent === "supporting" || context?.decorativeIntent === "feature";
17296
17301
  }
17297
- function remapPageDesignIntentForDuplicate(intent, input) {
17298
- const targetBySourceId = new Map(input.blockMappings.map((mapping) => [mapping.sourceBlockId, {
17299
- blockId: mapping.targetBlockId,
17300
- blockKind: mapping.targetBlockKind
17301
- }]));
17302
- const targetBlockOrder = input.blockMappings.map((mapping) => mapping.targetBlockId);
17303
- const adjacentTargetPairs = adjacentPairKeySet(targetBlockOrder);
17304
- const blockLookPins = intent.blockLookPins.flatMap((pin) => {
17305
- const target = targetBySourceId.get(pin.blockId);
17306
- return target ? [{
17307
- blockId: target.blockId,
17308
- blockKind: target.blockKind,
17309
- choice: pin.choice
17310
- }] : [];
17302
+ /** Checks whether a block annotation says media contributes visual weight. */
17303
+ function hasMediaAnnotation(context) {
17304
+ return context?.mediaIntent === "supporting" || context?.mediaIntent === "dominant";
17305
+ }
17306
+ function validateCompositionBudget(input) {
17307
+ const { budget, policy, resolved } = input;
17308
+ const totalBlocks = resolved.blocks.length;
17309
+ const knownBlockIds = new Set(resolved.blocks.map((block) => block.blockId));
17310
+ const blockContexts = input.blockContexts ?? [];
17311
+ const unknownContextBlockIds = blockContexts.filter((context) => !knownBlockIds.has(context.blockId)).map((context) => context.blockId);
17312
+ const knownBlockContexts = blockContexts.filter((context) => knownBlockIds.has(context.blockId));
17313
+ const contextsByBlockId = new Map(knownBlockContexts.map((context) => [context.blockId, context]));
17314
+ const issues = [];
17315
+ if (unknownContextBlockIds.length > 0) {
17316
+ issues.push({
17317
+ kind: "unknown-composition-context-block",
17318
+ blockIds: unknownContextBlockIds,
17319
+ ...issueMetadata(policy, [])
17320
+ });
17321
+ }
17322
+ const accentBlockIds = resolved.blocks.filter((block) => block.appliedSectionSurface === "accent-band" || block.appliedSectionSurface === "brand-depth-band").map((block) => block.blockId);
17323
+ const accentIssue = issueWhenRatioExceeded({
17324
+ kind: "accent-density-exceeded",
17325
+ policy,
17326
+ budget: budget.accentDensity,
17327
+ blockIds: accentBlockIds,
17328
+ totalBlocks,
17329
+ allowedRatio: compositionDensityRatioLimits[budget.accentDensity]
17311
17330
  });
17312
- const boundaryPins = intent.boundaryPins.flatMap((pin) => {
17313
- const targetPrevious = targetBySourceId.get(pin.previousBlockId);
17314
- const targetNext = targetBySourceId.get(pin.nextBlockId);
17315
- if (!targetPrevious || !targetNext) {
17316
- return [];
17317
- }
17318
- if (!adjacentTargetPairs.has(boundaryKey(targetPrevious.blockId, targetNext.blockId))) {
17319
- return [];
17320
- }
17321
- return [buildBoundaryPin(targetPrevious.blockId, targetNext.blockId, pin.choice)];
17331
+ if (accentIssue) {
17332
+ issues.push(accentIssue);
17333
+ }
17334
+ const panelBlockIds = resolved.blocks.filter(hasPanelFrameOrCardSurface).map((block) => block.blockId);
17335
+ const panelIssue = issueWhenRatioExceeded({
17336
+ kind: "panel-density-exceeded",
17337
+ policy,
17338
+ budget: budget.panelDensity,
17339
+ blockIds: panelBlockIds,
17340
+ totalBlocks,
17341
+ allowedRatio: compositionDensityRatioLimits[budget.panelDensity]
17342
+ });
17343
+ if (panelIssue) {
17344
+ issues.push(panelIssue);
17345
+ }
17346
+ const frameByBlockId = new Map(input.resolved.blocks.map((block) => [block.blockId, block.appliedContentFrame.kind]));
17347
+ const transitionBlockIds = input.joins.filter((join) => {
17348
+ const previous = frameByBlockId.get(join.previousBlockId);
17349
+ const next = frameByBlockId.get(join.nextBlockId);
17350
+ return previous !== undefined && next !== undefined && frameNotShownReason(previous, next) === null;
17351
+ }).map((join) => join.previousBlockId);
17352
+ const transitionIssue = issueWhenRatioExceeded({
17353
+ kind: "transition-mix-exceeded",
17354
+ policy,
17355
+ budget: budget.transitionMix,
17356
+ blockIds: transitionBlockIds,
17357
+ totalBlocks,
17358
+ allowedRatio: compositionTransitionMixRatioLimits[budget.transitionMix]
17359
+ });
17360
+ if (transitionIssue) {
17361
+ issues.push(transitionIssue);
17362
+ }
17363
+ const ctaContexts = knownBlockContexts.filter((context) => (context.primaryCtaCount ?? 0) > 0);
17364
+ const actualPrimaryCtaCount = ctaContexts.reduce((sum, context) => sum + (context.primaryCtaCount ?? 0), 0);
17365
+ if (actualPrimaryCtaCount > budget.primaryCtaLimit) {
17366
+ issues.push({
17367
+ kind: "primary-cta-limit-exceeded",
17368
+ actualCount: actualPrimaryCtaCount,
17369
+ allowedCount: budget.primaryCtaLimit,
17370
+ blockIds: ctaContexts.map((context) => context.blockId),
17371
+ ...issueMetadata(policy, ["reduce-primary-ctas"])
17372
+ });
17373
+ }
17374
+ const mediaBlockIds = resolved.blocks.filter((block) => hasMediaAnnotation(contextsByBlockId.get(block.blockId))).map((block) => block.blockId);
17375
+ const mediaIssue = issueWhenRatioExceeded({
17376
+ kind: "media-density-exceeded",
17377
+ policy,
17378
+ budget: budget.mediaDensity,
17379
+ blockIds: mediaBlockIds,
17380
+ totalBlocks,
17381
+ allowedRatio: compositionDensityRatioLimits[budget.mediaDensity]
17382
+ });
17383
+ if (mediaIssue) {
17384
+ issues.push(mediaIssue);
17385
+ }
17386
+ const decorativeBlockIds = resolved.blocks.filter((block) => hasDecorativeAnnotation(contextsByBlockId.get(block.blockId))).map((block) => block.blockId);
17387
+ const decorativeIssue = issueWhenRatioExceeded({
17388
+ kind: "decorative-repetition-exceeded",
17389
+ policy,
17390
+ budget: budget.decorativeRepetition,
17391
+ blockIds: decorativeBlockIds,
17392
+ totalBlocks,
17393
+ allowedRatio: compositionDensityRatioLimits[budget.decorativeRepetition]
17322
17394
  });
17395
+ if (decorativeIssue) {
17396
+ issues.push(decorativeIssue);
17397
+ }
17398
+ return issues;
17399
+ }
17400
+
17401
+ //#endregion
17402
+ //#region ../theme-core/src/site-styles/compilationPlan.ts
17403
+ const defaultHeaderBrandPreferences = {
17404
+ brandDisplay: "auto",
17405
+ logoSize: "auto"
17406
+ };
17407
+ function headerBrandPreferencesFromTheme(theme) {
17323
17408
  return {
17324
- intent: {
17325
- ...intent,
17326
- blockLookPins,
17327
- boundaryPins
17409
+ brandDisplay: theme?.header.logo?.brandDisplay ?? defaultHeaderBrandPreferences.brandDisplay,
17410
+ logoSize: theme?.header.logo?.size ?? defaultHeaderBrandPreferences.logoSize
17411
+ };
17412
+ }
17413
+ function replaceSiteStyleCompilationPlan(input) {
17414
+ return {
17415
+ kind: "replace-site-style",
17416
+ buttons: {
17417
+ kind: "recompile",
17418
+ selection: input.buttonStyleSelection ?? null,
17419
+ reason: "site-style-replacement"
17328
17420
  },
17329
- issues: []
17421
+ header: {
17422
+ kind: "recompile",
17423
+ selection: input.headerSelection,
17424
+ brandPresentation: input.headerLogo === undefined ? { kind: "use-compiled-defaults" } : {
17425
+ kind: "apply-logo-context",
17426
+ logo: input.headerLogo
17427
+ }
17428
+ },
17429
+ footerLookId: input.footerLookId ?? null
17330
17430
  };
17331
17431
  }
17332
- function setBlockLookChoice(intent, block, choice) {
17333
- const withoutBlock = intent.blockLookPins.filter((pin) => pin.blockId !== block.blockId);
17334
- if (choice.kind === "auto") {
17335
- return {
17336
- ...intent,
17337
- blockLookPins: withoutBlock
17338
- };
17432
+ /**
17433
+ * Writes the brand display / logo size preference onto the header's logo style.
17434
+ *
17435
+ * Moved here from preview-next in #1318 Phase 4a so the header preference
17436
+ * pipeline can be composed once, in theme-core, rather than half in the client.
17437
+ */
17438
+ function withHeaderBrandThemePreferences(theme, preferences) {
17439
+ const currentLogo = theme.header.logo;
17440
+ if (!currentLogo && preferences.brandDisplay === defaultHeaderBrandPreferences.brandDisplay && preferences.logoSize === defaultHeaderBrandPreferences.logoSize) {
17441
+ return theme;
17339
17442
  }
17340
17443
  return {
17341
- ...intent,
17342
- blockLookPins: [...withoutBlock, {
17343
- blockId: block.blockId,
17344
- blockKind: block.blockKind,
17345
- choice
17346
- }]
17444
+ ...theme,
17445
+ header: {
17446
+ ...theme.header,
17447
+ logo: {
17448
+ fontFamily: currentLogo?.fontFamily ?? "heading",
17449
+ letterSpacing: currentLogo?.letterSpacing ?? "normal",
17450
+ gradient: currentLogo?.gradient ?? false,
17451
+ ...currentLogo ?? {},
17452
+ brandDisplay: preferences.brandDisplay,
17453
+ size: preferences.logoSize
17454
+ }
17455
+ }
17347
17456
  };
17348
17457
  }
17349
- function setBoundaryChoice(intent, previousBlockId, nextBlockId, choice) {
17350
- const withoutBoundary = intent.boundaryPins.filter((pin) => pin.previousBlockId !== previousBlockId || pin.nextBlockId !== nextBlockId);
17351
- if (choice.kind === "auto") {
17458
+
17459
+ //#endregion
17460
+ //#region ../theme-core/src/site-styles/schemaVersionedJson.ts
17461
+ function parseSchemaVersionedJson(input) {
17462
+ if (input.value == null) {
17463
+ return { kind: "absent" };
17464
+ }
17465
+ if (typeof input.value === "object" && input.value !== null && "schemaVersion" in input.value && input.value.schemaVersion !== input.expectedSchemaVersion) {
17352
17466
  return {
17353
- ...intent,
17354
- boundaryPins: withoutBoundary
17467
+ kind: "invalid",
17468
+ issue: {
17469
+ kind: "unsupported-schema-version",
17470
+ message: input.unsupportedSchemaVersionMessage
17471
+ }
17472
+ };
17473
+ }
17474
+ const parsed = input.schema.safeParse(input.value);
17475
+ if (!parsed.success) {
17476
+ const firstIssue = parsed.error.issues[0];
17477
+ return {
17478
+ kind: "invalid",
17479
+ issue: {
17480
+ kind: "invalid-json-shape",
17481
+ message: firstIssue?.message ?? input.invalidShapeMessage
17482
+ }
17355
17483
  };
17356
17484
  }
17357
17485
  return {
17486
+ kind: "parsed",
17487
+ value: parsed.data
17488
+ };
17489
+ }
17490
+
17491
+ //#endregion
17492
+ //#region ../theme-core/src/site-styles/pageDesignIntent.ts
17493
+ const pageDesignIntentSchemaVersion = 2;
17494
+ const legacyPageDesignIntentSchemaVersion = 1;
17495
+ const unknownBoundaryOptionId = asBoundaryOptionId("break:unknown");
17496
+ /**
17497
+ * Site style is site-owned authority. Page design snapshots retain pins and
17498
+ * flow choices, but must not keep resolving against a superseded site style.
17499
+ */
17500
+ function rebasePageDesignIntentToSiteStyle(intent, siteStyleId) {
17501
+ return intent.siteStyleId === siteStyleId ? intent : {
17358
17502
  ...intent,
17359
- boundaryPins: [...withoutBoundary, buildBoundaryPin(previousBlockId, nextBlockId, choice)]
17503
+ siteStyleId
17360
17504
  };
17361
17505
  }
17362
- function removeBlockFromPageDesignIntent(intent, blockId) {
17363
- const removedLookIssues = intent.blockLookPins.flatMap((pin) => pin.blockId === blockId ? [{
17364
- kind: "removed-look-pin-for-deleted-block",
17365
- blockId,
17366
- presetId: pin.choice.value,
17367
- feedback: "debug"
17368
- }] : []);
17369
- const removedBoundaryIssues = intent.boundaryPins.flatMap((pin) => pin.previousBlockId === blockId || pin.nextBlockId === blockId ? [{
17370
- kind: "removed-boundary-pin-for-broken-adjacency",
17371
- boundaryId: pin.boundaryId,
17372
- previousBlockId: pin.previousBlockId,
17373
- nextBlockId: pin.nextBlockId,
17374
- presetId: boundaryIssuePresetId(pin.choice.value),
17375
- feedback: "debug"
17376
- }] : []);
17506
+ const pageDesignUserPinnedAppearanceChoiceSchema = z.object({
17507
+ kind: z.literal("user-pinned"),
17508
+ value: z.string().min(1).transform(asAppearancePresetId)
17509
+ }).strict();
17510
+ const pageDesignUserPinnedBoundaryChoiceSchema = z.object({
17511
+ kind: z.literal("user-pinned"),
17512
+ value: z.string().min(1).transform(asBoundaryOptionId)
17513
+ }).strict();
17514
+ const pageDesignBoundaryTreatmentSchema = z.discriminatedUnion("kind", [
17515
+ z.object({ kind: z.literal("none") }).strict(),
17516
+ z.object({
17517
+ kind: z.literal("soft-fade"),
17518
+ height: z.enum([
17519
+ "compact",
17520
+ "medium",
17521
+ "large",
17522
+ "expansive"
17523
+ ]),
17524
+ curve: z.enum([
17525
+ "soft",
17526
+ "spacious",
17527
+ "expansive"
17528
+ ])
17529
+ }).strict(),
17530
+ z.object({
17531
+ kind: z.literal("wave-edge"),
17532
+ height: z.enum([
17533
+ "compact",
17534
+ "medium",
17535
+ "large"
17536
+ ]),
17537
+ amplitude: z.enum([
17538
+ "subtle",
17539
+ "balanced",
17540
+ "bold"
17541
+ ]),
17542
+ rhythm: z.enum(["single", "double"]),
17543
+ symmetry: z.enum([
17544
+ "symmetrical",
17545
+ "asymmetrical",
17546
+ "drift"
17547
+ ])
17548
+ }).strict(),
17549
+ z.object({
17550
+ kind: z.literal("angle-edge"),
17551
+ height: z.enum([
17552
+ "compact",
17553
+ "medium",
17554
+ "large"
17555
+ ]),
17556
+ slope: z.enum([
17557
+ "subtle",
17558
+ "balanced",
17559
+ "steep"
17560
+ ]),
17561
+ direction: z.enum(["forward", "reverse"])
17562
+ }).strict(),
17563
+ z.object({
17564
+ kind: z.literal("legacy-unsupported"),
17565
+ boundaryOptionId: z.string().min(1).transform(asBoundaryOptionId)
17566
+ }).strict()
17567
+ ]);
17568
+ const pageDesignUserPinnedBoundaryTreatmentChoiceSchema = z.object({
17569
+ kind: z.literal("user-pinned"),
17570
+ value: pageDesignBoundaryTreatmentSchema
17571
+ }).strict();
17572
+ const pageDesignBlockLookPinSchema = z.object({
17573
+ blockId: z.string().min(1).transform(asDesignBlockId),
17574
+ blockKind: z.string().min(1),
17575
+ choice: pageDesignUserPinnedAppearanceChoiceSchema
17576
+ }).strict();
17577
+ const pageDesignBoundaryPinSchema = z.object({
17578
+ boundaryId: z.string().min(1).transform(asDesignBoundaryId),
17579
+ previousBlockId: z.string().min(1).transform(asDesignBlockId),
17580
+ nextBlockId: z.string().min(1).transform(asDesignBlockId),
17581
+ choice: pageDesignUserPinnedBoundaryTreatmentChoiceSchema
17582
+ }).strict().superRefine(validateBoundaryPinIdMatchesBlocks);
17583
+ const legacyPageDesignBoundaryPinSchema = z.object({
17584
+ boundaryId: z.string().min(1).transform(asDesignBoundaryId),
17585
+ previousBlockId: z.string().min(1).transform(asDesignBlockId),
17586
+ nextBlockId: z.string().min(1).transform(asDesignBlockId),
17587
+ choice: pageDesignUserPinnedBoundaryChoiceSchema
17588
+ }).strict().superRefine(validateBoundaryPinIdMatchesBlocks);
17589
+ /**
17590
+ * Persisted editor intent stores only user-pinned block Looks and boundary choices.
17591
+ * Missing pins mean `auto`; block-kind/preset compatibility is enforced by resolution,
17592
+ * not by this JSON parse boundary.
17593
+ */
17594
+ const pageDesignIntentV1Schema = z.object({
17595
+ schemaVersion: z.literal(legacyPageDesignIntentSchemaVersion),
17596
+ siteStyleId: z.string().min(1).transform(asSiteStyleId),
17597
+ pageFlowPresetId: z.string().min(1).transform(asPageFlowPresetId),
17598
+ blockLookPins: z.array(pageDesignBlockLookPinSchema),
17599
+ boundaryPins: z.array(legacyPageDesignBoundaryPinSchema)
17600
+ }).strict().superRefine(validateUniquePageDesignPins);
17601
+ const pageDesignIntentV2Schema = z.object({
17602
+ schemaVersion: z.literal(pageDesignIntentSchemaVersion),
17603
+ siteStyleId: z.string().min(1).transform(asSiteStyleId),
17604
+ pageFlowPresetId: z.string().min(1).transform(asPageFlowPresetId),
17605
+ blockLookPins: z.array(pageDesignBlockLookPinSchema),
17606
+ boundaryPins: z.array(pageDesignBoundaryPinSchema)
17607
+ }).strict().superRefine(validateUniquePageDesignPins);
17608
+ function parsePageDesignIntent(value, options = {}) {
17609
+ if (isLegacyPageDesignIntentCandidate(value)) {
17610
+ const legacyParsed = parseSchemaVersionedJson({
17611
+ value,
17612
+ schema: pageDesignIntentV1Schema,
17613
+ expectedSchemaVersion: legacyPageDesignIntentSchemaVersion,
17614
+ unsupportedSchemaVersionMessage: options.unsupportedSchemaVersionMessage ?? "Unsupported Theme V2 page design intent schemaVersion.",
17615
+ invalidShapeMessage: options.invalidShapeMessage ?? "Invalid Theme V2 page design intent shape."
17616
+ });
17617
+ switch (legacyParsed.kind) {
17618
+ case "absent": return legacyParsed;
17619
+ case "parsed": return {
17620
+ kind: "parsed",
17621
+ intent: normalizeLegacyPageDesignIntent(legacyParsed.value)
17622
+ };
17623
+ case "invalid": return legacyParsed;
17624
+ default: return assertNever$1(legacyParsed, "Unhandled legacy page design intent JSON parse result");
17625
+ }
17626
+ }
17627
+ const parsed = parseSchemaVersionedJson({
17628
+ value,
17629
+ schema: pageDesignIntentV2Schema,
17630
+ expectedSchemaVersion: pageDesignIntentSchemaVersion,
17631
+ unsupportedSchemaVersionMessage: options.unsupportedSchemaVersionMessage ?? "Unsupported Theme V2 page design intent schemaVersion.",
17632
+ invalidShapeMessage: options.invalidShapeMessage ?? "Invalid Theme V2 page design intent shape."
17633
+ });
17634
+ switch (parsed.kind) {
17635
+ case "absent": return parsed;
17636
+ case "parsed": return {
17637
+ kind: "parsed",
17638
+ intent: parsed.value
17639
+ };
17640
+ case "invalid": return parsed;
17641
+ default: return assertNever$1(parsed, "Unhandled page design intent JSON parse result");
17642
+ }
17643
+ }
17644
+ function isLegacyPageDesignIntentCandidate(value) {
17645
+ return typeof value === "object" && value !== null && "schemaVersion" in value && value.schemaVersion === legacyPageDesignIntentSchemaVersion;
17646
+ }
17647
+ function normalizeLegacyPageDesignIntent(intent) {
17377
17648
  return {
17378
- intent: {
17379
- ...intent,
17380
- blockLookPins: intent.blockLookPins.filter((pin) => pin.blockId !== blockId),
17381
- boundaryPins: intent.boundaryPins.filter((pin) => pin.previousBlockId !== blockId && pin.nextBlockId !== blockId)
17382
- },
17383
- issues: [...removedLookIssues, ...removedBoundaryIssues]
17649
+ schemaVersion: pageDesignIntentSchemaVersion,
17650
+ siteStyleId: intent.siteStyleId,
17651
+ pageFlowPresetId: intent.pageFlowPresetId,
17652
+ blockLookPins: intent.blockLookPins,
17653
+ boundaryPins: intent.boundaryPins.map((pin) => ({
17654
+ boundaryId: pin.boundaryId,
17655
+ previousBlockId: pin.previousBlockId,
17656
+ nextBlockId: pin.nextBlockId,
17657
+ choice: userPinnedDesignChoice(boundaryTreatmentForBoundaryOptionId(pin.choice.value))
17658
+ }))
17384
17659
  };
17385
17660
  }
17386
- function pruneBoundaryPinsForOrder(intent, orderedBlockIds) {
17387
- const adjacentPairs = adjacentPairKeySet(orderedBlockIds);
17388
- const removedBoundaryIssues = intent.boundaryPins.flatMap((pin) => adjacentPairs.has(boundaryKey(pin.previousBlockId, pin.nextBlockId)) ? [] : [{
17389
- kind: "removed-boundary-pin-for-broken-adjacency",
17390
- boundaryId: pin.boundaryId,
17391
- previousBlockId: pin.previousBlockId,
17392
- nextBlockId: pin.nextBlockId,
17393
- presetId: boundaryIssuePresetId(pin.choice.value),
17394
- feedback: "debug"
17395
- }]);
17661
+ function autoDesignChoice() {
17662
+ return { kind: "auto" };
17663
+ }
17664
+ function userPinnedDesignChoice(value) {
17396
17665
  return {
17397
- intent: {
17398
- ...intent,
17399
- boundaryPins: intent.boundaryPins.filter((pin) => adjacentPairs.has(boundaryKey(pin.previousBlockId, pin.nextBlockId)))
17400
- },
17401
- issues: removedBoundaryIssues
17666
+ kind: "user-pinned",
17667
+ value
17402
17668
  };
17403
17669
  }
17404
- /**
17405
- * A block-kind change updates the pin's current block kind so future resolution sees
17406
- * the current tree. If the pinned Look is incompatible, the pin remains stored and
17407
- * the resolver must render a safe auto fallback while the user-facing issue is open.
17408
- */
17409
- function changeBlockKindInPageDesignIntent(intent, event) {
17410
- const blockLookPins = intent.blockLookPins.map((pin) => pin.blockId === event.blockId ? {
17411
- ...pin,
17412
- blockKind: event.nextBlockKind
17413
- } : pin);
17414
- const changed = intent.blockLookPins.find((pin) => pin.blockId === event.blockId);
17415
- const incompatiblePinnedLook = changed && !event.compatiblePinnedLookPresetIds.includes(changed.choice.value) ? changed : null;
17670
+ function resolvePageDesignBlockLookChoice(intent, blockId) {
17671
+ return intent.blockLookPins.find((pin) => pin.blockId === blockId)?.choice ?? autoDesignChoice();
17672
+ }
17673
+ function resolvePageDesignBoundaryChoice(intent, previousBlockId, nextBlockId) {
17674
+ return intent.boundaryPins.find((pin) => pin.previousBlockId === previousBlockId && pin.nextBlockId === nextBlockId)?.choice ?? autoDesignChoice();
17675
+ }
17676
+ function buildBoundaryPin(previousBlockId, nextBlockId, choice) {
17416
17677
  return {
17417
- intent: {
17418
- ...intent,
17419
- blockLookPins
17420
- },
17421
- issues: incompatiblePinnedLook ? [{
17422
- kind: "pinned-look-incompatible-with-block-kind",
17423
- blockId: event.blockId,
17424
- presetId: incompatiblePinnedLook.choice.value,
17425
- previousBlockKind: incompatiblePinnedLook.blockKind,
17426
- nextBlockKind: event.nextBlockKind,
17427
- feedback: "user-facing"
17428
- }] : []
17678
+ boundaryId: makeBoundaryId(previousBlockId, nextBlockId),
17679
+ previousBlockId,
17680
+ nextBlockId,
17681
+ choice
17429
17682
  };
17430
17683
  }
17431
- function boundaryIssuePresetId(treatment) {
17432
- if (typeof treatment === "string") return treatment;
17433
- return boundaryOptionIdForTreatment(treatment) ?? unknownBoundaryOptionId;
17434
- }
17435
- /**
17436
- * Ordinary block duplication does not copy boundary pins. Boundary pins represent
17437
- * exact adjacent pairs, so duplicated blocks start with automatic surrounding breaks.
17438
- */
17439
- function duplicateBlockInPageDesignIntent(intent, event) {
17440
- const sourcePin = intent.blockLookPins.find((pin) => pin.blockId === event.sourceBlockId);
17441
- if (!event.copyPinnedLook) {
17442
- return {
17684
+ function reducePageDesignIntentForStructureEvent(intent, event) {
17685
+ switch (event.kind) {
17686
+ case "block-added": return {
17443
17687
  intent,
17444
17688
  issues: []
17445
17689
  };
17690
+ case "block-removed": return removeBlockFromPageDesignIntent(intent, event.blockId);
17691
+ case "blocks-reordered": return pruneBoundaryPinsForOrder(intent, event.orderedBlockIds);
17692
+ case "block-kind-changed": return changeBlockKindInPageDesignIntent(intent, event);
17693
+ case "block-duplicated": return duplicateBlockInPageDesignIntent(intent, event);
17694
+ default: return assertNever$1(event, "Unhandled page design structure event");
17446
17695
  }
17447
- if (!sourcePin) {
17448
- return {
17449
- intent,
17450
- issues: [{
17451
- kind: "missing-look-pin-to-copy-on-duplicate",
17452
- sourceBlockId: event.sourceBlockId,
17453
- duplicatedBlockId: event.block.blockId,
17454
- feedback: "debug"
17455
- }]
17696
+ }
17697
+ function reducePageDesignIntentForUserAction(intent, action) {
17698
+ switch (action.kind) {
17699
+ case "set-page-flow": return {
17700
+ ...intent,
17701
+ pageFlowPresetId: action.pageFlowPresetId
17456
17702
  };
17703
+ case "set-block-look": return setBlockLookChoice(intent, action.block, action.choice);
17704
+ case "set-boundary": return setBoundaryChoice(intent, action.previousBlockId, action.nextBlockId, action.choice);
17705
+ default: return assertNever$1(action, "Unhandled page design user action");
17457
17706
  }
17458
- return {
17459
- intent: {
17460
- ...intent,
17461
- blockLookPins: [...intent.blockLookPins, {
17462
- blockId: event.block.blockId,
17463
- blockKind: event.block.blockKind,
17707
+ }
17708
+ function remapPageDesignIntentForDuplicate(intent, input) {
17709
+ const targetBySourceId = new Map(input.blockMappings.map((mapping) => [mapping.sourceBlockId, {
17710
+ blockId: mapping.targetBlockId,
17711
+ blockKind: mapping.targetBlockKind
17712
+ }]));
17713
+ const targetBlockOrder = input.blockMappings.map((mapping) => mapping.targetBlockId);
17714
+ const adjacentTargetPairs = adjacentPairKeySet(targetBlockOrder);
17715
+ const blockLookPins = intent.blockLookPins.flatMap((pin) => {
17716
+ const target = targetBySourceId.get(pin.blockId);
17717
+ return target ? [{
17718
+ blockId: target.blockId,
17719
+ blockKind: target.blockKind,
17720
+ choice: pin.choice
17721
+ }] : [];
17722
+ });
17723
+ const boundaryPins = intent.boundaryPins.flatMap((pin) => {
17724
+ const targetPrevious = targetBySourceId.get(pin.previousBlockId);
17725
+ const targetNext = targetBySourceId.get(pin.nextBlockId);
17726
+ if (!targetPrevious || !targetNext) {
17727
+ return [];
17728
+ }
17729
+ if (!adjacentTargetPairs.has(makeBoundaryId(targetPrevious.blockId, targetNext.blockId))) {
17730
+ return [];
17731
+ }
17732
+ return [buildBoundaryPin(targetPrevious.blockId, targetNext.blockId, pin.choice)];
17733
+ });
17734
+ return {
17735
+ intent: {
17736
+ ...intent,
17737
+ blockLookPins,
17738
+ boundaryPins
17739
+ },
17740
+ issues: []
17741
+ };
17742
+ }
17743
+ function setBlockLookChoice(intent, block, choice) {
17744
+ const withoutBlock = intent.blockLookPins.filter((pin) => pin.blockId !== block.blockId);
17745
+ if (choice.kind === "auto") {
17746
+ return {
17747
+ ...intent,
17748
+ blockLookPins: withoutBlock
17749
+ };
17750
+ }
17751
+ return {
17752
+ ...intent,
17753
+ blockLookPins: [...withoutBlock, {
17754
+ blockId: block.blockId,
17755
+ blockKind: block.blockKind,
17756
+ choice
17757
+ }]
17758
+ };
17759
+ }
17760
+ function setBoundaryChoice(intent, previousBlockId, nextBlockId, choice) {
17761
+ const withoutBoundary = intent.boundaryPins.filter((pin) => pin.previousBlockId !== previousBlockId || pin.nextBlockId !== nextBlockId);
17762
+ if (choice.kind === "auto") {
17763
+ return {
17764
+ ...intent,
17765
+ boundaryPins: withoutBoundary
17766
+ };
17767
+ }
17768
+ return {
17769
+ ...intent,
17770
+ boundaryPins: [...withoutBoundary, buildBoundaryPin(previousBlockId, nextBlockId, choice)]
17771
+ };
17772
+ }
17773
+ function removeBlockFromPageDesignIntent(intent, blockId) {
17774
+ const removedLookIssues = intent.blockLookPins.flatMap((pin) => pin.blockId === blockId ? [{
17775
+ kind: "removed-look-pin-for-deleted-block",
17776
+ blockId,
17777
+ presetId: pin.choice.value,
17778
+ feedback: "debug"
17779
+ }] : []);
17780
+ const removedBoundaryIssues = intent.boundaryPins.flatMap((pin) => pin.previousBlockId === blockId || pin.nextBlockId === blockId ? [{
17781
+ kind: "removed-boundary-pin-for-broken-adjacency",
17782
+ boundaryId: pin.boundaryId,
17783
+ previousBlockId: pin.previousBlockId,
17784
+ nextBlockId: pin.nextBlockId,
17785
+ presetId: boundaryIssuePresetId(pin.choice.value),
17786
+ feedback: "debug"
17787
+ }] : []);
17788
+ return {
17789
+ intent: {
17790
+ ...intent,
17791
+ blockLookPins: intent.blockLookPins.filter((pin) => pin.blockId !== blockId),
17792
+ boundaryPins: intent.boundaryPins.filter((pin) => pin.previousBlockId !== blockId && pin.nextBlockId !== blockId)
17793
+ },
17794
+ issues: [...removedLookIssues, ...removedBoundaryIssues]
17795
+ };
17796
+ }
17797
+ function pruneBoundaryPinsForOrder(intent, orderedBlockIds) {
17798
+ const adjacentPairs = adjacentPairKeySet(orderedBlockIds);
17799
+ const removedBoundaryIssues = intent.boundaryPins.flatMap((pin) => adjacentPairs.has(makeBoundaryId(pin.previousBlockId, pin.nextBlockId)) ? [] : [{
17800
+ kind: "removed-boundary-pin-for-broken-adjacency",
17801
+ boundaryId: pin.boundaryId,
17802
+ previousBlockId: pin.previousBlockId,
17803
+ nextBlockId: pin.nextBlockId,
17804
+ presetId: boundaryIssuePresetId(pin.choice.value),
17805
+ feedback: "debug"
17806
+ }]);
17807
+ return {
17808
+ intent: {
17809
+ ...intent,
17810
+ boundaryPins: intent.boundaryPins.filter((pin) => adjacentPairs.has(makeBoundaryId(pin.previousBlockId, pin.nextBlockId)))
17811
+ },
17812
+ issues: removedBoundaryIssues
17813
+ };
17814
+ }
17815
+ /**
17816
+ * A block-kind change updates the pin's current block kind so future resolution sees
17817
+ * the current tree. If the pinned Look is incompatible, the pin remains stored and
17818
+ * the resolver must render a safe auto fallback while the user-facing issue is open.
17819
+ */
17820
+ function changeBlockKindInPageDesignIntent(intent, event) {
17821
+ const blockLookPins = intent.blockLookPins.map((pin) => pin.blockId === event.blockId ? {
17822
+ ...pin,
17823
+ blockKind: event.nextBlockKind
17824
+ } : pin);
17825
+ const changed = intent.blockLookPins.find((pin) => pin.blockId === event.blockId);
17826
+ const incompatiblePinnedLook = changed && !event.compatiblePinnedLookPresetIds.includes(changed.choice.value) ? changed : null;
17827
+ return {
17828
+ intent: {
17829
+ ...intent,
17830
+ blockLookPins
17831
+ },
17832
+ issues: incompatiblePinnedLook ? [{
17833
+ kind: "pinned-look-incompatible-with-block-kind",
17834
+ blockId: event.blockId,
17835
+ presetId: incompatiblePinnedLook.choice.value,
17836
+ previousBlockKind: incompatiblePinnedLook.blockKind,
17837
+ nextBlockKind: event.nextBlockKind,
17838
+ feedback: "user-facing"
17839
+ }] : []
17840
+ };
17841
+ }
17842
+ function boundaryIssuePresetId(treatment) {
17843
+ if (typeof treatment === "string") return treatment;
17844
+ return boundaryOptionIdForTreatment(treatment) ?? unknownBoundaryOptionId;
17845
+ }
17846
+ /**
17847
+ * Ordinary block duplication does not copy boundary pins. Boundary pins represent
17848
+ * exact adjacent pairs, so duplicated blocks start with automatic surrounding breaks.
17849
+ */
17850
+ function duplicateBlockInPageDesignIntent(intent, event) {
17851
+ const sourcePin = intent.blockLookPins.find((pin) => pin.blockId === event.sourceBlockId);
17852
+ if (!event.copyPinnedLook) {
17853
+ return {
17854
+ intent,
17855
+ issues: []
17856
+ };
17857
+ }
17858
+ if (!sourcePin) {
17859
+ return {
17860
+ intent,
17861
+ issues: [{
17862
+ kind: "missing-look-pin-to-copy-on-duplicate",
17863
+ sourceBlockId: event.sourceBlockId,
17864
+ duplicatedBlockId: event.block.blockId,
17865
+ feedback: "debug"
17866
+ }]
17867
+ };
17868
+ }
17869
+ return {
17870
+ intent: {
17871
+ ...intent,
17872
+ blockLookPins: [...intent.blockLookPins, {
17873
+ blockId: event.block.blockId,
17874
+ blockKind: event.block.blockKind,
17464
17875
  choice: sourcePin.choice
17465
17876
  }]
17466
17877
  },
@@ -17475,7 +17886,7 @@ function adjacentPairKeySet(orderedBlockIds) {
17475
17886
  }
17476
17887
  let previousBlockId = first;
17477
17888
  for (const nextBlockId of rest) {
17478
- pairs.add(boundaryKey(previousBlockId, nextBlockId));
17889
+ pairs.add(makeBoundaryId(previousBlockId, nextBlockId));
17479
17890
  previousBlockId = nextBlockId;
17480
17891
  }
17481
17892
  return pairs;
@@ -17514,14 +17925,11 @@ function validateUniquePageDesignPins(intent, ctx) {
17514
17925
  label: "block Look pin"
17515
17926
  });
17516
17927
  addDuplicateStringIssues(ctx, {
17517
- values: intent.boundaryPins.map((pin) => boundaryKey(pin.previousBlockId, pin.nextBlockId)),
17928
+ values: intent.boundaryPins.map((pin) => makeBoundaryId(pin.previousBlockId, pin.nextBlockId)),
17518
17929
  path: ["boundaryPins"],
17519
17930
  label: "boundary pin"
17520
17931
  });
17521
17932
  }
17522
- function boundaryKey(previousBlockId, nextBlockId) {
17523
- return makeBoundaryId(previousBlockId, nextBlockId);
17524
- }
17525
17933
 
17526
17934
  //#endregion
17527
17935
  //#region ../theme-core/src/site-styles/contentTemplateDesignIntent.ts
@@ -20886,7 +21294,7 @@ function underlineNavLinkStyle(colorToken, underlineColorToken, typography = "pl
20886
21294
  }] }
20887
21295
  }, typography);
20888
21296
  }
20889
- function pillNavLinkStyle(colorToken, backgroundToken = "primary", opacity = 10, typography = "plain") {
21297
+ function pillNavLinkStyle(colorToken, backgroundToken$1 = "primary", opacity = 10, typography = "plain") {
20890
21298
  return applyNavTypography({
20891
21299
  name: "Pill",
20892
21300
  typography: "body",
@@ -20900,14 +21308,14 @@ function pillNavLinkStyle(colorToken, backgroundToken = "primary", opacity = 10,
20900
21308
  hover: [{
20901
21309
  effectId: "background-on-hover-alpha",
20902
21310
  options: {
20903
- colorToken: backgroundToken,
21311
+ colorToken: backgroundToken$1,
20904
21312
  opacity
20905
21313
  }
20906
21314
  }],
20907
21315
  active: [{
20908
21316
  effectId: "background-on-hover-alpha",
20909
21317
  options: {
20910
- colorToken: backgroundToken,
21318
+ colorToken: backgroundToken$1,
20911
21319
  opacity: opacity + 5
20912
21320
  }
20913
21321
  }]
@@ -21961,151 +22369,650 @@ function classifyRuntimeStyleFields(input) {
21961
22369
  fields: []
21962
22370
  };
21963
22371
  }
21964
- if (!input.authority) {
21965
- return {
21966
- kind: "unsupported-manual-review",
21967
- fields
21968
- };
22372
+ if (!input.authority) {
22373
+ return {
22374
+ kind: "unsupported-manual-review",
22375
+ fields
22376
+ };
22377
+ }
22378
+ switch (input.authority.kind) {
22379
+ case "theme-v2": return {
22380
+ kind: "interpretable-override",
22381
+ fields,
22382
+ authority: input.authority
22383
+ };
22384
+ case "advanced-detached": return {
22385
+ kind: "advanced-detached",
22386
+ fields,
22387
+ authority: input.authority
22388
+ };
22389
+ case "legacy-raw": return {
22390
+ kind: "unsupported-manual-review",
22391
+ fields,
22392
+ authority: input.authority
22393
+ };
22394
+ default: return assertNever$1(input.authority);
22395
+ }
22396
+ }
22397
+ function canWriteThemeV2RuntimeFields(authority) {
22398
+ return authority?.kind === "theme-v2";
22399
+ }
22400
+ function blockReasonForAuthority(authority) {
22401
+ if (!authority) {
22402
+ return "missing-theme-v2-authority";
22403
+ }
22404
+ switch (authority?.kind) {
22405
+ case "advanced-detached": return "advanced-detached";
22406
+ case "legacy-raw": return "legacy-raw";
22407
+ default: return assertNever$1(authority);
22408
+ }
22409
+ }
22410
+ /**
22411
+ * Replace every compiler-owned field on a block's content with a fresh compiled
22412
+ * set — the one primitive both writers of compiled output share.
22413
+ *
22414
+ * Replacement rather than merge is the whole point: a field the new
22415
+ * compilation does not produce must *go*, or a recompile onto a Look that asks
22416
+ * for no card leaves the old `_cardStyles` sitting there claiming a surface
22417
+ * nobody asked for. Every writer that discovers compiler ownership for itself
22418
+ * eventually forgets one of these names, so nobody does.
22419
+ *
22420
+ * Pure, and deliberately free of authority checks — those belong to the
22421
+ * persistence wrapper, not to the shape of the content.
22422
+ */
22423
+ function withCompiledRuntimeStyleFields(input) {
22424
+ const authored = Object.fromEntries(Object.entries(stripStaleTransitionStyleGroup(input.content)).filter(([fieldName]) => !themeV2CompilerOwnedFieldNames.includes(fieldName)));
22425
+ return {
22426
+ ...authored,
22427
+ ...input.fields
22428
+ };
22429
+ }
22430
+ /**
22431
+ * Persisted Theme V2 output is compile-at-write: once a block has Theme V2
22432
+ * authority, compiler-owned runtime fields are replaced in the same content
22433
+ * version. Preview-only compile-at-read callers should use compileBlockRuntimeFields
22434
+ * and avoid persisting these fields.
22435
+ */
22436
+ function replaceCompiledRuntimeStyleFields(input) {
22437
+ if (!canWriteThemeV2RuntimeFields(input.authority)) {
22438
+ return {
22439
+ kind: "blocked",
22440
+ reason: blockReasonForAuthority(input.authority),
22441
+ content: input.content
22442
+ };
22443
+ }
22444
+ return {
22445
+ kind: "applied",
22446
+ content: withCompiledRuntimeStyleFields(input)
22447
+ };
22448
+ }
22449
+ function uniqueBlockIds(values) {
22450
+ return [...new Set(values)];
22451
+ }
22452
+ function collectPageStyleAuthorityBuckets(styleAuthorities) {
22453
+ const themeV2BlockIds = [];
22454
+ const legacyRawBlockIds = [];
22455
+ const advancedDetachedBlockIds = [];
22456
+ for (const entry of styleAuthorities) {
22457
+ switch (entry.authority.kind) {
22458
+ case "theme-v2":
22459
+ themeV2BlockIds.push(entry.blockId);
22460
+ break;
22461
+ case "legacy-raw":
22462
+ legacyRawBlockIds.push(entry.blockId);
22463
+ break;
22464
+ case "advanced-detached":
22465
+ advancedDetachedBlockIds.push(entry.blockId);
22466
+ break;
22467
+ default: assertNever$1(entry.authority);
22468
+ }
22469
+ }
22470
+ return {
22471
+ themeV2BlockIds: uniqueBlockIds(themeV2BlockIds),
22472
+ legacyRawBlockIds: uniqueBlockIds(legacyRawBlockIds),
22473
+ advancedDetachedBlockIds: uniqueBlockIds(advancedDetachedBlockIds)
22474
+ };
22475
+ }
22476
+ function summarizePageStyleAuthorities(state) {
22477
+ const { themeV2BlockIds, legacyRawBlockIds, advancedDetachedBlockIds } = collectPageStyleAuthorityBuckets(state.styleAuthorities);
22478
+ if (themeV2BlockIds.length === 0 && legacyRawBlockIds.length === 0 && advancedDetachedBlockIds.length === 0) {
22479
+ return {
22480
+ kind: "no-runtime-authority",
22481
+ themeV2BlockIds: [],
22482
+ legacyRawBlockIds: [],
22483
+ advancedDetachedBlockIds: []
22484
+ };
22485
+ }
22486
+ if (legacyRawBlockIds.length > 0) {
22487
+ return {
22488
+ kind: "manual-review-required",
22489
+ themeV2BlockIds,
22490
+ legacyRawBlockIds,
22491
+ advancedDetachedBlockIds
22492
+ };
22493
+ }
22494
+ if (themeV2BlockIds.length === 0) {
22495
+ return {
22496
+ kind: "advanced-detached-only",
22497
+ themeV2BlockIds: [],
22498
+ legacyRawBlockIds: [],
22499
+ advancedDetachedBlockIds
22500
+ };
22501
+ }
22502
+ if (advancedDetachedBlockIds.length > 0) {
22503
+ return {
22504
+ kind: "mixed-authority",
22505
+ themeV2BlockIds,
22506
+ legacyRawBlockIds: [],
22507
+ advancedDetachedBlockIds
22508
+ };
22509
+ }
22510
+ return {
22511
+ kind: "theme-v2-only",
22512
+ themeV2BlockIds,
22513
+ legacyRawBlockIds: [],
22514
+ advancedDetachedBlockIds: []
22515
+ };
22516
+ }
22517
+
22518
+ //#endregion
22519
+ //#region ../theme-core/src/site-styles/pageFlowPresets.ts
22520
+ /**
22521
+ * Must remain non-empty; downstream code indexes `[0]` as the documented default.
22522
+ */
22523
+ const pageFlowPresets = [
22524
+ {
22525
+ id: asPageFlowPresetId("flow:quiet"),
22526
+ label: "Quiet",
22527
+ boundaryStrategy: "quiet",
22528
+ defaultBoundaryOptionId: noneBoundaryOption.id
22529
+ },
22530
+ {
22531
+ id: asPageFlowPresetId("flow:balanced"),
22532
+ label: "Balanced",
22533
+ boundaryStrategy: "balanced",
22534
+ defaultBoundaryOptionId: softBoundaryOption.id
22535
+ },
22536
+ {
22537
+ id: asPageFlowPresetId("flow:expressive"),
22538
+ label: "Expressive",
22539
+ boundaryStrategy: "expressive",
22540
+ defaultBoundaryOptionId: spaciousBoundaryOption.id
22541
+ }
22542
+ ];
22543
+ /**
22544
+ * The break a join gets when nothing more specific applies. An unknown preset
22545
+ * id (a stale or policy-owned one) falls back to the first boundary option,
22546
+ * and `preset` is null so callers can tell the two apart.
22547
+ */
22548
+ function pageFlowDefaultBoundary(pageFlowPresetId) {
22549
+ const preset = pageFlowPresets.find((candidate$1) => candidate$1.id === pageFlowPresetId) ?? null;
22550
+ return {
22551
+ preset,
22552
+ boundaryOptionId: preset?.defaultBoundaryOptionId ?? boundaryOptions[0].id
22553
+ };
22554
+ }
22555
+
22556
+ //#endregion
22557
+ //#region ../theme-core/src/site-styles/lookResolution.ts
22558
+ function blockAppearancePresetSupportsBlockKind(preset, blockKind) {
22559
+ return preset.blockKinds.some((candidate$1) => candidate$1 === blockKind);
22560
+ }
22561
+ function preferredAppearanceTargetForBlockKind(blockKind) {
22562
+ switch (blockKind) {
22563
+ case "block.event-calendar":
22564
+ case "block.event-listing": return "events";
22565
+ default: return null;
22566
+ }
22567
+ }
22568
+ function sortCompatibleBlockAppearancePresets(blockKind, options) {
22569
+ const preferredTarget = preferredAppearanceTargetForBlockKind(blockKind);
22570
+ if (!preferredTarget) {
22571
+ return options;
22572
+ }
22573
+ return [...options].sort((left, right) => {
22574
+ const leftPreset = appearancePresets.find((preset) => preset.id === left.id);
22575
+ const rightPreset = appearancePresets.find((preset) => preset.id === right.id);
22576
+ const leftPreferred = leftPreset?.target === preferredTarget;
22577
+ const rightPreferred = rightPreset?.target === preferredTarget;
22578
+ if (leftPreferred === rightPreferred) {
22579
+ return 0;
22580
+ }
22581
+ return leftPreferred ? -1 : 1;
22582
+ });
22583
+ }
22584
+ function blockAppearancePresetSupportsLayoutProfile(preset, profile, input = {}) {
22585
+ const constraints = preset.constraints;
22586
+ if (!constraints || !constraints.layoutFamilies && !constraints.layoutVariantKeys) {
22587
+ return true;
22588
+ }
22589
+ if (!profile) {
22590
+ return input.allowLayoutConstrainedPresetWithoutProfile ?? true;
22591
+ }
22592
+ return layoutProfileSupportsConstraints(profile, constraints);
22593
+ }
22594
+ function resolveBlockAppearancePreset(preset, layoutProfile = null) {
22595
+ return {
22596
+ id: asAppearancePresetId(preset.id),
22597
+ label: preset.label,
22598
+ design: resolveBlockAppearancePresetDesign(preset, layoutProfile)
22599
+ };
22600
+ }
22601
+ function getCompatibleBlockAppearancePresets(input) {
22602
+ const options = appearancePresets.flatMap((preset) => {
22603
+ if (!blockAppearancePresetSupportsBlockKind(preset, input.blockKind) || !blockAppearancePresetSupportsLayoutProfile(preset, input.layoutProfile ?? null, { allowLayoutConstrainedPresetWithoutProfile: input.allowLayoutConstrainedPresetWithoutProfile })) {
22604
+ return [];
22605
+ }
22606
+ return [resolveBlockAppearancePreset(preset, input.layoutProfile ?? null)];
22607
+ });
22608
+ const sortedOptions = sortCompatibleBlockAppearancePresets(input.blockKind, options);
22609
+ const recommendedIds = input.siteStyle?.recommendedAppearancePresetIds ?? [];
22610
+ if (recommendedIds.length === 0) {
22611
+ return sortedOptions;
22612
+ }
22613
+ const recommended = recommendedIds.flatMap((presetId) => sortedOptions.find((option) => option.id === presetId) ?? []);
22614
+ const alreadyIncludedIds = new Set(recommended.map((option) => option.id));
22615
+ return [...recommended, ...sortedOptions.filter((option) => !alreadyIncludedIds.has(option.id))];
22616
+ }
22617
+
22618
+ //#endregion
22619
+ //#region ../theme-core/src/site-styles/siteStyleBlockPolicies.ts
22620
+ const defaultPolicyPageFlowPresetId = asPageFlowPresetId("generated-default");
22621
+ const southWestTwerkSchoolSiteStyleId = asSiteStyleId("site-style:south-west-twerk-school");
22622
+ const heroBrandScrimFixedFrameLookId = asAppearancePresetId("hero-brand-scrim-fixed-frame");
22623
+ const classicHeroLayoutVariantKey = asLayoutVariantKey("classic");
22624
+ const microHeroLayoutVariantKey = asLayoutVariantKey("micro");
22625
+ const waveBoundaryOptionId = asBoundaryOptionId("break:wave");
22626
+ const registeredSiteStyleBlockPolicies = [{
22627
+ siteStyleId: southWestTwerkSchoolSiteStyleId,
22628
+ kind: "locked-look-and-layouts",
22629
+ blockKind: "block.hero",
22630
+ lookId: heroBrandScrimFixedFrameLookId,
22631
+ allowedLayoutVariantKeys: [classicHeroLayoutVariantKey, microHeroLayoutVariantKey],
22632
+ defaultLayoutVariantKey: classicHeroLayoutVariantKey,
22633
+ defaultBoundaryAfter: {
22634
+ optionId: waveBoundaryOptionId,
22635
+ mode: "default-only"
22636
+ },
22637
+ customize: {
22638
+ hideDisallowedLooks: true,
22639
+ hideDisallowedLayouts: true
22640
+ }
22641
+ }];
22642
+ function siteStyleBlockPolicies() {
22643
+ return registeredSiteStyleBlockPolicies;
22644
+ }
22645
+ function buildInitialPolicyPageDesignIntent(siteStyleId) {
22646
+ return {
22647
+ schemaVersion: pageDesignIntentSchemaVersion,
22648
+ siteStyleId,
22649
+ pageFlowPresetId: defaultPolicyPageFlowPresetId,
22650
+ blockLookPins: [],
22651
+ boundaryPins: []
22652
+ };
22653
+ }
22654
+ function deriveSiteStyleBlockPolicy(input) {
22655
+ return registeredSiteStyleBlockPolicies.find((policy) => policy.siteStyleId === input.siteStyleId && policy.blockKind === input.blockKind) ?? { kind: "open" };
22656
+ }
22657
+ function isLayoutVariantAllowedByPolicy(policy, layoutVariantKey) {
22658
+ switch (policy.kind) {
22659
+ case "open": return true;
22660
+ case "locked-look-and-layouts": return policy.allowedLayoutVariantKeys.includes(layoutVariantKey);
22661
+ default: return assertNever$1(policy, "Unhandled site style block policy");
21969
22662
  }
21970
- switch (input.authority.kind) {
21971
- case "theme-v2": return {
21972
- kind: "interpretable-override",
21973
- fields,
21974
- authority: input.authority
21975
- };
21976
- case "advanced-detached": return {
21977
- kind: "advanced-detached",
21978
- fields,
21979
- authority: input.authority
21980
- };
21981
- case "legacy-raw": return {
21982
- kind: "unsupported-manual-review",
21983
- fields,
21984
- authority: input.authority
21985
- };
21986
- default: return assertNever$1(input.authority);
22663
+ }
22664
+ function normalizeLayoutVariantForPolicy(policy, layoutVariantKey) {
22665
+ switch (policy.kind) {
22666
+ case "open": return layoutVariantKey ?? null;
22667
+ case "locked-look-and-layouts": return layoutVariantKey && isLayoutVariantAllowedByPolicy(policy, layoutVariantKey) ? layoutVariantKey : policy.defaultLayoutVariantKey;
22668
+ default: return assertNever$1(policy, "Unhandled site style block policy");
21987
22669
  }
21988
22670
  }
21989
- function canWriteThemeV2RuntimeFields(authority) {
21990
- return authority?.kind === "theme-v2";
22671
+ function filterLayoutProfilesForPolicy(policy, layoutProfiles) {
22672
+ switch (policy.kind) {
22673
+ case "open": return layoutProfiles;
22674
+ case "locked-look-and-layouts": return layoutProfiles.filter((profile) => isLayoutVariantAllowedByPolicy(policy, profile.variantKey));
22675
+ default: return assertNever$1(policy, "Unhandled site style block policy");
22676
+ }
21991
22677
  }
21992
- function blockReasonForAuthority(authority) {
21993
- if (!authority) {
21994
- return "missing-theme-v2-authority";
22678
+ function isLookAllowedByPolicy(policy, lookId) {
22679
+ switch (policy.kind) {
22680
+ case "open": return true;
22681
+ case "locked-look-and-layouts": return lookId === policy.lookId;
22682
+ default: return assertNever$1(policy, "Unhandled site style block policy");
21995
22683
  }
21996
- switch (authority?.kind) {
21997
- case "advanced-detached": return "advanced-detached";
21998
- case "legacy-raw": return "legacy-raw";
21999
- default: return assertNever$1(authority);
22684
+ }
22685
+ function filterLookIdsForPolicy(policy, looks) {
22686
+ switch (policy.kind) {
22687
+ case "open": return looks;
22688
+ case "locked-look-and-layouts": return looks.filter((look) => look.id === policy.lookId);
22689
+ default: return assertNever$1(policy, "Unhandled site style block policy");
22000
22690
  }
22001
22691
  }
22002
- /**
22003
- * Replace every compiler-owned field on a block's content with a fresh compiled
22004
- * set — the one primitive both writers of compiled output share.
22005
- *
22006
- * Replacement rather than merge is the whole point: a field the new
22007
- * compilation does not produce must *go*, or a recompile onto a Look that asks
22008
- * for no card leaves the old `_cardStyles` sitting there claiming a surface
22009
- * nobody asked for. Every writer that discovers compiler ownership for itself
22010
- * eventually forgets one of these names, so nobody does.
22011
- *
22012
- * Pure, and deliberately free of authority checks — those belong to the
22013
- * persistence wrapper, not to the shape of the content.
22014
- */
22015
- function withCompiledRuntimeStyleFields(input) {
22016
- const authored = Object.fromEntries(Object.entries(input.content).filter(([fieldName]) => !themeV2CompilerOwnedFieldNames.includes(fieldName)));
22692
+ function requiredLookChoice(lookId) {
22017
22693
  return {
22018
- ...authored,
22019
- ...input.fields
22694
+ kind: "user-pinned",
22695
+ value: lookId
22020
22696
  };
22021
22697
  }
22022
22698
  /**
22023
- * Persisted Theme V2 output is compile-at-write: once a block has Theme V2
22024
- * authority, compiler-owned runtime fields are replaced in the same content
22025
- * version. Preview-only compile-at-read callers should use compileBlockRuntimeFields
22026
- * and avoid persisting these fields.
22699
+ * Writes the look pins a locked-look site style requires.
22700
+ *
22701
+ * Boundary pins are never written: a site style's default break after a block
22702
+ * is resolved at read time by `resolveJoinTreatment`, so it shows as a default
22703
+ * and follows later changes to the style (issue 1422). Pins already stored,
22704
+ * including ones an earlier version wrote on the style's behalf, are left as
22705
+ * they are.
22027
22706
  */
22028
- function replaceCompiledRuntimeStyleFields(input) {
22029
- if (!canWriteThemeV2RuntimeFields(input.authority)) {
22030
- return {
22031
- kind: "blocked",
22032
- reason: blockReasonForAuthority(input.authority),
22033
- content: input.content
22034
- };
22707
+ function applySiteStyleBlockPoliciesToPageDesignIntent(intent, blocks) {
22708
+ const policyLookPins = blocks.flatMap((block) => {
22709
+ const policy = deriveSiteStyleBlockPolicy({
22710
+ siteStyleId: intent.siteStyleId,
22711
+ blockKind: block.blockKind
22712
+ });
22713
+ switch (policy.kind) {
22714
+ case "open": return [];
22715
+ case "locked-look-and-layouts": return [{
22716
+ blockId: block.blockId,
22717
+ blockKind: block.blockKind,
22718
+ choice: requiredLookChoice(policy.lookId)
22719
+ }];
22720
+ default: return assertNever$1(policy, "Unhandled site style block policy");
22721
+ }
22722
+ });
22723
+ if (policyLookPins.length === 0) {
22724
+ return intent;
22035
22725
  }
22726
+ const policyPinBlockIds = new Set(policyLookPins.map((pin) => pin.blockId));
22036
22727
  return {
22037
- kind: "applied",
22038
- content: withCompiledRuntimeStyleFields(input)
22728
+ ...intent,
22729
+ blockLookPins: [...intent.blockLookPins.filter((pin) => !policyPinBlockIds.has(pin.blockId)), ...policyLookPins]
22039
22730
  };
22040
22731
  }
22041
- function uniqueBlockIds(values) {
22042
- return [...new Set(values)];
22732
+ function planBlockContentVariantPolicyUpdate(input) {
22733
+ switch (input.policy.kind) {
22734
+ case "open": return { kind: "unchanged" };
22735
+ case "locked-look-and-layouts": {
22736
+ const currentVariant = typeof input.content.variant === "string" ? asLayoutVariantKey(input.content.variant) : null;
22737
+ return currentVariant && isLayoutVariantAllowedByPolicy(input.policy, currentVariant) ? { kind: "unchanged" } : {
22738
+ kind: "set-variant",
22739
+ variantKey: input.policy.defaultLayoutVariantKey
22740
+ };
22741
+ }
22742
+ default: return assertNever$1(input.policy, "Unhandled site style block policy");
22743
+ }
22043
22744
  }
22044
- function collectPageStyleAuthorityBuckets(styleAuthorities) {
22045
- const themeV2BlockIds = [];
22046
- const legacyRawBlockIds = [];
22047
- const advancedDetachedBlockIds = [];
22048
- for (const entry of styleAuthorities) {
22049
- switch (entry.authority.kind) {
22050
- case "theme-v2":
22051
- themeV2BlockIds.push(entry.blockId);
22052
- break;
22053
- case "legacy-raw":
22054
- legacyRawBlockIds.push(entry.blockId);
22055
- break;
22056
- case "advanced-detached":
22057
- advancedDetachedBlockIds.push(entry.blockId);
22058
- break;
22059
- default: assertNever$1(entry.authority);
22745
+ function applyBlockContentVariantPolicy(input) {
22746
+ const plan = planBlockContentVariantPolicyUpdate(input);
22747
+ switch (plan.kind) {
22748
+ case "unchanged": return input.content;
22749
+ case "set-variant": return {
22750
+ ...input.content,
22751
+ variant: plan.variantKey
22752
+ };
22753
+ default: return assertNever$1(plan, "Unhandled block content variant policy plan");
22754
+ }
22755
+ }
22756
+ function auditPageDesignIntentPolicy(input) {
22757
+ return input.blocks.flatMap((block) => {
22758
+ const policy = deriveSiteStyleBlockPolicy({
22759
+ siteStyleId: input.intent.siteStyleId,
22760
+ blockKind: block.blockKind
22761
+ });
22762
+ switch (policy.kind) {
22763
+ case "open": return [];
22764
+ case "locked-look-and-layouts": {
22765
+ const choice = resolvePageDesignBlockLookChoice(input.intent, block.blockId);
22766
+ const lookIssue = choice.kind === "auto" ? {
22767
+ kind: "missing-required-look-pin",
22768
+ blockId: block.blockId,
22769
+ blockKind: block.blockKind,
22770
+ requiredLookId: policy.lookId
22771
+ } : choice.value === policy.lookId ? null : {
22772
+ kind: "unsupported-look-pin",
22773
+ blockId: block.blockId,
22774
+ blockKind: block.blockKind,
22775
+ actualLookId: choice.value,
22776
+ requiredLookId: policy.lookId
22777
+ };
22778
+ const layoutIssue = block.currentLayoutVariantKey && !isLayoutVariantAllowedByPolicy(policy, block.currentLayoutVariantKey) ? {
22779
+ kind: "unsupported-layout-variant",
22780
+ blockId: block.blockId,
22781
+ blockKind: block.blockKind,
22782
+ actualLayoutVariantKey: block.currentLayoutVariantKey,
22783
+ defaultLayoutVariantKey: policy.defaultLayoutVariantKey
22784
+ } : null;
22785
+ const issues = [];
22786
+ if (lookIssue) {
22787
+ issues.push(lookIssue);
22788
+ }
22789
+ if (layoutIssue) {
22790
+ issues.push(layoutIssue);
22791
+ }
22792
+ return issues;
22793
+ }
22794
+ default: return assertNever$1(policy, "Unhandled site style block policy");
22795
+ }
22796
+ });
22797
+ }
22798
+ function auditPageDesignAuthorityPolicy(input) {
22799
+ if (input.intent) {
22800
+ return auditPageDesignIntentPolicy({
22801
+ intent: input.intent,
22802
+ blocks: input.blocks
22803
+ });
22804
+ }
22805
+ const policyBlockCount = input.blocks.filter((block) => deriveSiteStyleBlockPolicy({
22806
+ siteStyleId: input.siteStyleId,
22807
+ blockKind: block.blockKind
22808
+ }).kind !== "open").length;
22809
+ return policyBlockCount === 0 ? [] : [{
22810
+ kind: "missing-page-design-authority",
22811
+ siteStyleId: input.siteStyleId,
22812
+ policyBlockCount
22813
+ }];
22814
+ }
22815
+ function assertRegisteredSiteStyleBlockPoliciesAreValid() {
22816
+ registeredSiteStyleBlockPolicies.forEach((policy) => {
22817
+ if (!policy.allowedLayoutVariantKeys.includes(policy.defaultLayoutVariantKey)) {
22818
+ throw new Error(`Site style block policy for ${policy.siteStyleId} ${policy.blockKind} has a default layout outside its allowed layouts.`);
22819
+ }
22820
+ const preset = appearancePresets.find((candidate$1) => candidate$1.id === policy.lookId && blockAppearancePresetSupportsBlockKind(candidate$1, policy.blockKind));
22821
+ if (!preset) {
22822
+ throw new Error(`Site style block policy for ${policy.siteStyleId} ${policy.blockKind} references an unavailable Look ${policy.lookId}.`);
22060
22823
  }
22824
+ });
22825
+ }
22826
+ function autoChoiceAllowedByPolicy(policy) {
22827
+ switch (policy.kind) {
22828
+ case "open": return autoDesignChoice();
22829
+ case "locked-look-and-layouts": return requiredLookChoice(policy.lookId);
22830
+ default: return assertNever$1(policy, "Unhandled site style block policy");
22061
22831
  }
22832
+ }
22833
+
22834
+ //#endregion
22835
+ //#region ../theme-core/src/site-styles/joinTreatment.ts
22836
+ /** Narrows any join (a resolved intent included) to its public, intent-only form. */
22837
+ function toPageRuntimeJoin(join) {
22062
22838
  return {
22063
- themeV2BlockIds: uniqueBlockIds(themeV2BlockIds),
22064
- legacyRawBlockIds: uniqueBlockIds(legacyRawBlockIds),
22065
- advancedDetachedBlockIds: uniqueBlockIds(advancedDetachedBlockIds)
22839
+ previousBlockId: join.previousBlockId,
22840
+ nextBlockId: join.nextBlockId,
22841
+ treatment: join.treatment
22066
22842
  };
22067
22843
  }
22068
- function summarizePageStyleAuthorities(state) {
22069
- const { themeV2BlockIds, legacyRawBlockIds, advancedDetachedBlockIds } = collectPageStyleAuthorityBuckets(state.styleAuthorities);
22070
- if (themeV2BlockIds.length === 0 && legacyRawBlockIds.length === 0 && advancedDetachedBlockIds.length === 0) {
22844
+ /**
22845
+ * The single rule set for which treatment a join gets. Candidates arrive in
22846
+ * priority order; the first supported one wins.
22847
+ *
22848
+ * - `none` ends the search and yields no join.
22849
+ * - An unsupported candidate (a legacy option id, or a structured treatment the
22850
+ * catalogue lacks) falls through to the next one. When it was the user's
22851
+ * pin, the pin is reported unsupported and the existing
22852
+ * `pinned-boundary-option-unavailable` issue is raised.
22853
+ */
22854
+ function resolveJoinTreatment(input) {
22855
+ const boundaryId = makeBoundaryId(input.previousBlockId, input.nextBlockId);
22856
+ const selection = selectCandidate(input.candidates);
22857
+ const selectedOutcome = selection.kind === "selected" ? selection.outcome : { kind: "none" };
22858
+ const pin = pinStatus(input.candidates, selectedOutcome);
22859
+ const issues = pin.kind === "unsupported" ? [{
22860
+ kind: "pinned-boundary-option-unavailable",
22861
+ boundaryId,
22862
+ presetId: pin.optionId,
22863
+ feedback: "user-facing"
22864
+ }] : [];
22865
+ if (selection.kind === "exhausted") {
22071
22866
  return {
22072
- kind: "no-runtime-authority",
22073
- themeV2BlockIds: [],
22074
- legacyRawBlockIds: [],
22075
- advancedDetachedBlockIds: []
22867
+ join: null,
22868
+ selectedSource: null,
22869
+ pin,
22870
+ issues
22076
22871
  };
22077
22872
  }
22078
- if (legacyRawBlockIds.length > 0) {
22873
+ if (selection.outcome.kind === "none") {
22079
22874
  return {
22080
- kind: "manual-review-required",
22081
- themeV2BlockIds,
22082
- legacyRawBlockIds,
22083
- advancedDetachedBlockIds
22875
+ join: null,
22876
+ selectedSource: selection.candidate.source,
22877
+ pin,
22878
+ issues
22084
22879
  };
22085
22880
  }
22086
- if (themeV2BlockIds.length === 0) {
22087
- return {
22088
- kind: "advanced-detached-only",
22089
- themeV2BlockIds: [],
22090
- legacyRawBlockIds: [],
22091
- advancedDetachedBlockIds
22092
- };
22881
+ return {
22882
+ selectedSource: selection.candidate.source,
22883
+ join: {
22884
+ boundaryId,
22885
+ previousBlockId: input.previousBlockId,
22886
+ nextBlockId: input.nextBlockId,
22887
+ treatment: selection.outcome.treatment,
22888
+ source: selection.candidate.source
22889
+ },
22890
+ pin,
22891
+ issues
22892
+ };
22893
+ }
22894
+ function pinStatus(candidates, selectedOutcome) {
22895
+ const pinned = candidates.find((candidate$1) => candidate$1.source.kind === "user-pinned");
22896
+ if (!pinned) return { kind: "none" };
22897
+ const outcome = candidateOutcome(pinned.treatment);
22898
+ return outcome.kind === "unsupported" ? {
22899
+ kind: "unsupported",
22900
+ optionId: outcome.boundaryOptionId ?? fallbackOptionId(selectedOutcome)
22901
+ } : { kind: "supported" };
22902
+ }
22903
+ function selectCandidate(candidates) {
22904
+ for (const candidate$1 of candidates) {
22905
+ const outcome = candidateOutcome(candidate$1.treatment);
22906
+ switch (outcome.kind) {
22907
+ case "decorative":
22908
+ case "none": return {
22909
+ kind: "selected",
22910
+ candidate: candidate$1,
22911
+ outcome
22912
+ };
22913
+ case "unsupported": continue;
22914
+ default: return assertNever$1(outcome, "Unhandled join candidate outcome");
22915
+ }
22093
22916
  }
22094
- if (advancedDetachedBlockIds.length > 0) {
22095
- return {
22096
- kind: "mixed-authority",
22097
- themeV2BlockIds,
22098
- legacyRawBlockIds: [],
22099
- advancedDetachedBlockIds
22100
- };
22917
+ return { kind: "exhausted" };
22918
+ }
22919
+ function candidateOutcome(treatment) {
22920
+ if (treatment.kind === "none") return { kind: "none" };
22921
+ const intent = transitionIntentForTreatment(treatment);
22922
+ return intent ? {
22923
+ kind: "decorative",
22924
+ treatment: intent
22925
+ } : {
22926
+ kind: "unsupported",
22927
+ boundaryOptionId: boundaryOptionIdForTreatment(treatment)
22928
+ };
22929
+ }
22930
+ /**
22931
+ * Matches the editor's existing issue payload: the pin's own option id when
22932
+ * it has one, otherwise the option the join fell back to.
22933
+ */
22934
+ function fallbackOptionId(outcome) {
22935
+ switch (outcome.kind) {
22936
+ case "decorative": return boundaryOptionForTransitionIntent(outcome.treatment).id;
22937
+ case "none": return noneBoundaryOption.id;
22938
+ default: return assertNever$1(outcome, "Unhandled join fallback outcome");
22101
22939
  }
22940
+ }
22941
+ /**
22942
+ * The page-design defaults for a join, in priority order: the site style's
22943
+ * default break after the previous block, then the page-flow default, which
22944
+ * is always last.
22945
+ */
22946
+ function pageDesignDefaultJoinCandidates(input) {
22947
+ const policy = deriveSiteStyleBlockPolicy({
22948
+ siteStyleId: input.siteStyleId,
22949
+ blockKind: input.previousBlockKind
22950
+ });
22951
+ const siteStyleDefault = policy.kind === "locked-look-and-layouts" && policy.defaultBoundaryAfter ? [{
22952
+ source: {
22953
+ kind: "site-style-default",
22954
+ siteStyleId: input.siteStyleId
22955
+ },
22956
+ treatment: boundaryTreatmentForBoundaryOptionId(policy.defaultBoundaryAfter.optionId)
22957
+ }] : [];
22958
+ return [...siteStyleDefault, pageFlowDefaultJoinCandidate(input.pageFlowPresetId)];
22959
+ }
22960
+ /** The last-resort page-design default, also the source of a fully unsupported join. */
22961
+ function pageFlowDefaultJoinCandidate(pageFlowPresetId) {
22962
+ const pageFlow = pageFlowDefaultBoundary(pageFlowPresetId);
22102
22963
  return {
22103
- kind: "theme-v2-only",
22104
- themeV2BlockIds,
22105
- legacyRawBlockIds: [],
22106
- advancedDetachedBlockIds: []
22964
+ source: {
22965
+ kind: "page-flow-default",
22966
+ pageFlowPresetId: pageFlow.preset?.id ?? null
22967
+ },
22968
+ treatment: boundaryTreatmentForBoundaryOptionId(pageFlow.boundaryOptionId)
22107
22969
  };
22108
22970
  }
22971
+ /**
22972
+ * Page-design source adapter. Priority: the user's pin, then the defaults.
22973
+ * The pin is offered even when unsupported so the rule function can report it.
22974
+ */
22975
+ function joinCandidatesFromPageDesign(intent, siteStyleId, previous, next) {
22976
+ const choice = resolvePageDesignBoundaryChoice(intent, previous.blockId, next.blockId);
22977
+ return [...choice.kind === "user-pinned" ? [{
22978
+ source: { kind: "user-pinned" },
22979
+ treatment: choice.value
22980
+ }] : [], ...pageDesignDefaultJoinCandidates({
22981
+ siteStyleId,
22982
+ pageFlowPresetId: intent.pageFlowPresetId,
22983
+ previousBlockKind: previous.blockKind
22984
+ })];
22985
+ }
22986
+ /**
22987
+ * Generated-design source adapter. Mirrors today's materialisation: the
22988
+ * boundary intent's explicit choice, then the block design's explicit choice,
22989
+ * then the automatic default of no break.
22990
+ */
22991
+ function joinCandidatesFromGeneratedIntent(block) {
22992
+ return [
22993
+ ...explicitGeneratedCandidate(block.boundaryIntent.transitionAfter, "boundary-intent"),
22994
+ ...explicitGeneratedCandidate(block.design.transitionAfter, "block-design"),
22995
+ {
22996
+ source: {
22997
+ kind: "generated",
22998
+ origin: "automatic"
22999
+ },
23000
+ treatment: { kind: "none" }
23001
+ }
23002
+ ];
23003
+ }
23004
+ function explicitGeneratedCandidate(choice, origin) {
23005
+ return choice.kind === "explicit" ? [{
23006
+ source: {
23007
+ kind: "generated",
23008
+ origin
23009
+ },
23010
+ treatment: treatmentForTransitionIntent(choice.value)
23011
+ }] : [];
23012
+ }
23013
+ function treatmentForTransitionIntent(transition) {
23014
+ return transition === "none" ? { kind: "none" } : transitionCatalogueEntryForIntent(transition).treatment;
23015
+ }
22109
23016
 
22110
23017
  //#endregion
22111
23018
  //#region ../theme-core/src/site-styles/imageSurfaceBackground.ts
@@ -22219,13 +23126,15 @@ const compiledRuntimeStyleFieldNames = [
22219
23126
  "_contentCardStyles",
22220
23127
  "_heroLayoutStyles",
22221
23128
  "_cardStyles",
22222
- "_transitionStyles",
22223
23129
  "blockLook",
22224
- "blockComposition",
22225
- "styleGroup"
23130
+ "blockComposition"
22226
23131
  ];
23132
+ /**
23133
+ * Removes every compiler-owned field, plus a stale transition `styleGroup`.
23134
+ * An authored `styleGroup` is content and survives.
23135
+ */
22227
23136
  function stripCompiledRuntimeStyleFields(content) {
22228
- return Object.fromEntries(Object.entries(content).filter(([fieldName]) => !compiledRuntimeStyleFieldNames.includes(fieldName)));
23137
+ return Object.fromEntries(Object.entries(stripStaleTransitionStyleGroup(content)).filter(([fieldName]) => !compiledRuntimeStyleFieldNames.includes(fieldName)));
22229
23138
  }
22230
23139
  function surfaceResolutionContextFromPalette(palette) {
22231
23140
  return {
@@ -22244,7 +23153,7 @@ function defineComponentRuntimeRules(rules) {
22244
23153
  }
22245
23154
  /**
22246
23155
  * Default design rules translate semantic page-level choices into the current
22247
- * runtime's section background and transition style-group vocabulary.
23156
+ * runtime's section background vocabulary.
22248
23157
  */
22249
23158
  const defaultDesignRuntimeRules = defineDesignRuntimeRules({
22250
23159
  sectionSurfaces: {
@@ -22279,8 +23188,7 @@ const defaultDesignRuntimeRules = defineDesignRuntimeRules({
22279
23188
  color: "surface",
22280
23189
  opacity: .88
22281
23190
  }
22282
- },
22283
- transitionClassName: (transition, fromSurface, toSurface) => transition === "none" ? null : `site-style-transition-${transition}-from-${fromSurface}-to-${toSurface}`
23191
+ }
22284
23192
  });
22285
23193
  /**
22286
23194
  * Default component rules translate frame and item-surface semantics into the
@@ -22506,26 +23414,6 @@ function compileCardStyles(input) {
22506
23414
  if (!role) return undefined;
22507
23415
  return cardStylesOverrideSchema.parse({ role });
22508
23416
  }
22509
- function sectionAdjacencyForBlock(input) {
22510
- if (input.sectionAdjacency) {
22511
- return input.sectionAdjacency;
22512
- }
22513
- if (input.nextSectionSurface) {
22514
- return {
22515
- kind: "next-section",
22516
- surface: input.nextSectionSurface
22517
- };
22518
- }
22519
- return { kind: "page-end" };
22520
- }
22521
- function transitionTargetSurfaceForAdjacency(adjacency) {
22522
- switch (adjacency.kind) {
22523
- case "next-section":
22524
- case "terminal-surface": return adjacency.surface;
22525
- case "page-end": return null;
22526
- default: return assertNever$1(adjacency);
22527
- }
22528
- }
22529
23417
  function resolveEyebrowTreatmentForRuntime(block, rules) {
22530
23418
  return block.eyebrowTreatment ?? rules.defaultEyebrowTreatment ?? block.appliedEyebrowTreatment;
22531
23419
  }
@@ -22543,19 +23431,6 @@ function compileBlockRuntimeFields(input) {
22543
23431
  });
22544
23432
  const shouldSuppressContainerFrame = shouldMoveFrameToHeroContentCard(input.block);
22545
23433
  const cardStyles = compileCardStyles({ cardRole: input.block.appliedCardRole });
22546
- const transitionTargetSurface = transitionTargetSurfaceForAdjacency(sectionAdjacencyForBlock(input));
22547
- const transitionClassName = transitionTargetSurface ? designRules.transitionClassName(input.block.appliedTransitionAfter, visualSectionSurface, transitionTargetSurface) : null;
22548
- const transitionRuntime = transitionTargetSurface && transitionClassName ? compileTransitionRuntime({
22549
- treatment: input.block.appliedTransitionAfter,
22550
- from: {
22551
- kind: "surface",
22552
- surface: visualSectionSurface
22553
- },
22554
- to: {
22555
- kind: "surface",
22556
- surface: transitionTargetSurface
22557
- }
22558
- }) : null;
22559
23434
  return {
22560
23435
  blockId: input.block.blockId,
22561
23436
  visualSectionSurface,
@@ -22580,72 +23455,10 @@ function compileBlockRuntimeFields(input) {
22580
23455
  ...cardStyles ? { _cardStyles: cardStyles } : {},
22581
23456
  ...heroRuntimeFields,
22582
23457
  ...input.block.appearancePresetId ? { blockLook: input.block.appearancePresetId } : {},
22583
- ...input.block.appliedComposition ? { blockComposition: input.block.appliedComposition } : {},
22584
- ...transitionRuntime ? { _transitionStyles: transitionRuntime } : {},
22585
- ...transitionClassName ? { styleGroup: transitionClassName } : {}
22586
- }
22587
- };
22588
- }
22589
- function compileTransitionRuntime(input) {
22590
- return {
22591
- treatment: input.treatment,
22592
- from: input.from,
22593
- to: input.to,
22594
- cssVars: {
22595
- from: input.from.kind === "surface" && input.from.surface === "hero-image" ? "transparent" : transitionEndpointCssValue(input.from),
22596
- to: transitionEndpointCssValue(input.to)
22597
- }
22598
- };
22599
- }
22600
- function compileBoundaryRuntime(input) {
22601
- const rules = input.rules ?? defaultDesignRuntimeRules;
22602
- const styleGroup = rules.transitionClassName(input.treatment, input.fromSurface, input.toSurface);
22603
- if (!styleGroup) return null;
22604
- const metrics = boundaryRuntimeMetricsForTransition(input.treatment);
22605
- if (!metrics) return null;
22606
- const transition = compileTransitionRuntime({
22607
- treatment: input.treatment,
22608
- from: {
22609
- kind: "surface",
22610
- surface: input.fromSurface
22611
- },
22612
- to: {
22613
- kind: "surface",
22614
- surface: input.toSurface
23458
+ ...input.block.appliedComposition ? { blockComposition: input.block.appliedComposition } : {}
22615
23459
  }
22616
- });
22617
- return {
22618
- boundaryId: input.boundaryId,
22619
- previousBlockId: input.previousBlockId,
22620
- nextBlockId: input.nextBlockId,
22621
- treatment: transition.treatment,
22622
- metrics,
22623
- from: transition.from,
22624
- to: transition.to,
22625
- cssVars: transition.cssVars,
22626
- styleGroup
22627
23460
  };
22628
23461
  }
22629
- function transitionEndpointCssValue(endpoint) {
22630
- switch (endpoint.kind) {
22631
- case "color-token": return themeColorTokenCssValue(endpoint.token);
22632
- case "surface": return themeColorTokenCssValue(tokenForTransitionSurface(endpoint.surface));
22633
- default: return assertNever$1(endpoint);
22634
- }
22635
- }
22636
- function tokenForTransitionSurface(surface$1) {
22637
- switch (surface$1) {
22638
- case "base":
22639
- case "hero-image": return "background";
22640
- case "muted-band": return "muted";
22641
- case "accent-band": return "accent";
22642
- case "brand-depth-band": return "primary-800";
22643
- default: return assertNever$1(surface$1);
22644
- }
22645
- }
22646
- function themeColorTokenCssValue(token) {
22647
- return `rgb(var(--tb-${token}))`;
22648
- }
22649
23462
  /**
22650
23463
  * Compiles block runtime fields and writes them into content only when Theme V2
22651
23464
  * owns that block's runtime-style fields.
@@ -22658,58 +23471,30 @@ function compileBlockRuntimeFieldsIntoContent(input) {
22658
23471
  authority: input.authority
22659
23472
  });
22660
23473
  }
22661
- /** Compiles a resolved page plan into legacy runtime fields for each resolved block. */
23474
+ /**
23475
+ * Compiles a resolved page plan into legacy runtime fields for each resolved
23476
+ * block, plus the public runtime joins between them.
23477
+ *
23478
+ * Joins come from `resolveJoinTreatment`; plans no longer carry a treatment.
23479
+ * Only joins whose two blocks were compiled are kept, and they leave as intent
23480
+ * only: the blocks renderer composes each one from final content after
23481
+ * omission, including whether it draws at all (issue 1422).
23482
+ */
22662
23483
  function compilePageRuntimeFields(input) {
22663
- const visualSurfaces = input.resolved.blocks.map(resolveVisualSectionSurfaceForBlock);
22664
- const terminalSectionAdjacency = input.terminalSectionAdjacency ?? { kind: "page-end" };
22665
- const designRules = input.designRules ?? defaultDesignRuntimeRules;
22666
- const blocks = input.resolved.blocks.map((block, index) => {
22667
- const nextSurface = visualSurfaces[index + 1];
22668
- const compiled = compileBlockRuntimeFields({
22669
- block,
22670
- surfaceResolutionContext: input.surfaceResolutionContext,
22671
- designRules: input.designRules,
22672
- componentRules: input.componentRules,
22673
- sectionSpacing: input.sectionSpacing,
22674
- sectionAdjacency: nextSurface ? {
22675
- kind: "next-section",
22676
- surface: nextSurface
22677
- } : terminalSectionAdjacency
22678
- });
22679
- return {
22680
- ...compiled,
22681
- fields: stripPageLevelTransitionFields(compiled.fields)
22682
- };
22683
- });
22684
- const boundaries = input.resolved.blocks.flatMap((block, index) => {
22685
- const nextBlock = input.resolved.blocks[index + 1];
22686
- const fromSurface = visualSurfaces[index];
22687
- const toSurface = visualSurfaces[index + 1];
22688
- if (!nextBlock || !fromSurface || !toSurface) {
22689
- return [];
22690
- }
22691
- const boundary = compileBoundaryRuntime({
22692
- boundaryId: `boundary:${block.blockId}:${nextBlock.blockId}`,
22693
- previousBlockId: block.blockId,
22694
- nextBlockId: nextBlock.blockId,
22695
- treatment: block.appliedTransitionAfter,
22696
- fromSurface,
22697
- toSurface,
22698
- rules: designRules
22699
- });
22700
- return boundary ? [boundary] : [];
22701
- });
23484
+ const blocks = input.resolved.blocks.map((block) => compileBlockRuntimeFields({
23485
+ block,
23486
+ surfaceResolutionContext: input.surfaceResolutionContext,
23487
+ designRules: input.designRules,
23488
+ componentRules: input.componentRules,
23489
+ sectionSpacing: input.sectionSpacing
23490
+ }));
23491
+ const compiledIds = new Set(blocks.map((block) => block.blockId));
23492
+ const joins = input.joins.filter((join) => compiledIds.has(join.previousBlockId) && compiledIds.has(join.nextBlockId)).map(toPageRuntimeJoin);
22702
23493
  return {
22703
23494
  blocks,
22704
- boundaries
23495
+ joins
22705
23496
  };
22706
23497
  }
22707
- function stripPageLevelTransitionFields(fields) {
22708
- const { _transitionStyles, styleGroup, ...rest } = fields;
22709
- void _transitionStyles;
22710
- void styleGroup;
22711
- return rest;
22712
- }
22713
23498
  const paletteByRecipe = {
22714
23499
  "brand-led": [
22715
23500
  {
@@ -24769,372 +25554,174 @@ const curatedSiteStylesById = new Map(curatedSiteStyles.map((style) => [style.id
24769
25554
  * only to the compiler.
24770
25555
  */
24771
25556
  function headerStyleRecipeForTheme(theme, styleId) {
24772
- const siteStyle = theme?.siteStyleId ? getCuratedSiteStyle(theme.siteStyleId) : null;
24773
- return resolveHeaderStyleRecipe(siteStyle, styleId);
24774
- }
24775
- function getCuratedSiteStyle(id) {
24776
- return curatedSiteStylesById.get(id) ?? null;
24777
- }
24778
- function designRuntimeRulesForSiteStyle(siteStyle) {
24779
- return siteStyle?.designRuntimeRules ?? defaultDesignRuntimeRules;
24780
- }
24781
- function designRuntimeRulesForTheme(theme) {
24782
- const siteStyleId = theme?.siteStyleId;
24783
- if (!siteStyleId) return defaultDesignRuntimeRules;
24784
- return designRuntimeRulesForSiteStyle(curatedSiteStylesById.get(asSiteStyleId(siteStyleId)) ?? null);
24785
- }
24786
- function componentRuntimeRulesForSiteStyle(siteStyle) {
24787
- return siteStyle?.componentRuntimeRules ?? defaultComponentRuntimeRules;
24788
- }
24789
- function componentRuntimeRulesForTheme(theme) {
24790
- const siteStyleId = theme?.siteStyleId;
24791
- if (!siteStyleId) return defaultComponentRuntimeRules;
24792
- return componentRuntimeRulesForSiteStyle(curatedSiteStylesById.get(asSiteStyleId(siteStyleId)) ?? null);
24793
- }
24794
- function defaultBlockVariantForSiteStyle(siteStyle, blockKind) {
24795
- return siteStyle.defaultBlockVariants?.[blockKind] ?? null;
24796
- }
24797
-
24798
- //#endregion
24799
- //#region ../theme-core/src/site-styles/pageDesignEditorCapabilities.ts
24800
- function cardRoleCapabilityOf(capability) {
24801
- return capability?.kind === "designed" ? capability.cardRoles : "unsupported";
24802
- }
24803
- function backgroundSupportForCapability(capability) {
24804
- if (capability === null) {
24805
- return {
24806
- kind: "unsupported",
24807
- reason: "missing-design-capability"
24808
- };
24809
- }
24810
- if (capability.kind === "not-designed") {
24811
- return {
24812
- kind: "unsupported",
24813
- reason: capability.reason
24814
- };
24815
- }
24816
- return capability.sectionSurfaces === "supported" ? {
24817
- kind: "supported",
24818
- richBackgrounds: capability.richBackgrounds ?? "unsupported"
24819
- } : {
24820
- kind: "unsupported",
24821
- reason: "section-surfaces-unsupported"
24822
- };
24823
- }
24824
- /**
24825
- * The capability-derived half of a block's editor read model, in one call so
24826
- * the read-model builder does not grow a projection per axis.
24827
- */
24828
- function capabilitySupportFor(capability) {
24829
- return {
24830
- backgroundSupport: backgroundSupportForCapability(capability),
24831
- cardRoles: cardRoleCapabilityOf(capability)
24832
- };
24833
- }
24834
-
24835
- //#endregion
24836
- //#region ../theme-core/src/site-styles/lookResolution.ts
24837
- function blockAppearancePresetSupportsBlockKind(preset, blockKind) {
24838
- return preset.blockKinds.some((candidate$1) => candidate$1 === blockKind);
24839
- }
24840
- function preferredAppearanceTargetForBlockKind(blockKind) {
24841
- switch (blockKind) {
24842
- case "block.event-calendar":
24843
- case "block.event-listing": return "events";
24844
- default: return null;
24845
- }
24846
- }
24847
- function sortCompatibleBlockAppearancePresets(blockKind, options) {
24848
- const preferredTarget = preferredAppearanceTargetForBlockKind(blockKind);
24849
- if (!preferredTarget) {
24850
- return options;
24851
- }
24852
- return [...options].sort((left, right) => {
24853
- const leftPreset = appearancePresets.find((preset) => preset.id === left.id);
24854
- const rightPreset = appearancePresets.find((preset) => preset.id === right.id);
24855
- const leftPreferred = leftPreset?.target === preferredTarget;
24856
- const rightPreferred = rightPreset?.target === preferredTarget;
24857
- if (leftPreferred === rightPreferred) {
24858
- return 0;
24859
- }
24860
- return leftPreferred ? -1 : 1;
24861
- });
24862
- }
24863
- function blockAppearancePresetSupportsLayoutProfile(preset, profile, input = {}) {
24864
- const constraints = preset.constraints;
24865
- if (!constraints || !constraints.layoutFamilies && !constraints.layoutVariantKeys) {
24866
- return true;
24867
- }
24868
- if (!profile) {
24869
- return input.allowLayoutConstrainedPresetWithoutProfile ?? true;
24870
- }
24871
- return layoutProfileSupportsConstraints(profile, constraints);
24872
- }
24873
- function resolveBlockAppearancePreset(preset, layoutProfile = null) {
24874
- return {
24875
- id: asAppearancePresetId(preset.id),
24876
- label: preset.label,
24877
- design: resolveBlockAppearancePresetDesign(preset, layoutProfile)
24878
- };
24879
- }
24880
- function getCompatibleBlockAppearancePresets(input) {
24881
- const options = appearancePresets.flatMap((preset) => {
24882
- if (!blockAppearancePresetSupportsBlockKind(preset, input.blockKind) || !blockAppearancePresetSupportsLayoutProfile(preset, input.layoutProfile ?? null, { allowLayoutConstrainedPresetWithoutProfile: input.allowLayoutConstrainedPresetWithoutProfile })) {
24883
- return [];
24884
- }
24885
- return [resolveBlockAppearancePreset(preset, input.layoutProfile ?? null)];
24886
- });
24887
- const sortedOptions = sortCompatibleBlockAppearancePresets(input.blockKind, options);
24888
- const recommendedIds = input.siteStyle?.recommendedAppearancePresetIds ?? [];
24889
- if (recommendedIds.length === 0) {
24890
- return sortedOptions;
24891
- }
24892
- const recommended = recommendedIds.flatMap((presetId) => sortedOptions.find((option) => option.id === presetId) ?? []);
24893
- const alreadyIncludedIds = new Set(recommended.map((option) => option.id));
24894
- return [...recommended, ...sortedOptions.filter((option) => !alreadyIncludedIds.has(option.id))];
25557
+ const siteStyle = theme?.siteStyleId ? getCuratedSiteStyle(theme.siteStyleId) : null;
25558
+ return resolveHeaderStyleRecipe(siteStyle, styleId);
24895
25559
  }
24896
-
24897
- //#endregion
24898
- //#region ../theme-core/src/site-styles/siteStyleBlockPolicies.ts
24899
- const defaultPolicyPageFlowPresetId = asPageFlowPresetId("generated-default");
24900
- const southWestTwerkSchoolSiteStyleId = asSiteStyleId("site-style:south-west-twerk-school");
24901
- const heroBrandScrimFixedFrameLookId = asAppearancePresetId("hero-brand-scrim-fixed-frame");
24902
- const classicHeroLayoutVariantKey = asLayoutVariantKey("classic");
24903
- const microHeroLayoutVariantKey = asLayoutVariantKey("micro");
24904
- const waveBoundaryOptionId = asBoundaryOptionId("break:wave");
24905
- const registeredSiteStyleBlockPolicies = [{
24906
- siteStyleId: southWestTwerkSchoolSiteStyleId,
24907
- kind: "locked-look-and-layouts",
24908
- blockKind: "block.hero",
24909
- lookId: heroBrandScrimFixedFrameLookId,
24910
- allowedLayoutVariantKeys: [classicHeroLayoutVariantKey, microHeroLayoutVariantKey],
24911
- defaultLayoutVariantKey: classicHeroLayoutVariantKey,
24912
- defaultBoundaryAfter: {
24913
- optionId: waveBoundaryOptionId,
24914
- mode: "default-only"
24915
- },
24916
- customize: {
24917
- hideDisallowedLooks: true,
24918
- hideDisallowedLayouts: true
24919
- }
24920
- }];
24921
- function siteStyleBlockPolicies() {
24922
- return registeredSiteStyleBlockPolicies;
25560
+ function getCuratedSiteStyle(id) {
25561
+ return curatedSiteStylesById.get(id) ?? null;
24923
25562
  }
24924
- function buildInitialPolicyPageDesignIntent(siteStyleId) {
24925
- return {
24926
- schemaVersion: pageDesignIntentSchemaVersion,
24927
- siteStyleId,
24928
- pageFlowPresetId: defaultPolicyPageFlowPresetId,
24929
- blockLookPins: [],
24930
- boundaryPins: []
24931
- };
25563
+ function designRuntimeRulesForSiteStyle(siteStyle) {
25564
+ return siteStyle?.designRuntimeRules ?? defaultDesignRuntimeRules;
24932
25565
  }
24933
- function hasPolicyForBlocks(input) {
24934
- return input.blocks.some((block) => deriveSiteStyleBlockPolicy({
24935
- siteStyleId: input.siteStyleId,
24936
- blockKind: block.blockKind
24937
- }).kind !== "open");
25566
+ function designRuntimeRulesForTheme(theme) {
25567
+ const siteStyleId = theme?.siteStyleId;
25568
+ if (!siteStyleId) return defaultDesignRuntimeRules;
25569
+ return designRuntimeRulesForSiteStyle(curatedSiteStylesById.get(asSiteStyleId(siteStyleId)) ?? null);
24938
25570
  }
24939
- function deriveSiteStyleBlockPolicy(input) {
24940
- return registeredSiteStyleBlockPolicies.find((policy) => policy.siteStyleId === input.siteStyleId && policy.blockKind === input.blockKind) ?? { kind: "open" };
25571
+ function componentRuntimeRulesForSiteStyle(siteStyle) {
25572
+ return siteStyle?.componentRuntimeRules ?? defaultComponentRuntimeRules;
24941
25573
  }
24942
- function isLayoutVariantAllowedByPolicy(policy, layoutVariantKey) {
24943
- switch (policy.kind) {
24944
- case "open": return true;
24945
- case "locked-look-and-layouts": return policy.allowedLayoutVariantKeys.includes(layoutVariantKey);
24946
- default: return assertNever$1(policy, "Unhandled site style block policy");
24947
- }
25574
+ function componentRuntimeRulesForTheme(theme) {
25575
+ const siteStyleId = theme?.siteStyleId;
25576
+ if (!siteStyleId) return defaultComponentRuntimeRules;
25577
+ return componentRuntimeRulesForSiteStyle(curatedSiteStylesById.get(asSiteStyleId(siteStyleId)) ?? null);
24948
25578
  }
24949
- function normalizeLayoutVariantForPolicy(policy, layoutVariantKey) {
24950
- switch (policy.kind) {
24951
- case "open": return layoutVariantKey ?? null;
24952
- case "locked-look-and-layouts": return layoutVariantKey && isLayoutVariantAllowedByPolicy(policy, layoutVariantKey) ? layoutVariantKey : policy.defaultLayoutVariantKey;
24953
- default: return assertNever$1(policy, "Unhandled site style block policy");
24954
- }
25579
+ function defaultBlockVariantForSiteStyle(siteStyle, blockKind) {
25580
+ return siteStyle.defaultBlockVariants?.[blockKind] ?? null;
24955
25581
  }
24956
- function filterLayoutProfilesForPolicy(policy, layoutProfiles) {
24957
- switch (policy.kind) {
24958
- case "open": return layoutProfiles;
24959
- case "locked-look-and-layouts": return layoutProfiles.filter((profile) => isLayoutVariantAllowedByPolicy(policy, profile.variantKey));
24960
- default: return assertNever$1(policy, "Unhandled site style block policy");
24961
- }
25582
+
25583
+ //#endregion
25584
+ //#region ../theme-core/src/site-styles/pageDesignEditorCapabilities.ts
25585
+ function cardRoleCapabilityOf(capability) {
25586
+ return capability?.kind === "designed" ? capability.cardRoles : "unsupported";
24962
25587
  }
24963
- function isLookAllowedByPolicy(policy, lookId) {
24964
- switch (policy.kind) {
24965
- case "open": return true;
24966
- case "locked-look-and-layouts": return lookId === policy.lookId;
24967
- default: return assertNever$1(policy, "Unhandled site style block policy");
25588
+ function backgroundSupportForCapability(capability) {
25589
+ if (capability === null) {
25590
+ return {
25591
+ kind: "unsupported",
25592
+ reason: "missing-design-capability"
25593
+ };
24968
25594
  }
24969
- }
24970
- function filterLookIdsForPolicy(policy, looks) {
24971
- switch (policy.kind) {
24972
- case "open": return looks;
24973
- case "locked-look-and-layouts": return looks.filter((look) => look.id === policy.lookId);
24974
- default: return assertNever$1(policy, "Unhandled site style block policy");
25595
+ if (capability.kind === "not-designed") {
25596
+ return {
25597
+ kind: "unsupported",
25598
+ reason: capability.reason
25599
+ };
24975
25600
  }
24976
- }
24977
- function requiredLookChoice(lookId) {
24978
- return {
24979
- kind: "user-pinned",
24980
- value: lookId
24981
- };
24982
- }
24983
- function requiredBoundaryChoice(optionId) {
24984
- return {
24985
- kind: "user-pinned",
24986
- value: boundaryTreatmentForBoundaryOptionId(optionId)
25601
+ return capability.sectionSurfaces === "supported" ? {
25602
+ kind: "supported",
25603
+ richBackgrounds: capability.richBackgrounds ?? "unsupported"
25604
+ } : {
25605
+ kind: "unsupported",
25606
+ reason: "section-surfaces-unsupported"
24987
25607
  };
24988
25608
  }
24989
- function applySiteStyleBlockPoliciesToPageDesignIntent(intent, blocks) {
24990
- const policyLookPins = blocks.flatMap((block) => {
24991
- const policy = deriveSiteStyleBlockPolicy({
24992
- siteStyleId: intent.siteStyleId,
24993
- blockKind: block.blockKind
24994
- });
24995
- switch (policy.kind) {
24996
- case "open": return [];
24997
- case "locked-look-and-layouts": return [{
24998
- blockId: block.blockId,
24999
- blockKind: block.blockKind,
25000
- choice: requiredLookChoice(policy.lookId)
25001
- }];
25002
- default: return assertNever$1(policy, "Unhandled site style block policy");
25003
- }
25004
- });
25005
- const policyBoundaryPins = blocks.flatMap((block, index) => {
25006
- const nextBlock = blocks[index + 1];
25007
- if (!nextBlock) {
25008
- return [];
25009
- }
25010
- const policy = deriveSiteStyleBlockPolicy({
25011
- siteStyleId: intent.siteStyleId,
25012
- blockKind: block.blockKind
25013
- });
25014
- switch (policy.kind) {
25015
- case "open": return [];
25016
- case "locked-look-and-layouts": {
25017
- const defaultBoundary = policy.defaultBoundaryAfter;
25018
- if (!defaultBoundary) {
25019
- return [];
25020
- }
25021
- const currentChoice = resolvePageDesignBoundaryChoice(intent, block.blockId, nextBlock.blockId);
25022
- return currentChoice.kind === "auto" ? [buildBoundaryPin(block.blockId, nextBlock.blockId, requiredBoundaryChoice(defaultBoundary.optionId))] : [];
25023
- }
25024
- default: return assertNever$1(policy, "Unhandled site style block policy");
25025
- }
25026
- });
25027
- if (policyLookPins.length === 0 && policyBoundaryPins.length === 0) {
25028
- return intent;
25029
- }
25030
- const policyPinBlockIds = new Set(policyLookPins.map((pin) => pin.blockId));
25609
+ /**
25610
+ * The capability-derived half of a block's editor read model, in one call so
25611
+ * the read-model builder does not grow a projection per axis.
25612
+ */
25613
+ function capabilitySupportFor(capability) {
25031
25614
  return {
25032
- ...intent,
25033
- blockLookPins: [...intent.blockLookPins.filter((pin) => !policyPinBlockIds.has(pin.blockId)), ...policyLookPins],
25034
- boundaryPins: [...intent.boundaryPins, ...policyBoundaryPins]
25615
+ backgroundSupport: backgroundSupportForCapability(capability),
25616
+ cardRoles: cardRoleCapabilityOf(capability)
25035
25617
  };
25036
25618
  }
25037
- function planBlockContentVariantPolicyUpdate(input) {
25038
- switch (input.policy.kind) {
25039
- case "open": return { kind: "unchanged" };
25040
- case "locked-look-and-layouts": {
25041
- const currentVariant = typeof input.content.variant === "string" ? asLayoutVariantKey(input.content.variant) : null;
25042
- return currentVariant && isLayoutVariantAllowedByPolicy(input.policy, currentVariant) ? { kind: "unchanged" } : {
25043
- kind: "set-variant",
25044
- variantKey: input.policy.defaultLayoutVariantKey
25045
- };
25046
- }
25047
- default: return assertNever$1(input.policy, "Unhandled site style block policy");
25048
- }
25049
- }
25050
- function applyBlockContentVariantPolicy(input) {
25051
- const plan = planBlockContentVariantPolicyUpdate(input);
25052
- switch (plan.kind) {
25053
- case "unchanged": return input.content;
25054
- case "set-variant": return {
25055
- ...input.content,
25056
- variant: plan.variantKey
25057
- };
25058
- default: return assertNever$1(plan, "Unhandled block content variant policy plan");
25059
- }
25060
- }
25061
- function auditPageDesignIntentPolicy(input) {
25062
- return input.blocks.flatMap((block) => {
25063
- const policy = deriveSiteStyleBlockPolicy({
25064
- siteStyleId: input.intent.siteStyleId,
25065
- blockKind: block.blockKind
25066
- });
25067
- switch (policy.kind) {
25068
- case "open": return [];
25069
- case "locked-look-and-layouts": {
25070
- const choice = resolvePageDesignBlockLookChoice(input.intent, block.blockId);
25071
- const lookIssue = choice.kind === "auto" ? {
25072
- kind: "missing-required-look-pin",
25073
- blockId: block.blockId,
25074
- blockKind: block.blockKind,
25075
- requiredLookId: policy.lookId
25076
- } : choice.value === policy.lookId ? null : {
25077
- kind: "unsupported-look-pin",
25078
- blockId: block.blockId,
25079
- blockKind: block.blockKind,
25080
- actualLookId: choice.value,
25081
- requiredLookId: policy.lookId
25082
- };
25083
- const layoutIssue = block.currentLayoutVariantKey && !isLayoutVariantAllowedByPolicy(policy, block.currentLayoutVariantKey) ? {
25084
- kind: "unsupported-layout-variant",
25085
- blockId: block.blockId,
25086
- blockKind: block.blockKind,
25087
- actualLayoutVariantKey: block.currentLayoutVariantKey,
25088
- defaultLayoutVariantKey: policy.defaultLayoutVariantKey
25089
- } : null;
25090
- const issues = [];
25091
- if (lookIssue) {
25092
- issues.push(lookIssue);
25093
- }
25094
- if (layoutIssue) {
25095
- issues.push(layoutIssue);
25096
- }
25097
- return issues;
25098
- }
25099
- default: return assertNever$1(policy, "Unhandled site style block policy");
25100
- }
25619
+
25620
+ //#endregion
25621
+ //#region ../theme-core/src/site-styles/pageDesignEditorBoundaries.ts
25622
+ function buildBoundaryReadModels(input) {
25623
+ return input.blocks.flatMap((previousBlock, index) => {
25624
+ const nextBlock = input.blocks[index + 1];
25625
+ return nextBlock ? [buildBoundaryReadModel({
25626
+ siteStyleId: input.siteStyleId,
25627
+ pageFlowPresetId: input.intent.pageFlowPresetId,
25628
+ pin: resolvePageDesignBoundaryChoice(input.intent, previousBlock.blockId, nextBlock.blockId),
25629
+ previousBlock,
25630
+ nextBlock
25631
+ })] : [];
25101
25632
  });
25102
25633
  }
25103
- function auditPageDesignAuthorityPolicy(input) {
25104
- if (input.intent) {
25105
- return auditPageDesignIntentPolicy({
25106
- intent: input.intent,
25107
- blocks: input.blocks
25108
- });
25109
- }
25110
- const policyBlockCount = input.blocks.filter((block) => deriveSiteStyleBlockPolicy({
25111
- siteStyleId: input.siteStyleId,
25112
- blockKind: block.blockKind
25113
- }).kind !== "open").length;
25114
- return policyBlockCount === 0 ? [] : [{
25115
- kind: "missing-page-design-authority",
25634
+ function buildBoundaryReadModel(input) {
25635
+ const previousBlockId = input.previousBlock.blockId;
25636
+ const nextBlockId = input.nextBlock.blockId;
25637
+ const defaults = pageDesignDefaultJoinCandidates({
25116
25638
  siteStyleId: input.siteStyleId,
25117
- policyBlockCount
25118
- }];
25119
- }
25120
- function assertRegisteredSiteStyleBlockPoliciesAreValid() {
25121
- registeredSiteStyleBlockPolicies.forEach((policy) => {
25122
- if (!policy.allowedLayoutVariantKeys.includes(policy.defaultLayoutVariantKey)) {
25123
- throw new Error(`Site style block policy for ${policy.siteStyleId} ${policy.blockKind} has a default layout outside its allowed layouts.`);
25124
- }
25125
- const preset = appearancePresets.find((candidate$1) => candidate$1.id === policy.lookId && blockAppearancePresetSupportsBlockKind(candidate$1, policy.blockKind));
25126
- if (!preset) {
25127
- throw new Error(`Site style block policy for ${policy.siteStyleId} ${policy.blockKind} references an unavailable Look ${policy.lookId}.`);
25128
- }
25639
+ pageFlowPresetId: input.pageFlowPresetId,
25640
+ previousBlockKind: input.previousBlock.blockKind
25129
25641
  });
25642
+ const projection = input.pin.kind === "user-pinned" ? pinnedBreakChoice(resolveJoinTreatment({
25643
+ candidates: [{
25644
+ source: { kind: "user-pinned" },
25645
+ treatment: input.pin.value
25646
+ }, ...defaults],
25647
+ previousBlockId,
25648
+ nextBlockId
25649
+ })) : autoBreakChoice(resolveJoinTreatment({
25650
+ candidates: defaults,
25651
+ previousBlockId,
25652
+ nextBlockId
25653
+ }), input.pageFlowPresetId);
25654
+ return {
25655
+ boundary: {
25656
+ boundaryId: makeBoundaryId(previousBlockId, nextBlockId),
25657
+ previousBlockId,
25658
+ nextBlockId,
25659
+ breakChoice: projection.breakChoice,
25660
+ availableBreaks: boundaryOptions
25661
+ },
25662
+ issues: projection.issues
25663
+ };
25130
25664
  }
25131
- function autoChoiceAllowedByPolicy(policy) {
25132
- switch (policy.kind) {
25133
- case "open": return autoDesignChoice();
25134
- case "locked-look-and-layouts": return requiredLookChoice(policy.lookId);
25135
- default: return assertNever$1(policy, "Unhandled site style block policy");
25665
+ function autoBreakChoice(resolution, pageFlowPresetId) {
25666
+ return {
25667
+ breakChoice: {
25668
+ kind: "auto",
25669
+ source: resolution.selectedSource ?? pageFlowDefaultJoinCandidate(pageFlowPresetId).source,
25670
+ effectiveValue: effectiveBreak(resolution)
25671
+ },
25672
+ issues: resolution.issues
25673
+ };
25674
+ }
25675
+ function pinnedBreakChoice(resolution) {
25676
+ const effectiveValue = effectiveBreak(resolution);
25677
+ switch (resolution.pin.kind) {
25678
+ case "unsupported": return {
25679
+ breakChoice: {
25680
+ kind: "user-pinned",
25681
+ value: resolution.pin.optionId,
25682
+ status: "unsupported",
25683
+ effectiveValue
25684
+ },
25685
+ issues: resolution.issues
25686
+ };
25687
+ case "supported":
25688
+ case "none": return {
25689
+ breakChoice: {
25690
+ kind: "user-pinned",
25691
+ value: effectiveValue ?? noneBoundaryOption.id,
25692
+ status: "supported",
25693
+ effectiveValue
25694
+ },
25695
+ issues: resolution.issues
25696
+ };
25697
+ default: return assertNever$1(resolution.pin, "Unhandled join pin status");
25136
25698
  }
25137
25699
  }
25700
+ /** The effective break as the option the editor shows; null when nothing was usable. */
25701
+ function effectiveBreak(resolution) {
25702
+ if (resolution.join) {
25703
+ return boundaryOptionForTransitionIntent(resolution.join.treatment).id;
25704
+ }
25705
+ return resolution.selectedSource ? noneBoundaryOption.id : null;
25706
+ }
25707
+ /**
25708
+ * Re-resolves one boundary of an editor model for a changed break choice,
25709
+ * through the same rules the model was built with. The preview uses it for
25710
+ * optimistic updates, so a reset lands on whichever default the rules pick
25711
+ * (site style or Page Flow). Null when either block is not in the model.
25712
+ */
25713
+ function resolvePageDesignEditorBoundaryForChoice(input) {
25714
+ const previousBlock = input.editorModel.blocks.find((block) => block.blockId === input.previousBlockId);
25715
+ const nextBlock = input.editorModel.blocks.find((block) => block.blockId === input.nextBlockId);
25716
+ if (!previousBlock || !nextBlock) return null;
25717
+ return buildBoundaryReadModel({
25718
+ siteStyleId: input.editorModel.siteStyleId,
25719
+ pageFlowPresetId: input.editorModel.pageFlow.value,
25720
+ pin: input.choice,
25721
+ previousBlock,
25722
+ nextBlock
25723
+ }).boundary;
25724
+ }
25138
25725
 
25139
25726
  //#endregion
25140
25727
  //#region ../theme-core/src/site-styles/pageDesignEditorModel.ts
@@ -25160,29 +25747,6 @@ const backgroundImagePromotedFieldPath = "_sectionStyles.background.image";
25160
25747
  * Look/Layout semantics support them.
25161
25748
  */
25162
25749
  const normalEditorCuratedPromotedFieldPaths = [backgroundImagePromotedFieldPath];
25163
- /**
25164
- * Must remain non-empty; downstream code indexes `[0]` as the documented default.
25165
- */
25166
- const pageFlowPresets = [
25167
- {
25168
- id: asPageFlowPresetId("flow:quiet"),
25169
- label: "Quiet",
25170
- boundaryStrategy: "quiet",
25171
- defaultBoundaryOptionId: noneBoundaryOption.id
25172
- },
25173
- {
25174
- id: asPageFlowPresetId("flow:balanced"),
25175
- label: "Balanced",
25176
- boundaryStrategy: "balanced",
25177
- defaultBoundaryOptionId: softBoundaryOption.id
25178
- },
25179
- {
25180
- id: asPageFlowPresetId("flow:expressive"),
25181
- label: "Expressive",
25182
- boundaryStrategy: "expressive",
25183
- defaultBoundaryOptionId: spaciousBoundaryOption.id
25184
- }
25185
- ];
25186
25750
  function buildPageDesignEditorReadModel(input) {
25187
25751
  const defaultPageFlow = pageFlowPresets[0];
25188
25752
  const currentPageFlow = pageFlowPresets.find((preset) => preset.id === input.intent.pageFlowPresetId) ?? null;
@@ -25270,8 +25834,8 @@ function buildPageDesignEditorReadModel(input) {
25270
25834
  };
25271
25835
  });
25272
25836
  const boundaryResults = buildBoundaryReadModels({
25273
- intent: input.intent,
25274
- blocks: input.blocks
25837
+ ...input,
25838
+ blocks: blockResults.map((result) => result.block)
25275
25839
  });
25276
25840
  return {
25277
25841
  siteStyleId: input.siteStyleId,
@@ -25515,62 +26079,6 @@ function editorChoiceForLook(input) {
25515
26079
  })
25516
26080
  });
25517
26081
  }
25518
- function buildBoundaryReadModels(input) {
25519
- return input.blocks.flatMap((previous, index) => {
25520
- const next = input.blocks[index + 1];
25521
- if (!next) {
25522
- return [];
25523
- }
25524
- const boundaryId = makeBoundaryId(previous.blockId, next.blockId);
25525
- const choice = resolvePageDesignBoundaryChoice(input.intent, previous.blockId, next.blockId);
25526
- const breakResult = boundaryEditorChoiceFromResolvedChoice({
25527
- choice,
25528
- defaultValue: autoBoundaryOptionForIntent(input.intent),
25529
- unsupportedIssue: (presetId) => ({
25530
- kind: "pinned-boundary-option-unavailable",
25531
- boundaryId,
25532
- presetId,
25533
- feedback: "user-facing"
25534
- })
25535
- });
25536
- return {
25537
- boundary: {
25538
- boundaryId,
25539
- previousBlockId: previous.blockId,
25540
- nextBlockId: next.blockId,
25541
- breakChoice: breakResult.choice,
25542
- availableBreaks: boundaryOptions
25543
- },
25544
- issues: compactIssues([breakResult.issue])
25545
- };
25546
- });
25547
- }
25548
- function boundaryEditorChoiceFromResolvedChoice(input) {
25549
- switch (input.choice.kind) {
25550
- case "auto": return {
25551
- choice: {
25552
- kind: "auto",
25553
- effectiveValue: input.defaultValue
25554
- },
25555
- issue: null
25556
- };
25557
- case "user-pinned": {
25558
- const option = boundaryOptionForTreatment(input.choice.value);
25559
- const fallbackValue = boundaryOptionIdForTreatment(input.choice.value);
25560
- const issueValue = fallbackValue ?? input.defaultValue ?? noneBoundaryOption.id;
25561
- return {
25562
- choice: {
25563
- kind: "user-pinned",
25564
- value: issueValue,
25565
- status: option ? "supported" : "unsupported",
25566
- effectiveValue: option?.id ?? input.defaultValue
25567
- },
25568
- issue: option ? null : input.unsupportedIssue(issueValue)
25569
- };
25570
- }
25571
- default: return assertNever$1(input.choice, "Unhandled boundary design choice");
25572
- }
25573
- }
25574
26082
  function editorChoiceFromResolvedChoice(input) {
25575
26083
  switch (input.choice.kind) {
25576
26084
  case "auto": return {
@@ -25595,10 +26103,6 @@ function editorChoiceFromResolvedChoice(input) {
25595
26103
  default: return assertNever$1(input.choice, "Unhandled page design choice");
25596
26104
  }
25597
26105
  }
25598
- function autoBoundaryOptionForIntent(intent) {
25599
- const pageFlow = pageFlowPresets.find((preset) => preset.id === intent.pageFlowPresetId);
25600
- return pageFlow?.defaultBoundaryOptionId ?? boundaryOptions[0].id;
25601
- }
25602
26106
  function compactIssues(issues) {
25603
26107
  return issues.filter((issue$1) => issue$1 !== null);
25604
26108
  }
@@ -25649,7 +26153,6 @@ const repairActionsByIssueKind = {
25649
26153
  "image-surface-treatment-without-image-surface": ["fallback-image-surface-treatment"],
25650
26154
  "image-surface-motion-without-image-surface": ["fallback-image-surface-motion"],
25651
26155
  "hero-legibility-overridden-by-image-surface-treatment": ["remove-hero-legibility"],
25652
- "removed-transition-for-flush-frame": ["remove-transition"],
25653
26156
  "hero-image-without-legibility": ["fallback-hero-legibility"]
25654
26157
  };
25655
26158
  function repairActionsForIssue(issueKind) {
@@ -25737,11 +26240,6 @@ function repairDesignIssue(issue$1) {
25737
26240
  issue: issue$1,
25738
26241
  action: issue$1.allowedRepairActions[0]
25739
26242
  };
25740
- case "removed-transition-for-flush-frame": return {
25741
- kind: "repaired",
25742
- issue: issue$1,
25743
- action: issue$1.allowedRepairActions[0]
25744
- };
25745
26243
  case "hero-image-without-legibility": return {
25746
26244
  kind: "fallbackApplied",
25747
26245
  issue: issue$1,
@@ -25784,13 +26282,9 @@ function supportsBlockComposition(capability, composition) {
25784
26282
  default: return assertNever$1(composition);
25785
26283
  }
25786
26284
  }
25787
- function isDecorativeTransition(transition) {
25788
- return transition !== "none";
25789
- }
25790
26285
  const blockDesignFallback = {
25791
26286
  sectionSurface: "base",
25792
26287
  contentFrame: plainFrame,
25793
- transitionAfter: "none",
25794
26288
  sectionHeight: "default",
25795
26289
  heroLegibility: "none",
25796
26290
  imageSurfaceTreatment: { kind: "none" },
@@ -25806,7 +26300,6 @@ const appliedBlockDesignFallback = {
25806
26300
  appliedSectionSurface: blockDesignFallback.sectionSurface,
25807
26301
  appliedContentFrame: blockDesignFallback.contentFrame,
25808
26302
  appliedCardRole: null,
25809
- appliedTransitionAfter: blockDesignFallback.transitionAfter,
25810
26303
  appliedSectionHeight: blockDesignFallback.sectionHeight,
25811
26304
  appliedHeroLegibility: blockDesignFallback.heroLegibility,
25812
26305
  appliedImageSurfaceTreatment: blockDesignFallback.imageSurfaceTreatment,
@@ -25832,7 +26325,6 @@ function blockDesignRequest(block) {
25832
26325
  sectionSurface: block.sectionSurface,
25833
26326
  contentFrame: block.contentFrame,
25834
26327
  ...block.cardRole ? { cardRole: block.cardRole } : {},
25835
- transitionAfter: block.transitionAfter,
25836
26328
  sectionHeight: block.sectionHeight,
25837
26329
  heroLegibility: block.heroLegibility,
25838
26330
  imageSurfaceTreatment: block.imageSurfaceTreatment,
@@ -25851,7 +26343,6 @@ function resolveBlockDesignPlan(input) {
25851
26343
  let appliedSectionSurface = block.sectionSurface;
25852
26344
  let appliedContentFrame = block.contentFrame;
25853
26345
  const appliedCardRole = appliedCardRoleFor(block, capability);
25854
- let appliedTransitionAfter = block.transitionAfter;
25855
26346
  let appliedSectionHeight = block.sectionHeight ?? "default";
25856
26347
  let appliedHeroLegibility = block.heroLegibility ?? "none";
25857
26348
  let appliedImageSurfaceTreatment = block.imageSurfaceTreatment ?? { kind: "none" };
@@ -26116,7 +26607,6 @@ function resolveBlockDesignPlan(input) {
26116
26607
  appliedSectionSurface,
26117
26608
  appliedContentFrame,
26118
26609
  appliedCardRole,
26119
- appliedTransitionAfter,
26120
26610
  appliedSectionHeight,
26121
26611
  appliedHeroLegibility,
26122
26612
  appliedImageSurfaceTreatment,
@@ -26128,29 +26618,8 @@ function resolveBlockDesignPlan(input) {
26128
26618
  appliedComposition
26129
26619
  };
26130
26620
  });
26131
- const transitionSafeBlocks = blocks.map((block, index) => {
26132
- const nextBlock = blocks[index + 1];
26133
- if (!nextBlock || !isDecorativeTransition(block.appliedTransitionAfter) || block.appliedContentFrame.kind !== "flush-panel" && nextBlock.appliedContentFrame.kind !== "flush-panel") {
26134
- return block;
26135
- }
26136
- issues.push({
26137
- kind: "removed-transition-for-flush-frame",
26138
- blockId: block.blockId,
26139
- blockKind: block.blockKind,
26140
- purpose: block.purpose,
26141
- requested: block.appliedTransitionAfter,
26142
- fallback: "none",
26143
- policy: input.policy,
26144
- severity: designIssueSeverityForPolicy(input.policy),
26145
- allowedRepairActions: repairActionsForIssue("removed-transition-for-flush-frame")
26146
- });
26147
- return {
26148
- ...block,
26149
- appliedTransitionAfter: "none"
26150
- };
26151
- });
26152
26621
  return {
26153
- blocks: transitionSafeBlocks,
26622
+ blocks,
26154
26623
  issues
26155
26624
  };
26156
26625
  }
@@ -27037,80 +27506,6 @@ function recompileSiteChromeHeaderFromPersistedIntent(theme, siteStyle, requeste
27037
27506
  };
27038
27507
  }
27039
27508
 
27040
- //#endregion
27041
- //#region ../theme-core/src/site-styles/runtimeBackgroundResolution.ts
27042
- function parseCanonicalRuntimeBackground(content) {
27043
- const sectionStyles = readRecord$1(content._sectionStyles);
27044
- const background = readRecord$1(sectionStyles?.background);
27045
- if (!background) return { kind: "none" };
27046
- const parsed = boxBackgroundSchema.safeParse(background);
27047
- if (!parsed.success) return { kind: "none" };
27048
- return parsed.data.contentOwner === pageDesignEditorBackgroundOverrideOwner ? {
27049
- kind: "page-design-override",
27050
- background: parsed.data
27051
- } : {
27052
- kind: "block-authored",
27053
- background: parsed.data
27054
- };
27055
- }
27056
- function resolveSectionBackground(source, lookDefault) {
27057
- if (source.kind === "page-design-override") {
27058
- return {
27059
- kind: "page-design-override",
27060
- background: source.background
27061
- };
27062
- }
27063
- if (lookDefault) {
27064
- if (source.kind === "block-authored" && (source.background.type === "image" || source.background.type === "video")) {
27065
- return {
27066
- kind: "block-authored",
27067
- background: mergeLookForegroundWithAuthoredMedia(lookDefault, source.background)
27068
- };
27069
- }
27070
- return {
27071
- kind: "look-default",
27072
- background: lookDefault
27073
- };
27074
- }
27075
- if (source.kind === "block-authored" && (source.background.type === "image" || source.background.type === "video")) {
27076
- return {
27077
- kind: "block-authored",
27078
- background: source.background
27079
- };
27080
- }
27081
- return source.kind === "block-authored" ? {
27082
- kind: "block-authored",
27083
- background: source.background
27084
- } : { kind: "none" };
27085
- }
27086
- function resolveSectionForeground(background, palette) {
27087
- return resolveSurfaceForeground(background.kind === "none" ? null : background.background, palette, {}, { compositeBase: "background" });
27088
- }
27089
- /**
27090
- * Read foreground authority from post-compiler block content. The preview
27091
- * compiler has already materialized any Look-owned background into
27092
- * `_sectionStyles`; raw authored content must be composed with its Look through
27093
- * `resolveSectionBackground` instead.
27094
- */
27095
- function resolveCompiledSectionForegroundFromContent(content, palette) {
27096
- if (!content) return resolveSurfaceForeground(null, palette);
27097
- return resolveSectionForeground(resolveSectionBackground(parseCanonicalRuntimeBackground(content), null), palette);
27098
- }
27099
- function mergeLookForegroundWithAuthoredMedia(lookDefault, authored) {
27100
- const merged = boxBackgroundSchema.safeParse({
27101
- ...lookDefault,
27102
- type: authored.type,
27103
- image: authored.image,
27104
- objectFit: authored.objectFit,
27105
- scale: authored.scale,
27106
- position: authored.position
27107
- });
27108
- return merged.success ? merged.data : authored;
27109
- }
27110
- function readRecord$1(value) {
27111
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
27112
- }
27113
-
27114
27509
  //#endregion
27115
27510
  //#region ../theme-core/src/site-styles/previewRuntimeCapabilities.ts
27116
27511
  function cardRoleCapabilitiesByBlockKind(blocks) {
@@ -27586,7 +27981,6 @@ function compileEditorModelRuntime(input) {
27586
27981
  blockKind: block.blockKind,
27587
27982
  purpose: previewCustomizePurpose,
27588
27983
  ...look ? { appearancePresetId: look.id } : {},
27589
- transitionAfter: "none",
27590
27984
  emphasis: "medium",
27591
27985
  sectionSurface: "base",
27592
27986
  contentFrame: { kind: "plain" },
@@ -27596,30 +27990,31 @@ function compileEditorModelRuntime(input) {
27596
27990
  if (blocks.length === 0) {
27597
27991
  return {
27598
27992
  blocks: [],
27599
- boundaries: []
27993
+ joins: []
27600
27994
  };
27601
27995
  }
27602
- const fallbackTransitionAfterByBlockId = new Map(blocks.map((block) => [block.blockId, block.transitionAfter]));
27603
- const compiled = compilePageRuntimeFields({
27996
+ return compilePageRuntimeFields({
27604
27997
  resolved: resolveBlockDesignPlan({
27605
27998
  policy: "user",
27606
27999
  blocks,
27607
28000
  capabilities: runtimeCapabilitiesForBlocks(blocks, cardRoleCapabilitiesByBlockKind(input.editorModel.blocks))
27608
28001
  }),
28002
+ joins: input.editorModel.boundaries.flatMap(runtimeJoinForEditorBoundary),
27609
28003
  surfaceResolutionContext: input.surfaceResolutionContext,
27610
28004
  designRules: input.designRules,
27611
28005
  componentRules: input.componentRules,
27612
28006
  sectionSpacing: input.sectionSpacing
27613
28007
  });
27614
- return {
27615
- blocks: compiled.blocks,
27616
- boundaries: compileEditorBoundaryRuntime({
27617
- boundaries: input.editorModel.boundaries,
27618
- blocks: compiled.blocks,
27619
- fallbackTransitionAfterByPreviousBlockId: fallbackTransitionAfterByBlockId,
27620
- designRules: input.designRules
27621
- })
27622
- };
28008
+ }
28009
+ /** The editor boundary's effective break as an intent-only join; none for no break. */
28010
+ function runtimeJoinForEditorBoundary(boundary) {
28011
+ const option = boundary.availableBreaks.find((candidate$1) => candidate$1.id === boundary.breakChoice.effectiveValue);
28012
+ const treatment = option?.transitionAfter;
28013
+ return treatment && isDecorativeTransitionIntent(treatment) ? [{
28014
+ previousBlockId: boundary.previousBlockId,
28015
+ nextBlockId: boundary.nextBlockId,
28016
+ treatment
28017
+ }] : [];
27623
28018
  }
27624
28019
  function buildCompiledRuntimeFieldsByBlockId(compiled) {
27625
28020
  return new Map(compiled.blocks.map((block) => [block.blockId, block.fields]));
@@ -27627,31 +28022,6 @@ function buildCompiledRuntimeFieldsByBlockId(compiled) {
27627
28022
  function buildEditorModelRuntimeFieldsByBlockId(input) {
27628
28023
  return buildCompiledRuntimeFieldsByBlockId(compileEditorModelRuntime(input));
27629
28024
  }
27630
- function buildEditorModelRuntimeBoundaries(input) {
27631
- return compileEditorModelRuntime(input).boundaries;
27632
- }
27633
- function compileEditorBoundaryRuntime(input) {
27634
- const blockById = new Map(input.blocks.map((block) => [block.blockId, block]));
27635
- return input.boundaries.flatMap((boundary) => {
27636
- const previousBlock = blockById.get(boundary.previousBlockId);
27637
- const nextBlock = blockById.get(boundary.nextBlockId);
27638
- const transitionAfter = transitionAfterForEditorBoundary(boundary) ?? input.fallbackTransitionAfterByPreviousBlockId.get(boundary.previousBlockId) ?? null;
27639
- if (!previousBlock || !nextBlock || !transitionAfter) return [];
27640
- const compiled = compileBoundaryRuntime({
27641
- boundaryId: boundary.boundaryId,
27642
- previousBlockId: boundary.previousBlockId,
27643
- nextBlockId: boundary.nextBlockId,
27644
- treatment: transitionAfter,
27645
- fromSurface: previousBlock.visualSectionSurface,
27646
- toSurface: nextBlock.visualSectionSurface,
27647
- rules: input.designRules
27648
- });
27649
- return compiled ? [compiled] : [];
27650
- });
27651
- }
27652
- function transitionAfterForEditorBoundary(boundary) {
27653
- return boundary.availableBreaks.find((candidate$1) => candidate$1.id === boundary.breakChoice.effectiveValue)?.transitionAfter ?? null;
27654
- }
27655
28025
  function compileEditorModelRuntimeFieldsForBlock(input) {
27656
28026
  return buildEditorModelRuntimeFieldsByBlockId(input).get(input.blockId) ?? null;
27657
28027
  }
@@ -27662,54 +28032,6 @@ function applyCompiledRuntimeFieldsToContent(content, fields) {
27662
28032
  ...runtimeFields
27663
28033
  };
27664
28034
  }
27665
- function retargetCompiledRuntimeFieldsForContentAdjacency(fields, adjacency) {
27666
- if (!fields?._transitionStyles) return fields;
27667
- const transition = fields._transitionStyles;
27668
- const fromSurface = contentOwnedBackgroundEndpoint(adjacency.currentContent) ?? transition.from;
27669
- const toSurface = contentOwnedBackgroundEndpoint(adjacency.nextContent) ?? transition.to;
27670
- if (fromSurface === transition.from && toSurface === transition.to) {
27671
- return fields;
27672
- }
27673
- return {
27674
- ...fields,
27675
- _transitionStyles: compileTransitionRuntime({
27676
- treatment: transition.treatment,
27677
- from: fromSurface,
27678
- to: toSurface
27679
- })
27680
- };
27681
- }
27682
- function retargetCompiledBoundaryRuntimeForContentAdjacency(boundary, adjacency) {
27683
- const fromSurface = contentOwnedBackgroundEndpoint(adjacency.currentContent) ?? boundary.from;
27684
- const toSurface = contentOwnedBackgroundEndpoint(adjacency.nextContent) ?? boundary.to;
27685
- if (fromSurface === boundary.from && toSurface === boundary.to) {
27686
- return boundary;
27687
- }
27688
- const transition = compileTransitionRuntime({
27689
- treatment: boundary.treatment,
27690
- from: fromSurface,
27691
- to: toSurface
27692
- });
27693
- return {
27694
- ...boundary,
27695
- from: transition.from,
27696
- to: transition.to,
27697
- cssVars: transition.cssVars
27698
- };
27699
- }
27700
- function contentOwnedBackgroundEndpoint(content) {
27701
- const sectionStyles = readRecord(content?._sectionStyles);
27702
- const background = readRecord(sectionStyles?.background);
27703
- if (background?.contentOwner !== pageDesignEditorBackgroundOverrideOwner || background.type !== "color") {
27704
- return null;
27705
- }
27706
- const token = parseThemeColorToken(background.color);
27707
- if (!token) return null;
27708
- return {
27709
- kind: "color-token",
27710
- token
27711
- };
27712
- }
27713
28035
  function preserveContentOwnedBackgroundFields(content, fields) {
27714
28036
  const source = parseCanonicalRuntimeBackground(content);
27715
28037
  const resolved = resolveSectionBackground(source, fields._sectionStyles?.background);
@@ -27724,9 +28046,6 @@ function preserveContentOwnedBackgroundFields(content, fields) {
27724
28046
  }
27725
28047
  };
27726
28048
  }
27727
- function readRecord(value) {
27728
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
27729
- }
27730
28049
  function restoreCompiledRuntimeFieldsFromContent(currentContent, previousContent) {
27731
28050
  const nextContent = stripCompiledRuntimeStyleFields(currentContent);
27732
28051
  for (const fieldName of compiledRuntimeStyleFieldNames) {
@@ -27773,7 +28092,7 @@ function compileContentTemplateRuntimeFields(input) {
27773
28092
  }));
27774
28093
  return {
27775
28094
  blocks,
27776
- boundaries: compiled.boundaries,
28095
+ joins: compiled.joins,
27777
28096
  fieldsByBlockId: new Map(blocks.map((block) => [block.blockId, block.fields])),
27778
28097
  issues: editorModel.issues
27779
28098
  };
@@ -28180,9 +28499,6 @@ function defaultContentFrame() {
28180
28499
  function cardIntentFor(design, lookDefaults) {
28181
28500
  return { cardRole: valueOrLookDefault(design.cardRole, lookDefaults?.cardRole, "none") };
28182
28501
  }
28183
- function defaultTransition() {
28184
- return "none";
28185
- }
28186
28502
  function defaultEmphasis(block) {
28187
28503
  return isHeroBlock(block) ? "high" : "medium";
28188
28504
  }
@@ -28207,8 +28523,24 @@ function isDefaultLookSafeForStyle(preset, siteStyle) {
28207
28523
  }
28208
28524
  return preset.design.contentFrame.kind === "plain" && preset.design.cardRole === "none";
28209
28525
  }
28210
- function materializeBoundaryTransition(design, boundary, defaultValue) {
28211
- return valueOrDefault(boundary.transitionAfter, valueOrDefault(design.transitionAfter, defaultValue));
28526
+ /**
28527
+ * The generated pipeline's joins: candidates come from the generated intent
28528
+ * and `resolveJoinTreatment` decides. Joins that resolve to no break are
28529
+ * absent; whether one draws is composition's call.
28530
+ */
28531
+ function resolveGeneratedJoins(input) {
28532
+ const intentByBlockId = new Map(input.intent.blocks.map((block) => [block.blockId, block]));
28533
+ return input.resolved.blocks.flatMap((block, index) => {
28534
+ const next = input.resolved.blocks[index + 1];
28535
+ const blockIntent = intentByBlockId.get(block.blockId);
28536
+ if (!next || !blockIntent) return [];
28537
+ const { join } = resolveJoinTreatment({
28538
+ candidates: joinCandidatesFromGeneratedIntent(blockIntent),
28539
+ previousBlockId: block.blockId,
28540
+ nextBlockId: next.blockId
28541
+ });
28542
+ return join ? [join] : [];
28543
+ });
28212
28544
  }
28213
28545
  function materializeBlockDesignPlan(block, siteStyle) {
28214
28546
  const lookDefaults = defaultAppearanceDesignForBlock({
@@ -28225,7 +28557,6 @@ function materializeBlockDesignPlan(block, siteStyle) {
28225
28557
  sectionSurface,
28226
28558
  contentFrame: valueOrLookDefault(block.design.contentFrame, lookDefaults?.contentFrame, defaultContentFrame()),
28227
28559
  ...cardIntentFor(block.design, lookDefaults),
28228
- transitionAfter: materializeBoundaryTransition(block.design, block.boundaryIntent, defaultTransition()),
28229
28560
  emphasis: valueOrDefault(block.design.emphasis, defaultEmphasis(block)),
28230
28561
  heroLegibility: valueOrLookDefault(block.mediaIntent.heroLegibility, lookDefaults?.heroLegibility, defaultLegibility),
28231
28562
  ...lookDefaults?.sectionHeight ? { sectionHeight: lookDefaults.sectionHeight } : {},
@@ -28281,7 +28612,6 @@ function resolvedBlockToPlan(block) {
28281
28612
  sectionSurface: block.appliedSectionSurface,
28282
28613
  contentFrame: block.appliedContentFrame,
28283
28614
  ...block.appliedCardRole ? { cardRole: block.appliedCardRole } : {},
28284
- transitionAfter: block.appliedTransitionAfter,
28285
28615
  emphasis: block.emphasis,
28286
28616
  sectionHeight: block.appliedSectionHeight,
28287
28617
  heroLegibility: block.appliedHeroLegibility,
@@ -28418,10 +28748,15 @@ function runGeneratedPageDesignPipeline(input) {
28418
28748
  })
28419
28749
  };
28420
28750
  }
28751
+ const joins = resolveGeneratedJoins({
28752
+ intent,
28753
+ resolved
28754
+ });
28421
28755
  const compositionIssues = validateCompositionBudget({
28422
28756
  policy: "generated",
28423
28757
  budget: input.siteStyle.template.compositionBudget,
28424
28758
  resolved,
28759
+ joins,
28425
28760
  blockContexts: intent.blocks.map((block) => ({
28426
28761
  blockId: block.blockId,
28427
28762
  mediaIntent: block.mediaIntent.media,
@@ -28445,6 +28780,7 @@ function runGeneratedPageDesignPipeline(input) {
28445
28780
  }
28446
28781
  const compiledRuntime = compilePageRuntimeFields({
28447
28782
  resolved,
28783
+ joins,
28448
28784
  surfaceResolutionContext: surfaceResolutionContextFromTheme(compileTokenRecipes(input.siteStyle.template.tokenRecipes)),
28449
28785
  designRules: designRuntimeRulesForSiteStyle(input.siteStyle),
28450
28786
  componentRules: componentRuntimeRulesForSiteStyle(input.siteStyle)
@@ -28463,6 +28799,7 @@ function runGeneratedPageDesignPipeline(input) {
28463
28799
  intent,
28464
28800
  plan,
28465
28801
  resolved,
28802
+ joins,
28466
28803
  headerOpeningSectionContext: deriveHeaderOpeningSectionContext({ openingBlock: resolved.blocks[0] ?? null }),
28467
28804
  headerOpeningSectionRepairs: headerOpeningSectionPlan.repairs,
28468
28805
  repairs,
@@ -29851,14 +30188,26 @@ const paidOnlineServiceInputSchema = z.object({
29851
30188
  priceAmount: z.number().int().positive(),
29852
30189
  payment: z.object({ mode: z.literal("paid_online_intent") }).strict()
29853
30190
  }).strict();
30191
+ /**
30192
+ * A service input with one of the three category placements: none, a
30193
+ * `categoryRef` to a category upserted in the same batch, or the
30194
+ * `categoryIdentifier` of one that already exists (issue 1403).
30195
+ */
30196
+ function withAppointmentServiceCategoryPlacement(schema) {
30197
+ return z.union([
30198
+ schema.extend(noAppointmentServiceCategoryFields),
30199
+ schema.extend(appointmentServiceCategoryRefFields),
30200
+ schema.extend(appointmentServiceCategoryIdentifierFields)
30201
+ ]);
30202
+ }
29854
30203
  const upsertAppointmentServiceCommandSchema = z.object({
29855
30204
  type: z.literal("upsertAppointmentService"),
29856
30205
  ref: appointmentServiceRefSchema,
29857
- input: z.union([
29858
- manualRequestServiceInputSchema,
29859
- freeServiceInputSchema,
29860
- paidOnlineServiceInputSchema
29861
- ])
30206
+ input: z.unknown().superRefine(rejectBothAppointmentServiceCategoryKeys).pipe(z.union([
30207
+ withAppointmentServiceCategoryPlacement(manualRequestServiceInputSchema),
30208
+ withAppointmentServiceCategoryPlacement(freeServiceInputSchema),
30209
+ withAppointmentServiceCategoryPlacement(paidOnlineServiceInputSchema)
30210
+ ]))
29862
30211
  }).strict();
29863
30212
  const upsertBookableResourceCommandSchema = z.object({
29864
30213
  type: z.literal("upsertBookableResource"),
@@ -29910,6 +30259,31 @@ function validatePortalOrder(input, ctx) {
29910
30259
  });
29911
30260
  }
29912
30261
 
30262
+ //#endregion
30263
+ //#region ../site-commands/src/bookingCommandSchemaOptions.ts
30264
+ /**
30265
+ * The booking, entitlement and appointment command schemas, in the order the
30266
+ * site command union lists them. `configureBookingSettings` stays with the
30267
+ * site-level commands at the head of the union.
30268
+ */
30269
+ const bookingCommandSchemaOptions = [
30270
+ upsertBookingPassProductCommandSchema,
30271
+ upsertBookingMembershipProductCommandSchema,
30272
+ seedBookingCustomerCommandSchema,
30273
+ grantCustomerPassCommandSchema,
30274
+ grantCustomerMembershipCommandSchema,
30275
+ seedAppointmentBookingCommandSchema,
30276
+ seedEventSeriesRegistrationCommandSchema,
30277
+ seedEventRegistrationCommandSchema,
30278
+ seedPassRedemptionCommandSchema,
30279
+ seedPortalOrderCommandSchema,
30280
+ upsertAppointmentServiceCategoryCommandSchema,
30281
+ upsertAppointmentServiceCommandSchema,
30282
+ upsertBookableResourceCommandSchema,
30283
+ assignAppointmentServiceResourcesCommandSchema,
30284
+ configureAppointmentAvailabilityCommandSchema
30285
+ ];
30286
+
29913
30287
  //#endregion
29914
30288
  //#region ../site-commands/src/commands.ts
29915
30289
  const mediaRefSchema = resourceRefSchema("media");
@@ -30378,20 +30752,7 @@ const siteCommandSchema = z.discriminatedUnion("type", [
30378
30752
  deleteEventCategoryCommandSchema,
30379
30753
  upsertEventSeriesCommandSchema,
30380
30754
  deleteEventSeriesCommandSchema,
30381
- upsertBookingPassProductCommandSchema,
30382
- upsertBookingMembershipProductCommandSchema,
30383
- seedBookingCustomerCommandSchema,
30384
- grantCustomerPassCommandSchema,
30385
- grantCustomerMembershipCommandSchema,
30386
- seedAppointmentBookingCommandSchema,
30387
- seedEventSeriesRegistrationCommandSchema,
30388
- seedEventRegistrationCommandSchema,
30389
- seedPassRedemptionCommandSchema,
30390
- seedPortalOrderCommandSchema,
30391
- upsertAppointmentServiceCommandSchema,
30392
- upsertBookableResourceCommandSchema,
30393
- assignAppointmentServiceResourcesCommandSchema,
30394
- configureAppointmentAvailabilityCommandSchema
30755
+ ...bookingCommandSchemaOptions
30395
30756
  ]);
30396
30757
 
30397
30758
  //#endregion
@@ -30513,6 +30874,7 @@ const commandScopes = [
30513
30874
  "order",
30514
30875
  "booking_settings",
30515
30876
  "appointment_service",
30877
+ "appointment_service_category",
30516
30878
  "appointment_resource",
30517
30879
  "appointment_availability",
30518
30880
  "publish"
@@ -31141,6 +31503,19 @@ const siteCommandMetadata = {
31141
31503
  capability: "commerce.orders.write",
31142
31504
  support: "supported"
31143
31505
  },
31506
+ upsertAppointmentServiceCategory: {
31507
+ scope: "appointment_service_category",
31508
+ risk: "medium",
31509
+ writeSemantics: "upsert",
31510
+ lifecycleIntent: "mutation",
31511
+ idempotencyStrategy: "natural_key",
31512
+ conflictPolicy: "update_existing",
31513
+ previewability: "dry_run_only",
31514
+ allowedRuntimes: allRuntimes,
31515
+ intendedExposure: recipeExposure,
31516
+ capability: "bookings.appointment_service_categories.write",
31517
+ support: "supported"
31518
+ },
31144
31519
  upsertAppointmentService: {
31145
31520
  scope: "appointment_service",
31146
31521
  risk: "medium",
@@ -31320,6 +31695,7 @@ const bookingRefCommandTypeValues = [
31320
31695
  "seedEventRegistration",
31321
31696
  "seedPassRedemption",
31322
31697
  "seedPortalOrder",
31698
+ "upsertAppointmentServiceCategory",
31323
31699
  "upsertAppointmentService",
31324
31700
  "upsertBookableResource",
31325
31701
  "assignAppointmentServiceResources",
@@ -31333,8 +31709,12 @@ function bookingCommandRefContributions(command) {
31333
31709
  switch (command.type) {
31334
31710
  case "upsertBookingMembershipProduct":
31335
31711
  case "seedBookingCustomer":
31336
- case "upsertAppointmentService":
31712
+ case "upsertAppointmentServiceCategory":
31337
31713
  case "upsertBookableResource": return provided(command.ref);
31714
+ case "upsertAppointmentService": return providedWithRequirements(command.ref, command.input.categoryRef === undefined ? [] : [{
31715
+ ref: command.input.categoryRef,
31716
+ field: "input.categoryRef"
31717
+ }]);
31338
31718
  case "upsertBookingPassProduct": return {
31339
31719
  ...provided(command.ref),
31340
31720
  requires: command.input.appointmentScope?.mode === "selected_services" ? command.input.appointmentScope.serviceRefs.map((ref, index) => ({
@@ -31867,7 +32247,8 @@ function matchSiteCommand(command, match) {
31867
32247
  case "seedEventRegistration": return match(command);
31868
32248
  case "seedPassRedemption": return match(command);
31869
32249
  case "seedPortalOrder": return match(command);
31870
- case "upsertAppointmentService": return match(command);
32250
+ case "upsertAppointmentServiceCategory":
32251
+ case "upsertAppointmentService":
31871
32252
  case "upsertBookableResource": return match(command);
31872
32253
  case "assignAppointmentServiceResources": return match(command);
31873
32254
  case "configureAppointmentAvailability": return match(command);
@@ -32877,4 +33258,4 @@ function parseSdkEventRunUpsertInput(input, runIndex, options) {
32877
33258
  }
32878
33259
 
32879
33260
  //#endregion
32880
- export { generateBaseInteractiveCSS as $, parseBlockColorToken as $n, NAV_LINK_BORDER_RADIUS_MAP as $t, getGradientPreset as A, buildThemeTokens as An, resolveThemeColorReferenceValue as At, generateCssVars as B, surfaceForegroundValue as Bn, eyebrowTreatmentValues as Bt, retargetCompiledBoundaryRuntimeForContentAdjacency as C, textColorStyle as Cn, MQ_MD_DOWN as Ct, compileTransitionRuntime as D, resolveSurfaceForeground as Dn, headerVariantValues as Dt, getCuratedSiteStyle as E, surfaceToneFromForegroundToken as En, MQ_TABLET_ONLY as Et, parseBackgroundAppearanceColorToken as F, hexToRgb as Fn, assertNever$1 as Ft, resolvedButtonColourDeclarations as G, mediaIdentifierSchema as Gn, offeringsPreviewCompositionTreatmentValues as Gt, PUBLIC_BUTTON_ROLES as H, cardRoleSchema as Hn, headingDecorationColorTokenValues as Ht, defineBlockDesignMetadata as I, oklchToHexGamut as In, defineBlockDesignCapability as It, contextualButtonSurfaceStyles as J, persistableMediaSchema as Jn, visibleHeadingDecorationFamilyValues as Jt, resolvedButtonFocusDeclarations as K, mediaSchema as Kn, testimonialsCompositionTreatmentValues as Kt, require_jsx_runtime as L, DEFAULT_SURFACE_TEXT_COLOR as Ln, defineFrameCapability as Lt, boundaryRuntimeMetricsForTransition as M, ORDINARY_TEXT_MIN_CONTRAST_RATIO as Mn, pageDesignEditorBackgroundOverrideOwner as Mt, backgroundAppearanceColorTokenNames as N, contrastRatio as Nn, asAppearancePresetId as Nt, gradientToCss as O, isOnColorBackgroundCandidateToken as On, isCssColorLiteral as Ot, backgroundAppearanceQuickColorTokenNames as P, featuresFromHex as Pn, asDesignBlockId as Pt, generateEffectsCSS as Q, unsafeAsOpaqueButtonColourToken as Qn, parseCustomCssRules as Qt, require_react as R, surfaceForegroundDeclarations as Rn, defineNoBlockDesignCapability as Rt, eventBookingOfferIntentSchema as S, rgbColorValue as Sn, MQ_LG_UP as St, readHeaderNavFontSizePreference as T, selectLogoVariant as Tn, MQ_SM_UP as Tt, generateButtonCss as U, cardVariantRoleValues as Un, introCompositionTreatmentValues as Ut, buttonStyleSelectionSchema as V, generateShadowValue$1 as Vn, featureCardCompositionTreatmentValues as Vt, RESOLVED_BUTTON_DISABLED_DECLARATIONS as W, normalizeCardRole as Wn, makeBoundaryId as Wt, resolveContextualButtonRole as X, VARIANT_ROLES as Xn, getComponentShadowVar as Xt, deriveButtonSurfaceContext as Y, footerModeSchema as Yn, getComponentBorderWidthPx as Yt, generateDefaultButtonSystem as Z, buttonVariantIdSchema as Zn, parseBlockCustomCssRules as Zt, entryIdentifierForCommand as _, TokenResolver as _n, renderHeaderPaintTargetRules as _t, classifySdkEventBookingOffer as a, INPUT_FONT_SIZE_MAP as an, createEventSeriesRef as ar, parseThemeForPersistence as at, pagePathForCommand as b, cssVarValue as bn, BREAKPOINT_SM as bt, stableStringify as c, ACCORDION_OPEN_ACCENT_THICKNESS as cn, createNavigationRef as cr, generateShadowValue as ct, mapEventCommandContent as d, ACCORDION_TRIGGER_LETTER_SPACING as dn, createVenueRef as dr, themeSchema as dt, NAV_LINK_LETTER_SPACING_MAP as en, createBlockRef as er, generateInteractiveChildInheritanceCSS as et, evaluateSiteCommandBatchExposure as f, ICON_CONFIG as fn, refSlug as fr, boxBackgroundOverlaySchema as ft, blockOrderIdentifiersForCommand as g, generateCardCss as gn, getHeaderPaintTargets as gt, blockIdentifierForCommand as h, getDefaultCardVariants as hn, generateMobileOverlayBackdropFilterFix as ht, classifySdkEventSchedule as i, FONT_WEIGHT_MAP$1 as in, createEventCategoryRef as ir, themeButtonVariantClassNames as it, parseGradientPresetId as j, defaultThemeTokens as jn, resolveTokenOrCssColorValue as jt, GRADIENT_PRESET_IDS as k, onColorTokenName as kn, resolveOnColorForThemeToken as kt, createDryRunReport as l, ACCORDION_SPACING_MAP as ln, createPageRef as lr, expandPalette as lt, getSiteCommandMetadata as m, generateCardCoreCss as mn, __toESM as mr, generateHeaderScrollTreatmentStyles as mt, sdkEventOccurrenceInputSchema as n, NAV_LINK_PADDING_X_MAP as nn, createCommandRef as nr, themeButtonExtraClassesWithTrailingIcon as nt, venueManagementFields as o, INPUT_SPACING_MAP as on, createFooterRef as or, compileButtonColourStates as ot, evaluateSiteCommandExposure as p, ICON_SIZE_PX as pn, slugPart as pr, boxBackgroundSchema as pt, contextualButtonSurfaceResetStyles as q, mediaTypeDiscriminants as qn, tileTreatmentValues as qt, sdkEventUpsertOptionalFieldSchemas as r, NAV_LINK_TEXT_SIZE_MAP as rn, createContentEntryRef as rr, themeButtonSizeClassName as rt, sortRecord as s, LABEL_FONT_SIZE_MAP as sn, createFormRef as sr, formatButtonColourContractError as st, classifySdkEventTiming as t, NAV_LINK_PADDING_MAP as tn, createBlocksRef as tr, parseThemeButtonVariantId as tt, parseAndPlanCommandBatchForTypes as u, ACCORDION_TRIGGER_FONT_SCALE as un, createSiteCommandSiteRef as ur, getTextColorForBackground as ut, pageIdentifierForBlockCommand as v, backgroundColorStyle as vn, BREAKPOINT_LG as vt, mergeHeaderNavTypographyPreference as w, textColorValue as wn, MQ_SM_DOWN as wt, hasConfiguredBookingVertical as x, mergeStyles as xn, BREAKPOINT_XL as xt, pageIdentifierForCommand as y, borderColorStyle as yn, BREAKPOINT_MD as yt, pickShadeTowardBackground as z, surfaceForegroundStyle as zn, eventListTreatmentValues as zt };
33261
+ export { resolveContextualButtonRole as $, footerModeSchema as $n, getComponentBorderWidthPx as $t, boundaryLayout as A, surfaceToneFromForegroundToken as An, headerVariantValues as At, require_jsx_runtime as B, oklchToHexGamut as Bn, defineBlockDesignCapability as Bt, mergeHeaderNavTypographyPreference as C, borderColorStyle as Cn, BREAKPOINT_SM as Ct, GRADIENT_PRESET_IDS as D, textColorStyle as Dn, MQ_SM_DOWN as Dt, gradientToCss as E, rgbColorValue as En, MQ_MD_DOWN as Et, transitionCatalogueEntries as F, defaultThemeTokens as Fn, pageDesignEditorBackgroundOverrideOwner as Ft, PUBLIC_BUTTON_ROLES as G, generateShadowValue$1 as Gn, featureCardCompositionTreatmentValues as Gt, pickShadeTowardBackground as H, surfaceForegroundDeclarations as Hn, defineNoBlockDesignCapability as Ht, backgroundAppearanceColorTokenNames as I, ORDINARY_TEXT_MIN_CONTRAST_RATIO as In, stripStaleTransitionStyleGroup as It, resolvedButtonColourDeclarations as J, normalizeCardRole as Jn, makeBoundaryId as Jt, generateButtonCss as K, cardRoleSchema as Kn, headingDecorationColorTokenValues as Kt, backgroundAppearanceQuickColorTokenNames as L, contrastRatio as Ln, asAppearancePresetId as Lt, sectionFactsForContent as M, isOnColorBackgroundCandidateToken as Mn, resolveOnColorForThemeToken as Mt, boundaryRuntimeMetricsForDecorativeTransition as N, onColorTokenName as Nn, resolveThemeColorReferenceValue as Nt, getGradientPreset as O, textColorValue as On, MQ_SM_UP as Ot, anglePolygonCss as P, buildThemeTokens as Pn, resolveTokenOrCssColorValue as Pt, deriveButtonSurfaceContext as Q, persistableMediaSchema as Qn, visibleHeadingDecorationFamilyValues as Qt, parseBackgroundAppearanceColorToken as R, featuresFromHex as Rn, asDesignBlockId as Rt, eventBookingOfferIntentSchema as S, backgroundColorStyle as Sn, BREAKPOINT_MD as St, getCuratedSiteStyle as T, mergeStyles as Tn, MQ_LG_UP as Tt, generateCssVars as U, surfaceForegroundStyle as Un, eventListTreatmentValues as Ut, require_react as V, DEFAULT_SURFACE_TEXT_COLOR as Vn, defineFrameCapability as Vt, buttonStyleSelectionSchema as W, surfaceForegroundValue as Wn, eyebrowTreatmentValues as Wt, contextualButtonSurfaceResetStyles as X, mediaSchema as Xn, testimonialsCompositionTreatmentValues as Xt, resolvedButtonFocusDeclarations as Y, mediaIdentifierSchema as Yn, offeringsPreviewCompositionTreatmentValues as Yt, contextualButtonSurfaceStyles as Z, mediaTypeDiscriminants as Zn, tileTreatmentValues as Zt, entryIdentifierForCommand as _, ICON_SIZE_PX as _n, slugPart as _r, generateHeaderScrollTreatmentStyles as _t, classifySdkEventBookingOffer as a, NAV_LINK_PADDING_MAP as an, createBlocksRef as ar, themeButtonExtraClassesWithTrailingIcon as at, pagePathForCommand as b, generateCardCss as bn, renderHeaderPaintTargetRules as bt, stableStringify as c, FONT_WEIGHT_MAP$1 as cn, createEventCategoryRef as cr, parseThemeForPersistence as ct, mapEventCommandContent as d, LABEL_FONT_SIZE_MAP as dn, createFormRef as dr, generateShadowValue as dt, getComponentShadowVar as en, VARIANT_ROLES as er, generateDefaultButtonSystem as et, evaluateSiteCommandBatchExposure as f, ACCORDION_OPEN_ACCENT_THICKNESS as fn, createNavigationRef as fr, expandPalette as ft, blockOrderIdentifiersForCommand as g, ICON_CONFIG as gn, refSlug as gr, boxBackgroundSchema as gt, blockIdentifierForCommand as h, ACCORDION_TRIGGER_LETTER_SPACING as hn, createVenueRef as hr, boxBackgroundOverlaySchema as ht, classifySdkEventSchedule as i, NAV_LINK_LETTER_SPACING_MAP as in, createBlockRef as ir, parseThemeButtonVariantId as it, composeBoundary as j, resolveSurfaceForeground as jn, isCssColorLiteral as jt, parseGradientPresetId as k, selectLogoVariant as kn, MQ_TABLET_ONLY as kt, createDryRunReport as l, INPUT_FONT_SIZE_MAP as ln, createEventSeriesRef as lr, compileButtonColourStates as lt, getSiteCommandMetadata as m, ACCORDION_TRIGGER_FONT_SCALE as mn, createSiteCommandSiteRef as mr, themeSchema as mt, sdkEventOccurrenceInputSchema as n, parseCustomCssRules as nn, unsafeAsOpaqueButtonColourToken as nr, generateBaseInteractiveCSS as nt, venueManagementFields as o, NAV_LINK_PADDING_X_MAP as on, createCommandRef as or, themeButtonSizeClassName as ot, evaluateSiteCommandExposure as p, ACCORDION_SPACING_MAP as pn, createPageRef as pr, getTextColorForBackground as pt, RESOLVED_BUTTON_DISABLED_DECLARATIONS as q, cardVariantRoleValues as qn, introCompositionTreatmentValues as qt, sdkEventUpsertOptionalFieldSchemas as r, NAV_LINK_BORDER_RADIUS_MAP as rn, parseBlockColorToken as rr, generateInteractiveChildInheritanceCSS as rt, sortRecord as s, NAV_LINK_TEXT_SIZE_MAP as sn, createContentEntryRef as sr, themeButtonVariantClassNames as st, classifySdkEventTiming as t, parseBlockCustomCssRules as tn, buttonVariantIdSchema as tr, generateEffectsCSS as tt, parseAndPlanCommandBatchForTypes as u, INPUT_SPACING_MAP as un, createFooterRef as ur, formatButtonColourContractError as ut, pageIdentifierForBlockCommand as v, generateCardCoreCss as vn, __toESM as vr, generateMobileOverlayBackdropFilterFix as vt, readHeaderNavFontSizePreference as w, cssVarValue as wn, BREAKPOINT_XL as wt, hasConfiguredBookingVertical as x, TokenResolver as xn, BREAKPOINT_LG as xt, pageIdentifierForCommand as y, getDefaultCardVariants as yn, getHeaderPaintTargets as yt, defineBlockDesignMetadata as z, hexToRgb as zn, assertNever$1 as zt };