@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/design-core/index.d.ts +266 -33
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +494 -77
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +39 -6
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/index.d.ts +25 -4
- package/package.json +1 -1
- package/src/design-core/design-apply.ts +116 -0
- package/src/design-core/design-settings.ts +194 -0
- package/src/design-core/element-styles.ts +4 -20
- package/src/design-core/empty-image.ts +52 -0
- package/src/design-core/flavor-seed.ts +39 -13
- package/src/design-core/flavor-spec.generated.ts +12 -4
- package/src/design-core/flavor.ts +6 -5
- package/src/design-core/fonts.ts +45 -8
- package/src/design-core/node-classes.ts +82 -0
- package/src/design-core/public-api.ts +4 -0
- package/src/design-core/token-css.ts +32 -2
- package/src/design-core/token-names.ts +21 -0
- package/styles/iox-design.css +30 -0
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
|
@@ -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
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
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 (`
|
|
89
|
-
* rewritten; any drift means the author took it over, and their hand wins. A row
|
|
90
|
-
* `flavor:` stamp
|
|
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
|
-
|
|
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
|
|
133
|
+
source,
|
|
110
134
|
...classification(def),
|
|
111
135
|
});
|
|
112
136
|
continue;
|
|
113
137
|
}
|
|
114
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
const untouched = !!old &&
|
|
119
|
-
if (untouched && row._id) {
|
|
120
|
-
plan.update.push({ id: row._id, data: { value: def.value, values: def.values ?? {}, source
|
|
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
|
|
8
|
-
*
|
|
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
|
|
44
|
-
* opinion F2's brand step
|
|
45
|
-
*
|
|
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',
|