@vectoriox/iox-ui 5.13.1 → 5.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -183,6 +183,12 @@ declare class BuilderImageComponent {
183
183
  src: string;
184
184
  alt: string;
185
185
  nodeId: string;
186
+ /**
187
+ * What a MARKED image shows while it has no source (design-system DS3) — set by each renderer from
188
+ * the shared `emptyImageSrc` (`@vectoriox/iox-ui/design-core`): the "Add image" label on the canvas,
189
+ * the blank on the live site, `null` for an unmarked image, which therefore renders as before.
190
+ */
191
+ emptySrc: string | null;
186
192
  /**
187
193
  * The `src` attribute value, or `null` to OMIT the attribute entirely when there is no image.
188
194
  *
@@ -194,8 +200,10 @@ declare class BuilderImageComponent {
194
200
  * filled when the visitor interacts (see element-context-state-plan.md).
195
201
  */
196
202
  get srcAttr(): string | null;
203
+ /** `data-iox-empty` while the placeholder stands in — the hook the shared fill rule keys on. */
204
+ get emptyAttr(): '' | null;
197
205
  static ɵfac: i0.ɵɵFactoryDeclaration<BuilderImageComponent, never>;
198
- static ɵcmp: i0.ɵɵComponentDeclaration<BuilderImageComponent, "img[iox-img]", never, { "src": { "alias": "src"; "required": false; }; "alt": { "alias": "alt"; "required": false; }; "nodeId": { "alias": "nodeId"; "required": false; }; }, {}, never, never, false, never>;
206
+ static ɵcmp: i0.ɵɵComponentDeclaration<BuilderImageComponent, "img[iox-img]", never, { "src": { "alias": "src"; "required": false; }; "alt": { "alias": "alt"; "required": false; }; "nodeId": { "alias": "nodeId"; "required": false; }; "emptySrc": { "alias": "emptySrc"; "required": false; }; }, {}, never, never, false, never>;
199
207
  }
200
208
 
201
209
  /** Directive-host spacer: the `<div>` IS the host — no wrapper. All styles on `.iox-node`. */
@@ -1634,6 +1642,19 @@ interface PageStyleSettings {
1634
1642
  */
1635
1643
  maxWidth: string;
1636
1644
  }
1645
+ interface PageStyleOptions {
1646
+ /**
1647
+ * `pageSettings.designSystem` (design-system DS3). A page created with the design system — or
1648
+ * adopted into it (DS7) — falls back to the org's tokens for whatever it does not set itself:
1649
+ * `background: var(--iox-page-bg)` (a PAINT — Glass and Midnight are gradients, so the shorthand),
1650
+ * `color: var(--iox-text)`, `font-family: var(--iox-font-body)`. A page without the flag (every
1651
+ * page saved before DS3) compiles exactly as before, so a seeded org never puts a new ground under
1652
+ * legacy text — the page follows the same opt-in rule as the element-style marker (§3b-bis).
1653
+ */
1654
+ designSystem?: boolean;
1655
+ }
1656
+ /** The token fallbacks of a design-system page (spec tokens only). */
1657
+ declare const PAGE_TOKEN_FALLBACKS: Readonly<Record<string, string>>;
1637
1658
  /**
1638
1659
  * Compile page style settings into CSS declarations.
1639
1660
  *
@@ -1643,7 +1664,7 @@ interface PageStyleSettings {
1643
1664
  * `backgroundType` selects which background field wins — it is derived from the author's
1644
1665
  * colour/image choice rather than picked directly (see the builder's page-background.util).
1645
1666
  */
1646
- declare function compilePageStyle(style: Partial<PageStyleSettings> | null | undefined): Record<string, string>;
1667
+ declare function compilePageStyle(style: Partial<PageStyleSettings> | null | undefined, options?: PageStyleOptions): Record<string, string>;
1647
1668
 
1648
1669
  /**
1649
1670
  * Shared data-source resolving pattern — the SINGLE source of truth for how an IoxDataSource
@@ -2772,5 +2793,5 @@ declare class GalleryLoop {
2772
2793
  private emit;
2773
2794
  }
2774
2795
 
2775
- export { AOSService, BACKDROP_TWIN_KEY, BACKDROP_WHOLE_KEY, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, CONTAINER_HTML_TAGS, CONTEXT_ACTION_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ContainerComponent, ContextStore, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_PHOSPHOR_WEIGHT, DEFAULT_WIDTH_BY_TYPE, DeclarationContext, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, HOVER_DEFAULT_TRANSITION, HOVER_STATES, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxDialogComponent, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, LEGACY_HOVER_TRANSITION, LinkedContainerComponent, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, RESERVED_ALIAS_PREFIX, STATE_PSEUDO_CLASS, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_RESPONSIVE_TRAITS, SectionComponent, TRANSITION_FEELS, TextBlockComponent, VALIDATION_STATES, VALIDATION_STATE_CLASS, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, activeActions, applyBindingPipes, applyFeel, breakpointDef, breakpointForWidth, breakpointQueryFor, buildDataSourceFilter, buildPartSelector, buildRelatedFilter, buildSwiperBreakpoints, buildTargetedStateSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, collectContextSlotKeys, collectReferencedAliases, compileDeclarations, compilePageStyle, composeVirtualTraits, computeRelative, containerHtmlTag, contextActionEffect, contextSlotKeys, countOverrides, dataSourcePlanToRequest, declaredInputNames, decomposeBackdropFilter, defaultHoverTransition, defaultMessageResolver, deriveSeoFromContent, describeSeoFields, effectiveTransitionId, excludeSelf, feelFor, getByPath, getSeoValue, hasHoverState, hasOwnTransition, initialSlotValues, isActionEnabled, isContextAction, isInputOverriddenAt, isOverriddenAt, isReservedAlias, isValidSlotKey, isWholeBackdropSet, legacyEnterToTransition, matchRouteParams, mediaQueryFor, mergeSwiperBreakpoints, nearestCommonAncestorId, nodeCssId, nodeUsesContext, normalizeCollectionResult, normalizeGalleryImage, normalizeIconClass, parentBreakpoint, parseUrlList, partClassName, planDataSource, planResponsiveRules, referencedDataSources, referencedDataSourcesDeep, renderDefaultDeclarations, resetSeoField, resolveBoundValue, resolveContextValue, resolveDerivedInto, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveRelativeInto, resolveRepeaterItems, resolveSeo, resolveSiblings, resolveSingleItemId, resolveStateStyles, resolveStyleProps, resolveValueChain, rewriteViewportUnits, safeSetInput, setInputOverride, setOverride, setSeoAuto, setSeoBinding, setSeoOverride, setSeoValue, stateNamesIn, statePseudoClass, stripSuppressedProps, variableAliases, variableItemsFrom, withContextSlots, withDefaultHoverTransition, wrapInBreakpointQuery, wrapInMediaQuery };
2776
- export type { BoundValueSpec, BreakpointDef, BuildRelatedFilterOptions, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, DataSourceDomain, DataSourceEndpoints, DataSourceMode, DataSourcePlan, DataSourceQueryFilter, DataSourceRequest, DataSourceRequestSpec, DataSourceRouteFilter, DataSourceSpec, FormMessageResolver, GalleryBoundSource, GalleryDurations, GalleryEffect, GalleryEffectDescriptor, GalleryImage, GalleryLoopOptions, GalleryLoopScheduler, GalleryLoopState, GallerySlideState, IBuilderEventEmitter, InputMap, IoxContextSlot, IoxPipe, OverrideBreakpoint, PageStyleSettings, PageTransitionCategory, PageTransitionCategoryMeta, PageTransitionOptions, PageTransitionPreset, PipeArgSpec, PipeContext, PipeDescriptor, RelativeInputs, RelativeSourceKind, RelativeSourceSpec, ResolveSiblingsOptions, ResolvedPageTransition, ResponsiveLayer, ResponsiveOverrides, ResponsiveRuleSet, RouteContext, SeoDerivedContent, SeoFieldSource, SeoFieldState, SeoResolveContext, SetContextParams, SiblingsResult, StateStyleMap, StyleMap, StyleTreeNode, SwiperBreakpointMap, ToggleableAction, TransitionFeel, TransitionPhase, TransitionTimelineInput, ValueSource, VirtualTraitGroup };
2796
+ export { AOSService, BACKDROP_TWIN_KEY, BACKDROP_WHOLE_KEY, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, CONTAINER_HTML_TAGS, CONTEXT_ACTION_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ContainerComponent, ContextStore, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_PHOSPHOR_WEIGHT, DEFAULT_WIDTH_BY_TYPE, DeclarationContext, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, HOVER_DEFAULT_TRANSITION, HOVER_STATES, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxDialogComponent, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, LEGACY_HOVER_TRANSITION, LinkedContainerComponent, PAGE_TOKEN_FALLBACKS, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, RESERVED_ALIAS_PREFIX, STATE_PSEUDO_CLASS, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_RESPONSIVE_TRAITS, SectionComponent, TRANSITION_FEELS, TextBlockComponent, VALIDATION_STATES, VALIDATION_STATE_CLASS, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, activeActions, applyBindingPipes, applyFeel, breakpointDef, breakpointForWidth, breakpointQueryFor, buildDataSourceFilter, buildPartSelector, buildRelatedFilter, buildSwiperBreakpoints, buildTargetedStateSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, collectContextSlotKeys, collectReferencedAliases, compileDeclarations, compilePageStyle, composeVirtualTraits, computeRelative, containerHtmlTag, contextActionEffect, contextSlotKeys, countOverrides, dataSourcePlanToRequest, declaredInputNames, decomposeBackdropFilter, defaultHoverTransition, defaultMessageResolver, deriveSeoFromContent, describeSeoFields, effectiveTransitionId, excludeSelf, feelFor, getByPath, getSeoValue, hasHoverState, hasOwnTransition, initialSlotValues, isActionEnabled, isContextAction, isInputOverriddenAt, isOverriddenAt, isReservedAlias, isValidSlotKey, isWholeBackdropSet, legacyEnterToTransition, matchRouteParams, mediaQueryFor, mergeSwiperBreakpoints, nearestCommonAncestorId, nodeCssId, nodeUsesContext, normalizeCollectionResult, normalizeGalleryImage, normalizeIconClass, parentBreakpoint, parseUrlList, partClassName, planDataSource, planResponsiveRules, referencedDataSources, referencedDataSourcesDeep, renderDefaultDeclarations, resetSeoField, resolveBoundValue, resolveContextValue, resolveDerivedInto, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveRelativeInto, resolveRepeaterItems, resolveSeo, resolveSiblings, resolveSingleItemId, resolveStateStyles, resolveStyleProps, resolveValueChain, rewriteViewportUnits, safeSetInput, setInputOverride, setOverride, setSeoAuto, setSeoBinding, setSeoOverride, setSeoValue, stateNamesIn, statePseudoClass, stripSuppressedProps, variableAliases, variableItemsFrom, withContextSlots, withDefaultHoverTransition, wrapInBreakpointQuery, wrapInMediaQuery };
2797
+ export type { BoundValueSpec, BreakpointDef, BuildRelatedFilterOptions, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, DataSourceDomain, DataSourceEndpoints, DataSourceMode, DataSourcePlan, DataSourceQueryFilter, DataSourceRequest, DataSourceRequestSpec, DataSourceRouteFilter, DataSourceSpec, FormMessageResolver, GalleryBoundSource, GalleryDurations, GalleryEffect, GalleryEffectDescriptor, GalleryImage, GalleryLoopOptions, GalleryLoopScheduler, GalleryLoopState, GallerySlideState, IBuilderEventEmitter, InputMap, IoxContextSlot, IoxPipe, OverrideBreakpoint, PageStyleOptions, PageStyleSettings, PageTransitionCategory, PageTransitionCategoryMeta, PageTransitionOptions, PageTransitionPreset, PipeArgSpec, PipeContext, PipeDescriptor, RelativeInputs, RelativeSourceKind, RelativeSourceSpec, ResolveSiblingsOptions, ResolvedPageTransition, ResponsiveLayer, ResponsiveOverrides, ResponsiveRuleSet, RouteContext, SeoDerivedContent, SeoFieldSource, SeoFieldState, SeoResolveContext, SetContextParams, SiblingsResult, StateStyleMap, StyleMap, StyleTreeNode, SwiperBreakpointMap, ToggleableAction, TransitionFeel, TransitionPhase, TransitionTimelineInput, ValueSource, VirtualTraitGroup };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.13.1",
3
+ "version": "5.15.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -0,0 +1,116 @@
1
+ import { deriveBrandTokens } from './brand';
2
+ import { SPEC_LEGACY_ALIASES } from './flavor-spec.generated';
3
+ import { SWISS_FLAVOR } from './flavor';
4
+ import { buildFlavorTokens } from './flavor-tokens';
5
+ import { FlavorSeedPlan, planFlavorSeed, SeedBaseline, SeedExpectation } from './flavor-seed';
6
+ import { DesignChoices, DesignTokenRow, flavorSourceStamp, StoredDesignTokenRow } from './design-token.model';
7
+ import { flavorPackById, isRetiredFlavorId, packDesignChoices, StoredDesign } from './design-settings';
8
+
9
+ /**
10
+ * Applying a look (design-system DS4): the COMPLETE `buildFlavorTokens` set for the chosen
11
+ * `DesignChoices`, merged into the org's rows by the §10 C2 rule — rewrite only what the previous
12
+ * seed wrote and nobody touched since, keep the author's rows, report what was kept. And the one
13
+ * migration the contract names: an org still on Swiss becomes Crisp.
14
+ * Pure plans; each caller (builder, the server-side site build) applies them with its own transport.
15
+ */
16
+
17
+ const SEED_STAMP = /^flavor:([^@]+)@/;
18
+
19
+ const expectations = (rows: ReadonlyArray<Pick<DesignTokenRow, 'name' | 'value' | 'values'>>): Map<string, SeedExpectation> =>
20
+ new Map(rows.map(r => [r.name, { value: r.value, ...(r.values?.dark ? { dark: r.values.dark } : {}) }]));
21
+
22
+ /** Every row a design writes — `buildFlavorTokens` for its pack. Empty for an unknown pack. */
23
+ export function designTokenRows(choices: DesignChoices): DesignTokenRow[] {
24
+ const pack = flavorPackById(choices.flavorId);
25
+ return pack ? buildFlavorTokens(pack, { ...choices, flavorId: pack.id }) : [];
26
+ }
27
+
28
+ /** What the F1/F2 Swiss seeding wrote: the four Swiss rows + the brand set derived from its seed. */
29
+ function swissExpectations(seed: string | null): Map<string, SeedExpectation> {
30
+ const rows: Array<Pick<DesignTokenRow, 'name' | 'value' | 'values'>> =
31
+ SWISS_FLAVOR.tokens.map(t => ({ name: t.name, value: t.value, values: t.values }));
32
+ if (seed) {
33
+ try { rows.push(...deriveBrandTokens(seed, SWISS_FLAVOR).map(t => ({ name: t.name, value: t.value, values: t.values }))); }
34
+ catch { /* an unparseable stored seed proves nothing about the rows */ }
35
+ }
36
+ return expectations(rows);
37
+ }
38
+
39
+ /**
40
+ * What the PREVIOUS design wrote, as a baseline keyed by the row's own stamp: a row Swiss stamped is
41
+ * judged against what Swiss wrote, every other row against `buildFlavorTokens` of the stored choices
42
+ * (for an org with no design row, Crisp's defaults — exactly what DS2's interim seed wrote). In a
43
+ * Swiss org the only v2 rows are that interim seed's, so they are judged against Crisp's DEFAULTS,
44
+ * never against the brand-seeded Crisp the migration is about to write.
45
+ */
46
+ export function designBaseline(previous: StoredDesign): SeedBaseline {
47
+ const retired = isRetiredFlavorId(previous.storedFlavorId);
48
+ const current = expectations(designTokenRows(retired ? packDesignChoices(previous.choices.flavorId) : previous.choices));
49
+ const swiss = swissExpectations(retired ? previous.storedSeed : null);
50
+ return (row: StoredDesignTokenRow) => {
51
+ const stampedBy = SEED_STAMP.exec(row.source ?? '')?.[1];
52
+ return stampedBy === SWISS_FLAVOR.id ? swiss.get(row.name) : current.get(row.name);
53
+ };
54
+ }
55
+
56
+ /** Apply `next` over the org's rows, judged against `previous` — the plan FlavorApplyService runs. */
57
+ export function planDesignApply(
58
+ existing: ReadonlyArray<StoredDesignTokenRow>,
59
+ previous: StoredDesign,
60
+ next: DesignChoices,
61
+ ): FlavorSeedPlan {
62
+ const pack = flavorPackById(next.flavorId);
63
+ if (!pack) return { create: [], update: [], kept: [] };
64
+ return planFlavorSeed(designTokenRows(next), existing, flavorSourceStamp(pack.id), designBaseline(previous));
65
+ }
66
+
67
+ /** The rows a design is missing by name — the builder-load top-up (never touches an existing row). */
68
+ export function missingDesignRows(existing: ReadonlyArray<{ name: string }>, choices: DesignChoices): DesignTokenRow[] {
69
+ const have = new Set((existing ?? []).map(r => r.name));
70
+ return designTokenRows(choices).filter(r => !have.has(r.name));
71
+ }
72
+
73
+ export interface SwissMigration {
74
+ /** The design the org now wears: Crisp, carrying the stored brand seed when it is a colour. */
75
+ choices: DesignChoices;
76
+ /** The token merge, the four legacy alias rewrites included. */
77
+ plan: FlavorSeedPlan;
78
+ }
79
+
80
+ const LEGACY_ALIASES: Readonly<Record<string, string>> = SPEC_LEGACY_ALIASES;
81
+
82
+ /**
83
+ * Swiss → Crisp (spec `legacyAliases`; D1). Needed when the design row says a retired flavor, or when
84
+ * there is no design row but the org holds Swiss-stamped alias rows (F1 seeded those before the row
85
+ * existed). Idempotent: once the row says Crisp nothing is needed, and re-running the plan writes
86
+ * nothing.
87
+ *
88
+ * - The look: `planDesignApply` from Swiss to Crisp — Swiss-stamped rows untouched since Swiss seeded
89
+ * them (incl. F2's `surface` / `line` / `accent`) become Crisp's; edited ones are kept.
90
+ * - The aliases: `radius-control` / `radius-surface` / `space-control` / `space-stack` are rewritten to
91
+ * their alias reference ONLY while still machine-owned and untouched, so a node saved since F1 that
92
+ * reads `var(--iox-radius-control)` keeps resolving — now to Crisp's button radius.
93
+ */
94
+ export function planSwissMigration(existing: ReadonlyArray<StoredDesignTokenRow>, stored: StoredDesign): SwissMigration | null {
95
+ const swissAliasRows = (existing ?? []).filter(r => r.name in LEGACY_ALIASES && SEED_STAMP.exec(r.source ?? '')?.[1] === SWISS_FLAVOR.id);
96
+ const needed = isRetiredFlavorId(stored.storedFlavorId) || (!stored.hasRow && swissAliasRows.length > 0);
97
+ if (!needed) return null;
98
+
99
+ const choices = stored.choices;
100
+ const previous: StoredDesign = isRetiredFlavorId(stored.storedFlavorId) ? stored : { ...stored, storedFlavorId: SWISS_FLAVOR.id };
101
+ const plan = planDesignApply(existing, previous, choices);
102
+ const stamp = flavorSourceStamp(choices.flavorId);
103
+ const swiss = swissExpectations(null);
104
+ for (const row of (existing ?? []).filter(r => r.name in LEGACY_ALIASES)) {
105
+ const alias = LEGACY_ALIASES[row.name];
106
+ if (row.value === alias) continue;
107
+ const was = swiss.get(row.name);
108
+ const untouched = SEED_STAMP.test(row.source ?? '') && !!was && row.value === was.value && !row.values?.dark;
109
+ if (untouched && row._id) {
110
+ plan.update.push({ id: row._id, data: { value: alias, values: {}, source: stamp } });
111
+ } else {
112
+ plan.kept.push(row.name);
113
+ }
114
+ }
115
+ return { choices, plan };
116
+ }
@@ -0,0 +1,194 @@
1
+ import { SPEC_AXES, SPEC_FONT_ROLES } from './flavor-spec.generated';
2
+ import { parseHexColor } from './brand';
3
+ import { DesignChoices, DesignMode, FlavorPackV2, FontRoleToken, SPEC_FLAVORS } from './design-token.model';
4
+ import type { FlavorRevertSnapshot } from './flavor-seed';
5
+
6
+ /**
7
+ * The `settings.design` row (design-system plan §3f), read and written by ONE pair of functions so
8
+ * the builder, the server's public page read (the engine's mode) and the site build agree on what a
9
+ * stored row MEANS. The row is the settings collection's generic `config: [{ key, value }]`; values
10
+ * are strings there (Boolean "true"/"false", Object as JSON), but a reader also accepts the typed
11
+ * value a client control may have sent.
12
+ */
13
+
14
+ /** The look a studio wears until it picks one (DS2's interim seed, and DS6's first-run selection). */
15
+ export const DEFAULT_FLAVOR_ID = 'crisp';
16
+
17
+ /** One `config` entry of a settings row. */
18
+ export interface DesignConfigEntry {
19
+ key: string;
20
+ value: unknown;
21
+ }
22
+
23
+ export enum DesignConfigKey {
24
+ FlavorId = 'flavorId',
25
+ FlavorVersion = 'flavorVersion',
26
+ BrandSeed = 'brandSeed',
27
+ PairId = 'pairId',
28
+ Shape = 'shape',
29
+ Density = 'density',
30
+ Motion = 'motion',
31
+ PillButtons = 'pillButtons',
32
+ Mode = 'mode',
33
+ Fonts = 'fonts',
34
+ Revert = 'revert',
35
+ }
36
+
37
+ /** A stored design, resolved: `choices` is ALWAYS complete and valid (pack defaults fill the gaps). */
38
+ export interface StoredDesign {
39
+ choices: DesignChoices;
40
+ /** The flavor id exactly as stored — `swiss` until the DS4 migration rewrites it; null when no row. */
41
+ storedFlavorId: string | null;
42
+ /** The seed exactly as stored (F2 orgs carry their brand colour here); null when absent. */
43
+ storedSeed: string | null;
44
+ flavorVersion: string | null;
45
+ revert: FlavorRevertSnapshot | null;
46
+ /** False when the org has never stored a design — DS6's first run. */
47
+ hasRow: boolean;
48
+ }
49
+
50
+ const MODES: ReadonlyArray<string> = SPEC_AXES.modes;
51
+ const FONT_ROLE_TOKENS: ReadonlyArray<string> = SPEC_FONT_ROLES.map(r => r.token);
52
+
53
+ /** The pack for an id; a retired id (Swiss) resolves to the pack that `replaces` it. */
54
+ export function flavorPackById(id: string | null | undefined): FlavorPackV2 | undefined {
55
+ if (!id) return undefined;
56
+ return SPEC_FLAVORS.find(f => f.id === id) ?? SPEC_FLAVORS.find(f => f.replaces === id);
57
+ }
58
+
59
+ /** True when `id` names a retired flavor that a v2 pack replaces (Swiss → Crisp). */
60
+ export function isRetiredFlavorId(id: string | null | undefined): boolean {
61
+ return !!id && !SPEC_FLAVORS.some(f => f.id === id) && SPEC_FLAVORS.some(f => f.replaces === id);
62
+ }
63
+
64
+ const defaultPack = (): FlavorPackV2 => flavorPackById(DEFAULT_FLAVOR_ID) as FlavorPackV2;
65
+
66
+ const asString = (v: unknown): string | null => (typeof v === 'string' && v.trim() !== '' ? v.trim() : null);
67
+
68
+ function asBoolean(v: unknown): boolean | null {
69
+ if (v === true || v === 'true') return true;
70
+ if (v === false || v === 'false') return false;
71
+ return null;
72
+ }
73
+
74
+ function asObject<T>(v: unknown): T | null {
75
+ if (v && typeof v === 'object') return v as T;
76
+ if (typeof v !== 'string' || v.trim() === '') return null;
77
+ try {
78
+ const parsed = JSON.parse(v);
79
+ return parsed && typeof parsed === 'object' ? parsed as T : null;
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ function asFonts(v: unknown): DesignChoices['fonts'] | undefined {
86
+ const raw = asObject<Record<string, unknown>>(v);
87
+ if (!raw) return undefined;
88
+ const fonts: Partial<Record<FontRoleToken, string>> = {};
89
+ for (const role of FONT_ROLE_TOKENS) {
90
+ const family = raw[role];
91
+ if (typeof family === 'string' && family.trim() !== '') fonts[role as FontRoleToken] = family.trim();
92
+ }
93
+ return Object.keys(fonts).length ? fonts : undefined;
94
+ }
95
+
96
+ const keyOf = <K extends string>(table: Readonly<Record<K, unknown>>, v: string | null): K | null =>
97
+ v !== null && Object.prototype.hasOwnProperty.call(table, v) ? v as K : null;
98
+
99
+ /** A config array → the key/value map (last entry wins, as the settings form writes it). */
100
+ function configMap(config: ReadonlyArray<DesignConfigEntry> | null | undefined): Map<string, unknown> {
101
+ const map = new Map<string, unknown>();
102
+ for (const kv of config ?? []) if (kv && typeof kv.key === 'string') map.set(kv.key, kv.value);
103
+ return map;
104
+ }
105
+
106
+ /**
107
+ * A partial set of choices completed against its pack: every missing or invalid value falls back to
108
+ * the pack's own default (an unknown pair → the pack's first, a bad seed → the pack's seed).
109
+ */
110
+ export function resolveDesignChoices(partial: Partial<DesignChoices> | null | undefined): DesignChoices {
111
+ const pack = flavorPackById(partial?.flavorId) ?? defaultPack();
112
+ const seed = asString(partial?.seed);
113
+ const pairId = asString(partial?.pairId);
114
+ const fonts = asFonts(partial?.fonts);
115
+ return {
116
+ flavorId: pack.id,
117
+ seed: seed && parseHexColor(seed) ? seed : pack.seed,
118
+ pairId: pairId && pack.pairs.some(p => p.id === pairId) ? pairId : pack.pairs[0]?.id ?? '',
119
+ shape: keyOf(SPEC_AXES.shapes, asString(partial?.shape)) ?? pack.defaults.shape,
120
+ density: keyOf(SPEC_AXES.densities, asString(partial?.density)) ?? pack.defaults.density,
121
+ motion: keyOf(SPEC_AXES.motions, asString(partial?.motion)) ?? pack.defaults.motion,
122
+ pillButtons: asBoolean(partial?.pillButtons) ?? pack.defaults.pillButtons,
123
+ mode: (MODES.includes(asString(partial?.mode) ?? '') ? asString(partial?.mode) : pack.defaults.mode) as DesignMode,
124
+ ...(fonts ? { fonts } : {}),
125
+ };
126
+ }
127
+
128
+ /** The pack's own defaults for `flavorId` — what picking a card means before anything is tuned. */
129
+ export function packDesignChoices(flavorId: string): DesignChoices {
130
+ return resolveDesignChoices({ flavorId });
131
+ }
132
+
133
+ /** A stored `settings.design` config → the resolved design. No row (or no config) → Crisp's defaults. */
134
+ export function readDesignConfig(config: ReadonlyArray<DesignConfigEntry> | null | undefined, hasRow = !!config): StoredDesign {
135
+ const map = configMap(config);
136
+ const storedFlavorId = asString(map.get(DesignConfigKey.FlavorId));
137
+ const storedSeed = asString(map.get(DesignConfigKey.BrandSeed));
138
+ const choices = resolveDesignChoices({
139
+ flavorId: storedFlavorId ?? undefined,
140
+ seed: storedSeed ?? undefined,
141
+ pairId: asString(map.get(DesignConfigKey.PairId)) ?? undefined,
142
+ shape: map.get(DesignConfigKey.Shape) as DesignChoices['shape'],
143
+ density: map.get(DesignConfigKey.Density) as DesignChoices['density'],
144
+ motion: map.get(DesignConfigKey.Motion) as DesignChoices['motion'],
145
+ pillButtons: asBoolean(map.get(DesignConfigKey.PillButtons)) ?? undefined,
146
+ mode: map.get(DesignConfigKey.Mode) as DesignMode,
147
+ fonts: map.get(DesignConfigKey.Fonts) as DesignChoices['fonts'],
148
+ });
149
+ return {
150
+ choices,
151
+ storedFlavorId,
152
+ storedSeed,
153
+ flavorVersion: asString(map.get(DesignConfigKey.FlavorVersion)),
154
+ revert: asObject<FlavorRevertSnapshot>(map.get(DesignConfigKey.Revert)),
155
+ hasRow,
156
+ };
157
+ }
158
+
159
+ /** The colour mode a stored config compiles in — what the public page read hands the engine. */
160
+ export function designModeOf(config: ReadonlyArray<DesignConfigEntry> | null | undefined): DesignMode {
161
+ return readDesignConfig(config).choices.mode;
162
+ }
163
+
164
+ /** Resolved choices (+ the revert snapshot) → the config array a settings row stores. */
165
+ export function designConfigEntries(
166
+ choices: DesignChoices,
167
+ flavorVersion: string | number,
168
+ revert?: FlavorRevertSnapshot | null,
169
+ ): DesignConfigEntry[] {
170
+ const c = resolveDesignChoices(choices);
171
+ return [
172
+ { key: DesignConfigKey.FlavorId, value: c.flavorId },
173
+ { key: DesignConfigKey.FlavorVersion, value: String(flavorVersion) },
174
+ { key: DesignConfigKey.BrandSeed, value: c.seed },
175
+ { key: DesignConfigKey.PairId, value: c.pairId },
176
+ { key: DesignConfigKey.Shape, value: c.shape },
177
+ { key: DesignConfigKey.Density, value: c.density },
178
+ { key: DesignConfigKey.Motion, value: c.motion },
179
+ { key: DesignConfigKey.PillButtons, value: String(c.pillButtons) },
180
+ { key: DesignConfigKey.Mode, value: c.mode },
181
+ ...(c.fonts ? [{ key: DesignConfigKey.Fonts, value: JSON.stringify(c.fonts) }] : []),
182
+ ...(revert ? [{ key: DesignConfigKey.Revert, value: JSON.stringify(revert) }] : []),
183
+ ];
184
+ }
185
+
186
+ /** Two choice sets describe the same look (fonts compared by role). */
187
+ export function sameDesignChoices(a: DesignChoices, b: DesignChoices): boolean {
188
+ const x = resolveDesignChoices(a);
189
+ const y = resolveDesignChoices(b);
190
+ return x.flavorId === y.flavorId && x.seed.toLowerCase() === y.seed.toLowerCase() && x.pairId === y.pairId
191
+ && x.shape === y.shape && x.density === y.density && x.motion === y.motion
192
+ && x.pillButtons === y.pillButtons && x.mode === y.mode
193
+ && JSON.stringify(x.fonts ?? {}) === JSON.stringify(y.fonts ?? {});
194
+ }
@@ -272,10 +272,10 @@ export function switchElementStyle(
272
272
 
273
273
  /**
274
274
  * The element-style MARKER (design-system §3b-bis, spec `elementStyleMarker`). A node that wears an
275
- * element style renders `iox-es iox-es-<id>` in BOTH renderers; every piece of component CSS that
276
- * carries the system look is scoped to it, so a node saved before DS2 (no `elementStyle`) keeps its
277
- * exact pre-DS2 component CSS no matter which tokens the org holds. This is the ONLY thing a
278
- * renderer reads `elementStyle` for.
275
+ * element style renders `iox-es iox-es-<id>` in BOTH renderers, through the one node-class helper
276
+ * `nodeDesignClasses` (node-classes.ts); every piece of component CSS that carries the system look is
277
+ * scoped to it, so a node saved before DS2 (no `elementStyle`) keeps its exact pre-DS2 component CSS
278
+ * no matter which tokens the org holds.
279
279
  */
280
280
  export const ELEMENT_STYLE_MARKER_CLASS = 'iox-es';
281
281
 
@@ -286,22 +286,6 @@ export function elementStyleClass(id: string | null | undefined): string | null
286
286
  return elementStyleById(id) ? `${MARKER_PREFIX}${id}` : null;
287
287
  }
288
288
 
289
- /** The marker classes a node renders: `'iox-es iox-es-<id>'`, or `''` for a node with no (known) style. */
290
- export function elementStyleClasses(node: { elementStyle?: string | null } | null | undefined): string {
291
- const own = elementStyleClass(node?.elementStyle);
292
- return own ? `${ELEMENT_STYLE_MARKER_CLASS} ${own}` : '';
293
- }
294
-
295
- /** Make an element's marker match `elementStyle` — the live switch on the canvas, and the
296
- * component path's one-time stamp. Removes a stale `iox-es-<old>`; touches no other class. */
297
- export function syncElementStyleMarker(el: { classList: DOMTokenList } | null | undefined, elementStyle: string | null | undefined): void {
298
- if (!el?.classList) return;
299
- const stale = Array.from(el.classList).filter(c => c === ELEMENT_STYLE_MARKER_CLASS || c.startsWith(MARKER_PREFIX));
300
- for (const c of stale) el.classList.remove(c);
301
- const classes = elementStyleClasses({ elementStyle });
302
- if (classes) el.classList.add(...classes.split(' '));
303
- }
304
-
305
289
  /**
306
290
  * The spec's `headingLevelRule`: changing a Heading's level moves it to the new level's DEFAULT
307
291
  * style only when it still wears its old level's default (author changes kept, via
@@ -0,0 +1,52 @@
1
+ import { hasElementStyleMarker } from './node-classes';
2
+
3
+ /**
4
+ * Empty images (design-system DS3). A MARKED Image (`.iox-es-media`) with no source renders a box in
5
+ * the `surface-2` token with its aspect ratio kept — on the canvas with an "Add image" label, on the
6
+ * live site with nothing in it. An UNMARKED empty Image renders exactly as before (no `src`).
7
+ *
8
+ * Why a placeholder `src` instead of a src-less `<img>`: Chromium paints a src-less image as a broken
9
+ * image (a frame, an icon and the alt text) and ignores `aspect-ratio` once alt text is present, and no
10
+ * pseudo-element paints on an `<img>`. A real image with no natural size of its own (an SVG with no
11
+ * width/height) is a proper replaced element: `aspect-ratio`/`object-fit` apply, the background shows
12
+ * through, and its default object size (300×150) is the SAME for the canvas label and the live blank,
13
+ * so both renderers lay the box out identically. The fill itself is shared CSS (`iox-design.css`)
14
+ * keyed on {@link EMPTY_IMAGE_ATTRIBUTE}.
15
+ */
16
+
17
+ /** Set on a marked Image while it has no source — the hook the shared fill rule keys on. */
18
+ export const EMPTY_IMAGE_ATTRIBUTE = 'data-iox-empty';
19
+
20
+ const svgUri = (svg: string) => `data:image/svg+xml,${encodeURIComponent(svg)}`;
21
+
22
+ /** The live site's blank: an SVG with no intrinsic size and nothing in it. */
23
+ export const EMPTY_IMAGE_SRC = svgUri('<svg xmlns="http://www.w3.org/2000/svg"/>');
24
+
25
+ const xmlEscape = (s: string) => s.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
26
+
27
+ /**
28
+ * The canvas blank: the same no-size SVG with `label` centred as a chip. The chip carries its own
29
+ * light ground, so it reads on every flavor's `surface-2`, light or dark; it is builder chrome, never
30
+ * site content. Width is estimated from the label length (an image cannot measure text).
31
+ */
32
+ export function emptyImageLabelSrc(label: string): string {
33
+ const text = xmlEscape(label);
34
+ const width = Math.round(28 + label.length * 7);
35
+ return svgUri(
36
+ '<svg xmlns="http://www.w3.org/2000/svg"><svg x="50%" y="50%" overflow="visible">'
37
+ + `<rect x="${-width / 2}" y="-13" width="${width}" height="26" rx="13" fill="#ffffff" fill-opacity="0.92"/>`
38
+ + '<text x="0" y="0" text-anchor="middle" dominant-baseline="central" font-family="system-ui, -apple-system, \'Segoe UI\', sans-serif" '
39
+ + `font-size="12" font-weight="500" fill="#1f1d1b">${text}</text></svg></svg>`,
40
+ );
41
+ }
42
+
43
+ /**
44
+ * The `src` a node's Image renders when its own source is empty: the canvas label (when `label` is
45
+ * given) or the live blank for a MARKED node; `null` — render exactly as before — for an unmarked
46
+ * node or one that has a source.
47
+ */
48
+ export function emptyImageSrc(node: { elementStyle?: string | null } | null | undefined, src: unknown, label?: string | null): string | null {
49
+ if (src != null && src !== '') return null;
50
+ if (!hasElementStyleMarker(node)) return null;
51
+ return label ? emptyImageLabelSrc(label) : EMPTY_IMAGE_SRC;
52
+ }
@@ -1,5 +1,5 @@
1
1
  import { FlavorTokenDef } from './flavor';
2
- import { DesignTokenRow, StoredDesignTokenRow } from './design-token.model';
2
+ import { DesignTokenRow, StoredDesignTokenRow, USER_TOKEN_SOURCE } from './design-token.model';
3
3
 
4
4
  /**
5
5
  * Seeding and reverting a look, as pure plans (flavors F2, moved here from iox-cms-client in
@@ -82,42 +82,68 @@ export function planFlavorRevert(
82
82
  return plan;
83
83
  }
84
84
 
85
+ /** What a previous seed wrote for one row — the "untouched" comparison base. */
86
+ export interface SeedExpectation {
87
+ value: string;
88
+ dark?: string;
89
+ }
90
+
91
+ /**
92
+ * What the PREVIOUS seed wrote, per row: a map by name, or a function of the stored row (a baseline
93
+ * that depends on WHICH seed stamped the row — the Swiss → Crisp migration's case).
94
+ */
95
+ export type SeedBaseline =
96
+ | ReadonlyMap<string, SeedExpectation>
97
+ | ((row: StoredDesignTokenRow) => SeedExpectation | undefined);
98
+
99
+ const expectationFor = (baseline: SeedBaseline | null | undefined, row: StoredDesignTokenRow): SeedExpectation | undefined =>
100
+ !baseline ? undefined : typeof baseline === 'function' ? baseline(row) : baseline.get(row.name);
101
+
102
+ const sameValue = (row: { value: string; values?: { dark?: string } }, e: SeedExpectation) =>
103
+ row.value === e.value && (row.values?.dark ?? '') === (e.dark ?? '');
104
+
85
105
  /**
86
106
  * The seeding MERGE: desired tokens vs existing rows, deciding create / update / keep. The §10 C2
87
107
  * rule — "an upgrade only rewrites untouched rows" — is a comparison against what the PREVIOUS
88
- * seed wrote (`oldExpected`): a seeded row whose value still equals it is the machine's and may be
89
- * rewritten; any drift means the author took it over, and their hand wins. A row without a
90
- * `flavor:` stamp is never touched at all. `group`/`tier` ride every write.
108
+ * seed wrote (`baseline`): a seeded row whose value still equals it is the machine's and may be
109
+ * rewritten; any drift means the author took it over, and their hand wins. A row the design did not
110
+ * write is never touched: no `flavor:` stamp, unless it is a `user` row (a font the author CHOSE in
111
+ * the design) whose value is exactly what the previous design wrote — that row is the design's own
112
+ * choice, so a new choice may replace it. `group`/`tier` ride every write; a desired row's own
113
+ * `source` (a chosen font's `user`) wins over the pack stamp.
91
114
  */
92
115
  export function planFlavorSeed(
93
- desired: ReadonlyArray<FlavorTokenDef>,
116
+ desired: ReadonlyArray<FlavorTokenDef & { source?: string }>,
94
117
  existing: ReadonlyArray<StoredDesignTokenRow>,
95
118
  stamp: string,
96
- oldExpected: Map<string, { value: string; dark?: string }> | null,
119
+ baseline: SeedBaseline | null,
97
120
  ): FlavorSeedPlan {
98
121
  const byName = new Map((existing ?? []).map(r => [r.name, r]));
99
122
  const plan: FlavorSeedPlan = { create: [], update: [], kept: [] };
100
123
  for (const def of desired ?? []) {
101
124
  if (!def?.name) continue;
102
125
  const row = byName.get(def.name);
126
+ const source = def.source ?? stamp;
103
127
  if (!row) {
104
128
  plan.create.push({
105
129
  name: def.name,
106
130
  type: def.type ?? DEFAULT_TOKEN_TYPE,
107
131
  value: def.value,
108
132
  ...(def.values ? { values: def.values } : {}),
109
- source: stamp,
133
+ source,
110
134
  ...classification(def),
111
135
  });
112
136
  continue;
113
137
  }
114
- if (!isSeeded(row)) { plan.kept.push(def.name); continue; }
138
+ const old = expectationFor(baseline, row);
139
+ const designOwned = isSeeded(row) || (row.source === USER_TOKEN_SOURCE && !!old && sameValue(row, old));
140
+ if (!designOwned) { plan.kept.push(def.name); continue; }
115
141
  const sameAsDesired = row.value === def.value && (row.values?.dark ?? '') === (def.values?.dark ?? '');
116
- if (sameAsDesired) continue; // already exactly this — nothing to write, nothing kept-against
117
- const old = oldExpected?.get(def.name);
118
- const untouched = !!old && row.value === old.value && (row.values?.dark ?? '') === (old.dark ?? '');
119
- if (untouched && row._id) {
120
- plan.update.push({ id: row._id, data: { value: def.value, values: def.values ?? {}, source: stamp, ...classification(def) } });
142
+ if (sameAsDesired && (row.source ?? '') === source) continue; // already exactly this
143
+ if (sameAsDesired && isSeeded(row) && source !== USER_TOKEN_SOURCE) continue; // same value, another pack's stamp — nothing to write
144
+ const untouched = !!old && sameValue(row, old);
145
+ if ((untouched || sameAsDesired) && row._id) {
146
+ plan.update.push({ id: row._id, data: { value: def.value, values: def.values ?? {}, source, ...classification(def) } });
121
147
  } else {
122
148
  plan.kept.push(def.name); // edited since seeding (or unverifiable) — the author's hand wins
123
149
  }
@@ -1077,7 +1077,7 @@ export const SPEC_SPRINGS = {
1077
1077
  } as const;
1078
1078
 
1079
1079
  export const SPEC_FONTS = {
1080
- "Inter": "Inter:wght@400;500;600;700",
1080
+ "Inter": "Inter:wght@300;400;500;600;700",
1081
1081
  "Inter Tight": "Inter+Tight:wght@500;600;700",
1082
1082
  "Manrope": "Manrope:wght@400;500;600;700",
1083
1083
  "IBM Plex Sans": "IBM+Plex+Sans:wght@400;500;600;700",
@@ -1092,12 +1092,21 @@ export const SPEC_FONTS = {
1092
1092
  "Figtree": "Figtree:wght@400;500;600;700;800",
1093
1093
  "Google Sans Flex": "Google+Sans+Flex:wght@400;500;600;700",
1094
1094
  "Bricolage Grotesque": "Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700",
1095
- "Nunito": "Nunito:wght@400;600;700;800",
1095
+ "Nunito": "Nunito:wght@300;400;600;700;800",
1096
1096
  "DM Sans": "DM+Sans:wght@400;500;700",
1097
1097
  "Archivo": "Archivo:wght@400;500;700",
1098
1098
  "Archivo Black": "Archivo+Black",
1099
1099
  "Syne": "Syne:wght@600;700;800",
1100
- "Space Mono": "Space+Mono:wght@400;700"
1100
+ "Space Mono": "Space+Mono:wght@400;700",
1101
+ "Poppins": "Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400",
1102
+ "Roboto": "Roboto:ital,wght@0,300;0,400;0,500;0,700;1,400",
1103
+ "Lato": "Lato:ital,wght@0,300;0,400;0,700;1,400",
1104
+ "Open Sans": "Open+Sans:ital,wght@0,300;0,400;0,600;0,700;1,400",
1105
+ "Montserrat": "Montserrat:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400",
1106
+ "Raleway": "Raleway:wght@300;400;500;600;700",
1107
+ "Playfair Display": "Playfair+Display:ital,wght@0,400;0,500;0,700;1,400",
1108
+ "Oswald": "Oswald:wght@300;400;500;600;700",
1109
+ "Ubuntu": "Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,400"
1101
1110
  } as const;
1102
1111
 
1103
1112
  export const SPEC_SCHEMES = {
@@ -1762,7 +1771,6 @@ export const SPEC_FIELD_CLASS_BASELINE = {
1762
1771
  export const SPEC_LEGACY_ALIASES = {
1763
1772
  "radius-control": "var(--iox-button-radius)",
1764
1773
  "radius-surface": "var(--iox-card-radius)",
1765
- "space-control": "var(--iox-button-pad)",
1766
1774
  "space-stack": "var(--iox-space-gap)"
1767
1775
  } as const;
1768
1776
 
@@ -4,8 +4,9 @@ import { DesignTokenGroup, DesignTokenTier, DesignTokenType } from './design-tok
4
4
  * Flavors v1 (flavors-and-theming-plan.md, F1–F2): a pack is its TOKENS plus the accent hue
5
5
  * opinion the brand derivation reads. The per-type `defaults` map and its resolvers are gone
6
6
  * (design-system plan §3e, DS2): a new element is born wearing an ELEMENT STYLE
7
- * (`element-styles.ts`), which references tokens and is flavor-independent. Swiss survives only
8
- * until DS4 migrates orgs to Crisp.
7
+ * (`element-styles.ts`), which references tokens and is flavor-independent. Swiss is retired (DS4
8
+ * migrates `flavorId: swiss` orgs to Crisp); its pack stays as the migration's BASELINE — what F1/F2
9
+ * wrote, so "untouched since seeding" stays a comparison (`design-apply.ts`).
9
10
  */
10
11
 
11
12
  export interface FlavorTokenDef {
@@ -40,9 +41,9 @@ export function validateFlavorPack(pack: FlavorPack): string[] {
40
41
  }
41
42
 
42
43
  /**
43
- * Swiss — the F1 pack: four tokens (control/surface radius, control/stack spacing) and the hue
44
- * opinion F2's brand step reads. Kept until DS4 migrates `flavorId: swiss` orgs to Crisp; its four
45
- * rows are the spec's `legacyAliases`.
44
+ * Swiss — the retired F1 pack: four tokens (control/surface radius, control/stack spacing) and the
45
+ * hue opinion F2's brand step read. Three of its rows become the spec's `legacyAliases` on migration;
46
+ * `space-control` is a v2 token in every pack and takes Crisp's value.
46
47
  */
47
48
  export const SWISS_FLAVOR: FlavorPack = {
48
49
  id: 'swiss',