@vectoriox/iox-ui 5.5.2 → 5.7.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
@@ -1490,6 +1490,115 @@ declare const PART_CLASS_NAMES: Record<string, Record<string, string>>;
1490
1490
  * Undefined when the type declares no such part (or no parts at all). */
1491
1491
  declare function partClassName(type: string, partKey: string): string | undefined;
1492
1492
 
1493
+ /**
1494
+ * Flavors (flavors-and-theming-plan.md, F1): the design language a studio launches with,
1495
+ * expressed as data. THE invariant — a flavor never writes a literal into a node; it writes
1496
+ * tokens, and element defaults that REFERENCE tokens. To that end a pack's `defaults` map
1497
+ * holds TOKEN NAMES, not CSS values: the resolver builds `var(--iox-<name>, <packValue>)`,
1498
+ * so smuggling a literal in is impossible by construction, and the validator only has to
1499
+ * check that every referenced token exists.
1500
+ *
1501
+ * The fallback is the bootstrap: until F2 seeds the org's `designtokens` rows, no `--iox-*`
1502
+ * var is defined anywhere, and `var(--iox-radius-control, 4px)` must still paint identically
1503
+ * in the builder canvas and the SSR page. Once tokens ARE seeded (or re-themed), the var wins
1504
+ * over the fallback everywhere — which is exactly the §3 propagation story.
1505
+ */
1506
+ interface FlavorTokenDef {
1507
+ name: string;
1508
+ value: string;
1509
+ /** Dark-scheme override, seeded into DesignToken.values.dark at F2. */
1510
+ values?: {
1511
+ dark?: string;
1512
+ };
1513
+ type?: 'color' | 'spacing' | 'size' | 'font' | 'other';
1514
+ /** Tokens-panel grouping (flavors plan §10 C5). */
1515
+ group?: 'colour' | 'type' | 'space' | 'radius' | 'shadow' | 'motion';
1516
+ }
1517
+ interface FlavorPack {
1518
+ id: string;
1519
+ /** Bumped when the pack's values change; seeding stamps `flavor:<id>@<version>` as DesignToken.source. */
1520
+ version: number;
1521
+ label: string;
1522
+ blurb: string;
1523
+ tokens: FlavorTokenDef[];
1524
+ /** type → styleTrait → TOKEN NAME (never a CSS value — see the module header). */
1525
+ defaults: Record<string, Record<string, string>>;
1526
+ /** F2: the accent's hue rotation (degrees in OKLab) from the brand seed — a pack opinion. */
1527
+ accentHueDeg?: number;
1528
+ }
1529
+ /** trait → `var(--iox-<token>, <packValue>)` for one node type. Unknown type → {}. */
1530
+ declare function resolveFlavorDefaults(type: string, pack: FlavorPack | null | undefined): Record<string, string>;
1531
+ /**
1532
+ * The ENGINE's half of the cascade: kebab `prop: var(…)` declarations for every pack default
1533
+ * the node's persisted styleProps does NOT set. Appended beside renderDefaultDeclarations —
1534
+ * same guard-on-undefined discipline, so a node saved post-F1 (var already serialised via the
1535
+ * builder's trait defaults) is untouched, and a legacy node picks the pack default up at
1536
+ * render time. Node styleProps always wins.
1537
+ */
1538
+ declare function flavorDefaultDeclarations(type: string, styleProps: Record<string, any> | null | undefined, pack: FlavorPack | null | undefined): string[];
1539
+ /**
1540
+ * The CI gate (flavors plan §10 C4): a pack may only name types and traits that exist in the
1541
+ * live component registry, and tokens it defines itself. Returns human-readable errors; a
1542
+ * consumer spec feeds it the REAL registry and asserts [].
1543
+ */
1544
+ declare function validateFlavorPack(pack: FlavorPack, knownTraitsByType: Record<string, ReadonlySet<string> | string[]>): string[];
1545
+ /**
1546
+ * Swiss — the default pack (flavors plan §8: "the flavor that can never embarrass anyone").
1547
+ * Deliberately SMALL at F1: radius + control padding on the clickables, surface radius on Card.
1548
+ * Colour, type and motion arrive with the brand step (F2) — a colour default here would repaint
1549
+ * legacy pages before anyone chose anything.
1550
+ */
1551
+ declare const SWISS_FLAVOR: FlavorPack;
1552
+ /** What both renderers use until F2 lets an org choose. */
1553
+ declare const DEFAULT_FLAVOR: FlavorPack;
1554
+
1555
+ /**
1556
+ * BRAND DERIVATION (flavors plan §6, F2): one colour in, an asserted accessible ramp out.
1557
+ *
1558
+ * Competitors ask for a palette of five and hope; we ask for ONE seed and compute the rest so
1559
+ * that contrast is a TEST, not a promise. WCAG 2 ratios are the gate (every guarantee below is
1560
+ * unit-asserted across seeds); APCA is exposed as the picker's quality read-out only.
1561
+ *
1562
+ * Colour math is OKLab (perceptually even lightness — the ramp steps LOOK even), hand-rolled
1563
+ * from the published matrices because this module must stay dependency-free: it runs in the
1564
+ * builder, the engine and (later) the generator's Node service.
1565
+ */
1566
+ interface Rgb {
1567
+ r: number;
1568
+ g: number;
1569
+ b: number;
1570
+ }
1571
+ interface OkLab {
1572
+ L: number;
1573
+ a: number;
1574
+ b: number;
1575
+ }
1576
+ declare function parseHexColor(hex: string): Rgb | null;
1577
+ declare function toHexColor(c: Rgb): string;
1578
+ declare function rgbToOklab(c: Rgb): OkLab;
1579
+ declare function oklabToRgb(c: OkLab): Rgb;
1580
+ declare function relativeLuminance(c: Rgb): number;
1581
+ /** WCAG 2 contrast ratio between two colours (order-independent), 1..21. */
1582
+ declare function wcagContrastRatio(a: string | Rgb, b: string | Rgb): number;
1583
+ /**
1584
+ * APCA-W3 lightness contrast Lc (0.0.98G-4g constants), text-on-background and signed:
1585
+ * positive for dark-on-light, negative for light-on-dark; |Lc| ≈ 106–108 at the extremes.
1586
+ * Shown in the picker as the perceptual read-out; never used as the pass/fail gate here.
1587
+ */
1588
+ declare function apcaLc(text: string | Rgb, bg: string | Rgb): number;
1589
+ /**
1590
+ * The derived brand tokens (plan §6's table, every row asserted in brand.spec.ts):
1591
+ * `brand-50…900` (monotonic OKLab lightness, gamut-safe) · `brandInk` (black or white, ≥4.5 on
1592
+ * brand-500 — brand-500's lightness is nudged when neither side clears it) · `surface`/`ink`/
1593
+ * `inkMuted` (seed-tinted neutrals, ≥7:1 / ≥4.5:1 on surface) · `line` (the LIGHTEST neutral that
1594
+ * still clears 3:1 on surface) · `accent` (pack-rotated hue, ≥4.5:1 on surface) — with dark-scheme
1595
+ * values for the neutral family riding `values.dark`.
1596
+ *
1597
+ * Throws on an unparseable seed: callers (the picker) validate; a silent fallback here would
1598
+ * launch a studio into a palette nobody chose.
1599
+ */
1600
+ declare function deriveBrandTokens(seed: string, pack: FlavorPack): FlavorTokenDef[];
1601
+
1493
1602
  /** Trait names whose value Swiper reads per breakpoint. Anything else is config-wide. */
1494
1603
  declare const SWIPER_RESPONSIVE_TRAITS: readonly string[];
1495
1604
  /** `{ [minWidth]: { slidesPerView, … } }` — Swiper's own `breakpoints` shape. */
@@ -1553,66 +1662,6 @@ declare function compileTokenCss(tokens: ReadonlyArray<DesignTokenLike> | null |
1553
1662
  dark: string;
1554
1663
  };
1555
1664
 
1556
- /**
1557
- * Flavors (flavors-and-theming-plan.md, F1): the design language a studio launches with,
1558
- * expressed as data. THE invariant — a flavor never writes a literal into a node; it writes
1559
- * tokens, and element defaults that REFERENCE tokens. To that end a pack's `defaults` map
1560
- * holds TOKEN NAMES, not CSS values: the resolver builds `var(--iox-<name>, <packValue>)`,
1561
- * so smuggling a literal in is impossible by construction, and the validator only has to
1562
- * check that every referenced token exists.
1563
- *
1564
- * The fallback is the bootstrap: until F2 seeds the org's `designtokens` rows, no `--iox-*`
1565
- * var is defined anywhere, and `var(--iox-radius-control, 4px)` must still paint identically
1566
- * in the builder canvas and the SSR page. Once tokens ARE seeded (or re-themed), the var wins
1567
- * over the fallback everywhere — which is exactly the §3 propagation story.
1568
- */
1569
- interface FlavorTokenDef {
1570
- name: string;
1571
- value: string;
1572
- /** Dark-scheme override, seeded into DesignToken.values.dark at F2. */
1573
- values?: {
1574
- dark?: string;
1575
- };
1576
- type?: 'color' | 'spacing' | 'size' | 'font' | 'other';
1577
- /** Tokens-panel grouping (flavors plan §10 C5). */
1578
- group?: 'colour' | 'type' | 'space' | 'radius' | 'shadow' | 'motion';
1579
- }
1580
- interface FlavorPack {
1581
- id: string;
1582
- /** Bumped when the pack's values change; seeding stamps `flavor:<id>@<version>` as DesignToken.source. */
1583
- version: number;
1584
- label: string;
1585
- blurb: string;
1586
- tokens: FlavorTokenDef[];
1587
- /** type → styleTrait → TOKEN NAME (never a CSS value — see the module header). */
1588
- defaults: Record<string, Record<string, string>>;
1589
- }
1590
- /** trait → `var(--iox-<token>, <packValue>)` for one node type. Unknown type → {}. */
1591
- declare function resolveFlavorDefaults(type: string, pack: FlavorPack | null | undefined): Record<string, string>;
1592
- /**
1593
- * The ENGINE's half of the cascade: kebab `prop: var(…)` declarations for every pack default
1594
- * the node's persisted styleProps does NOT set. Appended beside renderDefaultDeclarations —
1595
- * same guard-on-undefined discipline, so a node saved post-F1 (var already serialised via the
1596
- * builder's trait defaults) is untouched, and a legacy node picks the pack default up at
1597
- * render time. Node styleProps always wins.
1598
- */
1599
- declare function flavorDefaultDeclarations(type: string, styleProps: Record<string, any> | null | undefined, pack: FlavorPack | null | undefined): string[];
1600
- /**
1601
- * The CI gate (flavors plan §10 C4): a pack may only name types and traits that exist in the
1602
- * live component registry, and tokens it defines itself. Returns human-readable errors; a
1603
- * consumer spec feeds it the REAL registry and asserts [].
1604
- */
1605
- declare function validateFlavorPack(pack: FlavorPack, knownTraitsByType: Record<string, ReadonlySet<string> | string[]>): string[];
1606
- /**
1607
- * Swiss — the default pack (flavors plan §8: "the flavor that can never embarrass anyone").
1608
- * Deliberately SMALL at F1: radius + control padding on the clickables, surface radius on Card.
1609
- * Colour, type and motion arrive with the brand step (F2) — a colour default here would repaint
1610
- * legacy pages before anyone chose anything.
1611
- */
1612
- declare const SWISS_FLAVOR: FlavorPack;
1613
- /** What both renderers use until F2 lets an org choose. */
1614
- declare const DEFAULT_FLAVOR: FlavorPack;
1615
-
1616
1665
  /**
1617
1666
  * page-style — the ONE compiler that turns a page's style settings into CSS.
1618
1667
  *
@@ -2782,5 +2831,5 @@ declare class GalleryLoop {
2782
2831
  private emit;
2783
2832
  }
2784
2833
 
2785
- export { AOSService, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, CONTEXT_ACTION_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ContainerComponent, ContextStore, DEFAULT_FLAVOR, 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, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, LinkedContainerComponent, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, RESERVED_ALIAS_PREFIX, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_RESPONSIVE_TRAITS, SWISS_FLAVOR, 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, compileTokenCss, composeVirtualTraits, computeRelative, contextActionEffect, contextSlotKeys, countOverrides, dataSourcePlanToRequest, declaredInputNames, defaultMessageResolver, deriveSeoFromContent, describeSeoFields, effectiveTransitionId, excludeSelf, feelFor, flavorDefaultDeclarations, getByPath, getSeoValue, initialSlotValues, isActionEnabled, isContextAction, isInputOverriddenAt, isOverriddenAt, isReservedAlias, isValidSlotKey, legacyEnterToTransition, matchRouteParams, mediaQueryFor, mergeSwiperBreakpoints, nearestCommonAncestorId, nodeCssId, nodeUsesContext, normalizeCollectionResult, normalizeGalleryImage, normalizeIconClass, parentBreakpoint, parseUrlList, partClassName, planDataSource, planResponsiveRules, referencedDataSources, referencedDataSourcesDeep, renderDefaultDeclarations, resetSeoField, resolveBoundValue, resolveContextValue, resolveDerivedInto, resolveFlavorDefaults, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveRelativeInto, resolveRepeaterItems, resolveSeo, resolveSiblings, resolveSingleItemId, resolveStateStyles, resolveStyleProps, resolveValueChain, rewriteViewportUnits, safeSetInput, setInputOverride, setOverride, setSeoAuto, setSeoBinding, setSeoOverride, setSeoValue, stateNamesIn, stripSuppressedProps, styleKeyToKebab, tokensToDarkVarMap, tokensToVarMap, validateFlavorPack, variableAliases, variableItemsFrom, withContextSlots, wrapInBreakpointQuery, wrapInMediaQuery };
2786
- export type { BoundValueSpec, BreakpointDef, BuildRelatedFilterOptions, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, DataSourceDomain, DataSourceEndpoints, DataSourceMode, DataSourcePlan, DataSourceQueryFilter, DataSourceRequest, DataSourceRequestSpec, DataSourceRouteFilter, DataSourceSpec, DesignTokenLike, FlavorPack, FlavorTokenDef, 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 };
2834
+ export { AOSService, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, CONTEXT_ACTION_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ContainerComponent, ContextStore, DEFAULT_FLAVOR, 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, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, LinkedContainerComponent, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, RESERVED_ALIAS_PREFIX, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_RESPONSIVE_TRAITS, SWISS_FLAVOR, SectionComponent, TRANSITION_FEELS, TextBlockComponent, VALIDATION_STATES, VALIDATION_STATE_CLASS, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, activeActions, apcaLc, applyBindingPipes, applyFeel, breakpointDef, breakpointForWidth, breakpointQueryFor, buildDataSourceFilter, buildPartSelector, buildRelatedFilter, buildSwiperBreakpoints, buildTargetedStateSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, collectContextSlotKeys, collectReferencedAliases, compileDeclarations, compilePageStyle, compileTokenCss, composeVirtualTraits, computeRelative, contextActionEffect, contextSlotKeys, countOverrides, dataSourcePlanToRequest, declaredInputNames, defaultMessageResolver, deriveBrandTokens, deriveSeoFromContent, describeSeoFields, effectiveTransitionId, excludeSelf, feelFor, flavorDefaultDeclarations, getByPath, getSeoValue, initialSlotValues, isActionEnabled, isContextAction, isInputOverriddenAt, isOverriddenAt, isReservedAlias, isValidSlotKey, legacyEnterToTransition, matchRouteParams, mediaQueryFor, mergeSwiperBreakpoints, nearestCommonAncestorId, nodeCssId, nodeUsesContext, normalizeCollectionResult, normalizeGalleryImage, normalizeIconClass, oklabToRgb, parentBreakpoint, parseHexColor, parseUrlList, partClassName, planDataSource, planResponsiveRules, referencedDataSources, referencedDataSourcesDeep, relativeLuminance, renderDefaultDeclarations, resetSeoField, resolveBoundValue, resolveContextValue, resolveDerivedInto, resolveFlavorDefaults, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveRelativeInto, resolveRepeaterItems, resolveSeo, resolveSiblings, resolveSingleItemId, resolveStateStyles, resolveStyleProps, resolveValueChain, rewriteViewportUnits, rgbToOklab, safeSetInput, setInputOverride, setOverride, setSeoAuto, setSeoBinding, setSeoOverride, setSeoValue, stateNamesIn, stripSuppressedProps, styleKeyToKebab, toHexColor, tokensToDarkVarMap, tokensToVarMap, validateFlavorPack, variableAliases, variableItemsFrom, wcagContrastRatio, withContextSlots, wrapInBreakpointQuery, wrapInMediaQuery };
2835
+ export type { BoundValueSpec, BreakpointDef, BuildRelatedFilterOptions, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, DataSourceDomain, DataSourceEndpoints, DataSourceMode, DataSourcePlan, DataSourceQueryFilter, DataSourceRequest, DataSourceRequestSpec, DataSourceRouteFilter, DataSourceSpec, DesignTokenLike, FlavorPack, FlavorTokenDef, FormMessageResolver, GalleryBoundSource, GalleryDurations, GalleryEffect, GalleryEffectDescriptor, GalleryImage, GalleryLoopOptions, GalleryLoopScheduler, GalleryLoopState, GallerySlideState, IBuilderEventEmitter, InputMap, IoxContextSlot, IoxPipe, OkLab, OverrideBreakpoint, PageStyleSettings, PageTransitionCategory, PageTransitionCategoryMeta, PageTransitionOptions, PageTransitionPreset, PipeArgSpec, PipeContext, PipeDescriptor, RelativeInputs, RelativeSourceKind, RelativeSourceSpec, ResolveSiblingsOptions, ResolvedPageTransition, ResponsiveLayer, ResponsiveOverrides, ResponsiveRuleSet, Rgb, 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.5.2",
3
+ "version": "5.7.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -11,7 +11,7 @@ export type ActionKind = 'animated' | 'instant' | 'server';
11
11
 
12
12
  /** Picker grouping. Flat lists die at ~10 entries; the group is data, never a second list. */
13
13
  export type ActionGroup =
14
- | 'motion' | 'visibility' | 'navigate' | 'dialog' | 'form' | 'data' | 'service';
14
+ | 'motion' | 'visibility' | 'navigate' | 'dialog' | 'form' | 'data' | 'consent' | 'service';
15
15
 
16
16
  /**
17
17
  * What a step does inside the live builder canvas (the canvas RUNS interactions):
@@ -25,6 +25,8 @@ export interface ActionParamSpec {
25
25
  name: string;
26
26
  type: 'number' | 'string' | 'boolean' | 'page' | 'node' | 'form';
27
27
  label: string;
28
+ /** Closed value set — the editor renders a select instead of a free text input. */
29
+ options?: readonly { value: string; labelKey: string }[];
28
30
  }
29
31
 
30
32
  /** Does this action point at another node — and may it be any node or only certain types? */
@@ -59,6 +59,22 @@ export const ELEMENT_ACTION_DESCRIPTORS: readonly ActionDescriptor[] = Object.fr
59
59
  { type: 'copyToClipboard', labelKey: 'builder.actions.copyToClipboard', kind: 'instant', group: 'data', builderBehaviour: 'run',
60
60
  params: [{ name: 'text', type: 'string', label: 'Text' }] },
61
61
 
62
+ // ── Consent (C3 — the banner's vocabulary; all pure writes of the consent cookie) ──
63
+ // The canvas SIMULATES the recording ones: the builder runs in the ADMIN's browser, and an
64
+ // admin's cookie is not a visitor's decision. Opening the preferences dialog is honest to run.
65
+ { type: 'grantConsent', labelKey: 'builder.actions.grantConsent', kind: 'instant', group: 'consent', builderBehaviour: 'simulate' },
66
+ { type: 'denyConsent', labelKey: 'builder.actions.denyConsent', kind: 'instant', group: 'consent', builderBehaviour: 'simulate' },
67
+ { type: 'setConsentCategory', labelKey: 'builder.actions.setConsentCategory', kind: 'instant', group: 'consent', builderBehaviour: 'simulate',
68
+ params: [
69
+ { name: 'category', type: 'string', label: 'Category', options: [
70
+ { value: 'analytics', labelKey: 'builder.actions.params.categoryAnalytics' },
71
+ { value: 'marketing', labelKey: 'builder.actions.params.categoryMarketing' },
72
+ ] },
73
+ { name: 'granted', type: 'boolean', label: 'Granted' },
74
+ ] },
75
+ { type: 'openConsentPreferences', labelKey: 'builder.actions.openConsentPreferences', kind: 'instant', group: 'consent', builderBehaviour: 'run',
76
+ target: { required: true, types: ['Dialog'] } },
77
+
62
78
  // ── Services (D-A5: visible, greyed, with the reason — never hidden, never live) ──
63
79
  { type: 'signIn', labelKey: 'builder.actions.signIn', kind: 'server', group: 'service', builderBehaviour: 'inert',
64
80
  planned: true, entitlement: 'visitorIdentity' },
@@ -71,9 +87,14 @@ export const ELEMENT_ACTION_DESCRIPTORS: readonly ActionDescriptor[] = Object.fr
71
87
  export const BEHAVIOUR_ACTION_TYPES: ReadonlySet<string> =
72
88
  new Set(['navigate', 'scrollTo', 'submitForm', 'copyToClipboard']);
73
89
 
90
+ /** The consent actions' dispatch set (C3) — the engines branch on it beside DIALOG/BEHAVIOUR.
91
+ * `openConsentPreferences` is dialog-opening by nature and included so ONE branch owns consent. */
92
+ export const CONSENT_ACTION_TYPES: ReadonlySet<string> =
93
+ new Set(['grantConsent', 'denyConsent', 'setConsentCategory', 'openConsentPreferences']);
94
+
74
95
  /** Picker order — most-used groups first, services last (they are the shop window, not the shop). */
75
96
  export const ELEMENT_ACTION_GROUP_ORDER: readonly string[] =
76
- Object.freeze(['motion', 'visibility', 'navigate', 'dialog', 'form', 'data', 'service']);
97
+ Object.freeze(['motion', 'visibility', 'navigate', 'dialog', 'form', 'data', 'consent', 'service']);
77
98
 
78
99
  /** The catalog, grouped and ordered for the picker — one derivation, no hand-kept second list. */
79
100
  export function groupedElementActions(): ReadonlyArray<{ group: string; actions: ActionDescriptor[] }> {
@@ -1,4 +1,5 @@
1
1
  import { ConsentCategory, ConsentDecision, hasConsent } from './consent.model';
2
+ import { gtagLoaderUrl } from './decisions';
2
3
 
3
4
  /**
4
5
  * Google Consent Mode v2 (consent plan §4): four parameters, mandatory for EEA/UK measurement
@@ -44,10 +45,11 @@ export function consentModeBootstrapJs(): string {
44
45
  */
45
46
  export function analyticsInjectionHtml(measurementId: string, decision: ConsentDecision): string {
46
47
  const id = (measurementId ?? '').trim();
47
- if (!id || !/^[A-Za-z0-9-]+$/.test(id)) return '';
48
+ const loader = gtagLoaderUrl(id);
49
+ if (!loader) return '';
48
50
  if (!hasConsent(decision, ConsentCategory.Analytics)) return '';
49
51
  const signals = JSON.stringify(toConsentModeSignals(decision));
50
52
  return `<script>gtag('consent','update',${signals});</script>` +
51
- `<script async src="https://www.googletagmanager.com/gtag/js?id=${id}"></script>` +
53
+ `<script async src="${loader}"></script>` +
52
54
  `<script>gtag('js',new Date());gtag('config','${id}');</script>`;
53
55
  }
@@ -0,0 +1,111 @@
1
+ import { ConsentCategory, ConsentDecision } from './consent.model';
2
+
3
+ /**
4
+ * Decision construction for the banner's actions (consent plan C3). Pure on purpose: the builder
5
+ * canvas SIMULATES these (it is the admin's browser, not a visitor's), the engine executes them,
6
+ * and the e2e asserts the cookies they produce — one implementation of what each button means.
7
+ *
8
+ * `necessary` is always granted and never asked. A deny writes an EXPLICIT false per offered
9
+ * category — a recorded "no" is a decision (`needsPrompt` contract, C1), never an absence.
10
+ */
11
+
12
+ /** What an org offers when it has not chosen yet — the two non-necessary categories. */
13
+ export const DEFAULT_OFFERED_CATEGORIES: readonly ConsentCategory[] =
14
+ Object.freeze([ConsentCategory.Analytics, ConsentCategory.Marketing]);
15
+
16
+ function base(anonymousId: string, policyVersion: string, now: Date): ConsentDecision {
17
+ return {
18
+ categories: { [ConsentCategory.Necessary]: true },
19
+ policyVersion,
20
+ decidedAt: now.toISOString(),
21
+ anonymousId,
22
+ };
23
+ }
24
+
25
+ /** "Accept all": every OFFERED category granted. */
26
+ export function grantAllDecision(
27
+ offered: readonly ConsentCategory[],
28
+ anonymousId: string,
29
+ policyVersion: string,
30
+ now: Date = new Date(),
31
+ ): ConsentDecision {
32
+ const decision = base(anonymousId, policyVersion, now);
33
+ for (const category of offered) {
34
+ if (category !== ConsentCategory.Necessary) decision.categories[category] = true;
35
+ }
36
+ return decision;
37
+ }
38
+
39
+ /** "Reject all": every OFFERED category recorded as an explicit no. */
40
+ export function denyAllDecision(
41
+ offered: readonly ConsentCategory[],
42
+ anonymousId: string,
43
+ policyVersion: string,
44
+ now: Date = new Date(),
45
+ ): ConsentDecision {
46
+ const decision = base(anonymousId, policyVersion, now);
47
+ for (const category of offered) {
48
+ if (category !== ConsentCategory.Necessary) decision.categories[category] = false;
49
+ }
50
+ return decision;
51
+ }
52
+
53
+ /**
54
+ * One category set explicitly (the preferences modal's per-category buttons). Carries every
55
+ * other category of the PREVIOUS decision forward; with no previous decision the rest stay
56
+ * unrecorded — the prompt logic still owes the visitor a first-layer ask.
57
+ */
58
+ export function setCategoryDecision(
59
+ previous: ConsentDecision | null | undefined,
60
+ category: ConsentCategory,
61
+ granted: boolean,
62
+ anonymousId: string,
63
+ policyVersion: string,
64
+ now: Date = new Date(),
65
+ ): ConsentDecision {
66
+ const decision = base(anonymousId, policyVersion, now);
67
+ if (previous?.categories) {
68
+ for (const [key, value] of Object.entries(previous.categories)) {
69
+ decision.categories[key as ConsentCategory] = value === true;
70
+ }
71
+ decision.categories[ConsentCategory.Necessary] = true;
72
+ }
73
+ if (category !== ConsentCategory.Necessary) decision.categories[category] = granted;
74
+ return decision;
75
+ }
76
+
77
+ /** The per-site config the SSR server embeds for the prompt + the post-grant loader (C3). */
78
+ export interface ConsentSiteConfig {
79
+ policyVersion: string;
80
+ /** The categories the org offers beyond `necessary`. */
81
+ categories: readonly ConsentCategory[];
82
+ /** Site-tag id so a grant can inject the loader WITHOUT a reload; null = none configured. */
83
+ measurementId?: string | null;
84
+ /** Route of the org's policy page, for the banner's link; null = not chosen (C5 blocks publish). */
85
+ policyRoute?: string | null;
86
+ }
87
+
88
+ /** The window global the engine's runtime reads the config from. */
89
+ export const CONSENT_CONFIG_GLOBAL = '__ioxConsentCfg';
90
+
91
+ /**
92
+ * The inline `<head>` embed carrying the config to the browser. Values are JSON-escaped and
93
+ * `<` is unicode-escaped so user-set strings (a policy version is author input) can never
94
+ * close the script tag.
95
+ */
96
+ export function consentConfigJs(config: ConsentSiteConfig): string {
97
+ const json = JSON.stringify({
98
+ policyVersion: config.policyVersion,
99
+ categories: config.categories,
100
+ measurementId: config.measurementId ?? null,
101
+ policyRoute: config.policyRoute ?? null,
102
+ }).replace(/</g, '\\u003c');
103
+ return `window.${CONSENT_CONFIG_GLOBAL}=${json};`;
104
+ }
105
+
106
+ /** The gtag loader URL for a measurement id — one spelling for SSR injection and client inject. */
107
+ export function gtagLoaderUrl(measurementId: string): string | null {
108
+ const id = (measurementId ?? '').trim();
109
+ if (!id || !/^[A-Za-z0-9-]+$/.test(id)) return null;
110
+ return `https://www.googletagmanager.com/gtag/js?id=${id}`;
111
+ }
@@ -12,3 +12,4 @@
12
12
  export * from './consent.model';
13
13
  export * from './storage';
14
14
  export * from './consent-mode';
15
+ export * from './decisions';
@@ -36,8 +36,12 @@ export function decode(raw: string | null | undefined): ConsentDecision | null {
36
36
  }
37
37
  }
38
38
 
39
- /** `Set-Cookie` attributes for the decision: a NECESSARY cookie, 12 months, first-party. */
40
- export function cookieAttributes(maxMonths = 12): string {
39
+ /**
40
+ * `Set-Cookie` attributes for the decision: a NECESSARY cookie, 12 months, first-party.
41
+ * `secure` is a parameter because a Secure cookie is silently DROPPED on plain http — the
42
+ * engine passes `location.protocol === 'https:'` so dev servers and the e2e still record.
43
+ */
44
+ export function cookieAttributes(maxMonths = 12, secure = true): string {
41
45
  const maxAge = Math.floor(maxMonths * 30.44 * 24 * 60 * 60);
42
- return `Path=/; Max-Age=${maxAge}; SameSite=Lax; Secure`;
46
+ return `Path=/; Max-Age=${maxAge}; SameSite=Lax${secure ? '; Secure' : ''}`;
43
47
  }