@vectoriox/iox-ui 5.13.1 → 5.14.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.14.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -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
+ }
@@ -0,0 +1,82 @@
1
+ import { ELEMENT_STYLE_MARKER_CLASS, elementStyleClass } from './element-styles';
2
+ import { SPEC_SCHEMES } from './flavor-spec.generated';
3
+
4
+ /**
5
+ * The design-system classes a node renders, in BOTH renderers (builder canvas, engine component
6
+ * path, engine string path) — ONE helper, so the three can never disagree about which class a node
7
+ * carries:
8
+ *
9
+ * - the element-style MARKER `iox-es iox-es-<id>` (design-system §3b-bis) — component CSS carrying
10
+ * the system look is scoped to it, so a node saved before DS2 keeps its pre-DS2 look;
11
+ * - the section colour scheme `iox-scheme-<id>` (§3c) for a non-default `colorScheme` — the class
12
+ * `compileSchemeCss` re-declares tokens on, so everything inside re-resolves.
13
+ *
14
+ * Renderers read `elementStyle` and `colorScheme` for exactly this and nothing else.
15
+ */
16
+
17
+ /** The section colour schemes — the contract's `schemes` keys. */
18
+ export enum ColorScheme {
19
+ Default = 'default',
20
+ Subtle = 'subtle',
21
+ Inverse = 'inverse',
22
+ Accent = 'accent',
23
+ }
24
+
25
+ /** The content trait (stored in `props`, not a style prop) that picks a node's scheme. */
26
+ export const COLOR_SCHEME_TRAIT = 'colorScheme';
27
+
28
+ /** The node types that offer a scheme (spec §3c: Section/Container; LinkedContainer is a Container that links). */
29
+ export const COLOR_SCHEME_TYPES: ReadonlyArray<string> = ['Section', 'Container', 'LinkedContainer'];
30
+
31
+ /** Every scheme in contract order, with the contract's label (Default · Subtle · Inverse · Brand). */
32
+ export const COLOR_SCHEMES: ReadonlyArray<{ readonly id: ColorScheme; readonly label: string }> =
33
+ (Object.keys(SPEC_SCHEMES) as ColorScheme[]).map(id => ({ id, label: (SPEC_SCHEMES as Record<string, { label: string }>)[id].label }));
34
+
35
+ const SCHEME_IDS = new Set<string>(Object.keys(SPEC_SCHEMES));
36
+ const SCHEME_PREFIX = 'iox-scheme-';
37
+ const MARKER_PREFIX = `${ELEMENT_STYLE_MARKER_CLASS}-`;
38
+
39
+ /** `iox-scheme-<id>` for a known NON-default scheme, else null — an unknown id never reaches a class attribute. */
40
+ export function colorSchemeClass(id: unknown): string | null {
41
+ return typeof id === 'string' && id !== ColorScheme.Default && SCHEME_IDS.has(id) ? `${SCHEME_PREFIX}${id}` : null;
42
+ }
43
+
44
+ /** The node shapes this module reads: a serialized node (`props`) or a builder node (`traits[]`). */
45
+ export interface DesignClassSource {
46
+ elementStyle?: string | null;
47
+ props?: Readonly<Record<string, unknown>> | null;
48
+ traits?: ReadonlyArray<{ name?: string; default?: unknown }> | null;
49
+ }
50
+
51
+ /** A node's `colorScheme`, from `props` (serialized) or its trait (builder). */
52
+ export function colorSchemeOf(node: DesignClassSource | null | undefined): string | null {
53
+ if (!node) return null;
54
+ const fromProps = node.props?.[COLOR_SCHEME_TRAIT];
55
+ if (fromProps !== undefined) return typeof fromProps === 'string' ? fromProps : null;
56
+ const trait = node.traits?.find(t => t?.name === COLOR_SCHEME_TRAIT)?.default;
57
+ return typeof trait === 'string' ? trait : null;
58
+ }
59
+
60
+ /** True when the node wears a known element style — the marker is on it. */
61
+ export function hasElementStyleMarker(node: { elementStyle?: string | null } | null | undefined): boolean {
62
+ return elementStyleClass(node?.elementStyle) !== null;
63
+ }
64
+
65
+ /** The classes a node renders: `'iox-es iox-es-card iox-scheme-inverse'`, or `''` for none. */
66
+ export function nodeDesignClasses(node: DesignClassSource | null | undefined): string {
67
+ const marker = elementStyleClass(node?.elementStyle);
68
+ return [marker ? `${ELEMENT_STYLE_MARKER_CLASS} ${marker}` : '', colorSchemeClass(colorSchemeOf(node)) ?? '']
69
+ .filter(Boolean).join(' ');
70
+ }
71
+
72
+ const isDesignClass = (c: string) =>
73
+ c === ELEMENT_STYLE_MARKER_CLASS || c.startsWith(MARKER_PREFIX) || c.startsWith(SCHEME_PREFIX);
74
+
75
+ /** Make an element's design classes match the node — the live switch on the canvas and the
76
+ * component path's one-time stamp. Removes stale ones; touches no other class. */
77
+ export function syncNodeDesignClasses(el: { classList: DOMTokenList } | null | undefined, node: DesignClassSource | null | undefined): void {
78
+ if (!el?.classList) return;
79
+ for (const c of Array.from(el.classList).filter(isDesignClass)) el.classList.remove(c);
80
+ const classes = nodeDesignClasses(node);
81
+ if (classes) el.classList.add(...classes.split(' '));
82
+ }
@@ -14,6 +14,8 @@ export * from './css-names';
14
14
  export * from './token-css';
15
15
  export * from './flavor';
16
16
  export * from './element-styles';
17
+ export * from './node-classes';
18
+ export * from './empty-image';
17
19
  export * from './brand';
18
20
  export * from './flavor-tokens';
19
21
  export * from './flavor-seed';
@@ -0,0 +1,30 @@
1
+ /* Design-system component CSS that EVERY renderer needs — the ONE copy.
2
+ *
3
+ * Same reason as iox-forms.css: the SSR engine's string renderer emits plain markup, the engine's
4
+ * component path renders the shared components, and the builder canvas renders its own twins, so a
5
+ * component-scoped copy in each would be three copies that drift. Consumers add this file to their
6
+ * `angular.json` styles.
7
+ *
8
+ * Everything here is scoped to the element-style marker (design-system §3b-bis): a node saved
9
+ * before element styles never matches, so it renders exactly as before.
10
+ */
11
+
12
+ /* Empty images (DS3). A marked Image with no source carries `data-iox-empty` and a size-less
13
+ placeholder src (design-core `emptyImageSrc`), so it stays a real replaced element: its own
14
+ aspect-ratio and object-fit apply and this fill shows through — on the canvas behind the
15
+ "Add image" label, on the live site on its own. Out-specifies the node rule (0,2,1 vs 0,1,0)
16
+ only while the image is empty. */
17
+ img.iox-es-media[data-iox-empty] {
18
+ background-color: var(--iox-surface-2);
19
+ }
20
+
21
+ /* An empty marked Gallery takes the same fill, on the element that carries its node rule (size,
22
+ radius): the string renderer's one div, or the component's inner `.iox-gallery` under the
23
+ `<iox-gallery>` host that wears the marker. Its cinematic overlay has nothing to darken. */
24
+ .iox-gallery.iox-es-media[data-iox-empty],
25
+ iox-gallery.iox-es-media[data-iox-empty] > .iox-gallery {
26
+ background-color: var(--iox-surface-2);
27
+ }
28
+ .iox-es-media[data-iox-empty] .iox-gallery__overlay {
29
+ display: none;
30
+ }