@vectoriox/iox-ui 5.5.2 → 5.6.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.
@@ -8,7 +8,7 @@
8
8
  /** How a step executes — decides which editor fields appear (timing only for `animated`). */
9
9
  type ActionKind = 'animated' | 'instant' | 'server';
10
10
  /** Picker grouping. Flat lists die at ~10 entries; the group is data, never a second list. */
11
- type ActionGroup = 'motion' | 'visibility' | 'navigate' | 'dialog' | 'form' | 'data' | 'service';
11
+ type ActionGroup = 'motion' | 'visibility' | 'navigate' | 'dialog' | 'form' | 'data' | 'consent' | 'service';
12
12
  /**
13
13
  * What a step does inside the live builder canvas (the canvas RUNS interactions):
14
14
  * `run` for real (open the dialog), `simulate` announce-only (navigate), `inert` with a
@@ -20,6 +20,11 @@ interface ActionParamSpec {
20
20
  name: string;
21
21
  type: 'number' | 'string' | 'boolean' | 'page' | 'node' | 'form';
22
22
  label: string;
23
+ /** Closed value set — the editor renders a select instead of a free text input. */
24
+ options?: readonly {
25
+ value: string;
26
+ labelKey: string;
27
+ }[];
23
28
  }
24
29
  /** Does this action point at another node — and may it be any node or only certain types? */
25
30
  interface ActionTargetSpec {
@@ -97,6 +102,9 @@ declare const ELEMENT_ACTION_DESCRIPTORS: readonly ActionDescriptor[];
97
102
  /** The Tier-A behaviour executors' dispatch set (A1c) — the engines branch on it beside
98
103
  * DIALOG/SWIPER. Dialog actions have their own set; context/visibility stay in the legacy path. */
99
104
  declare const BEHAVIOUR_ACTION_TYPES: ReadonlySet<string>;
105
+ /** The consent actions' dispatch set (C3) — the engines branch on it beside DIALOG/BEHAVIOUR.
106
+ * `openConsentPreferences` is dialog-opening by nature and included so ONE branch owns consent. */
107
+ declare const CONSENT_ACTION_TYPES: ReadonlySet<string>;
100
108
  /** Picker order — most-used groups first, services last (they are the shop window, not the shop). */
101
109
  declare const ELEMENT_ACTION_GROUP_ORDER: readonly string[];
102
110
  /** The catalog, grouped and ordered for the picker — one derivation, no hand-kept second list. */
@@ -105,5 +113,5 @@ declare function groupedElementActions(): ReadonlyArray<{
105
113
  actions: ActionDescriptor[];
106
114
  }>;
107
115
 
108
- export { BEHAVIOUR_ACTION_TYPES, ELEMENT_ACTION_DESCRIPTORS, ELEMENT_ACTION_GROUP_ORDER, FORM_SUBMIT_ACTION_DESCRIPTORS, addableActions, groupedElementActions, resolveActionAvailability };
116
+ export { BEHAVIOUR_ACTION_TYPES, CONSENT_ACTION_TYPES, ELEMENT_ACTION_DESCRIPTORS, ELEMENT_ACTION_GROUP_ORDER, FORM_SUBMIT_ACTION_DESCRIPTORS, addableActions, groupedElementActions, resolveActionAvailability };
109
117
  export type { ActionAvailability, ActionAvailabilityContext, ActionAvailabilityState, ActionDescriptor, ActionGroup, ActionKind, ActionParamSpec, ActionTargetSpec, BuilderBehaviour };
@@ -40,8 +40,12 @@ declare function encode(decision: ConsentDecision): string;
40
40
  * ("no decision"), never a throw and never a guess. The old fork threw its way to a re-prompt.
41
41
  */
42
42
  declare function decode(raw: string | null | undefined): ConsentDecision | null;
43
- /** `Set-Cookie` attributes for the decision: a NECESSARY cookie, 12 months, first-party. */
44
- declare function cookieAttributes(maxMonths?: number): string;
43
+ /**
44
+ * `Set-Cookie` attributes for the decision: a NECESSARY cookie, 12 months, first-party.
45
+ * `secure` is a parameter because a Secure cookie is silently DROPPED on plain http — the
46
+ * engine passes `location.protocol === 'https:'` so dev servers and the e2e still record.
47
+ */
48
+ declare function cookieAttributes(maxMonths?: number, secure?: boolean): string;
45
49
 
46
50
  /**
47
51
  * Google Consent Mode v2 (consent plan §4): four parameters, mandatory for EEA/UK measurement
@@ -62,5 +66,46 @@ declare function consentModeBootstrapJs(): string;
62
66
  */
63
67
  declare function analyticsInjectionHtml(measurementId: string, decision: ConsentDecision): string;
64
68
 
65
- export { CONSENT_COOKIE_NAME, CONSENT_DEFAULTS_DENIED, CONSENT_MODE_PARAMS, ConsentCategory, analyticsInjectionHtml, consentModeBootstrapJs, cookieAttributes, decode, emptyDecision, encode, hasConsent, isExpired, needsPrompt, toConsentModeSignals };
66
- export type { ConsentDecision, ConsentModeSignal };
69
+ /**
70
+ * Decision construction for the banner's actions (consent plan C3). Pure on purpose: the builder
71
+ * canvas SIMULATES these (it is the admin's browser, not a visitor's), the engine executes them,
72
+ * and the e2e asserts the cookies they produce — one implementation of what each button means.
73
+ *
74
+ * `necessary` is always granted and never asked. A deny writes an EXPLICIT false per offered
75
+ * category — a recorded "no" is a decision (`needsPrompt` contract, C1), never an absence.
76
+ */
77
+ /** What an org offers when it has not chosen yet — the two non-necessary categories. */
78
+ declare const DEFAULT_OFFERED_CATEGORIES: readonly ConsentCategory[];
79
+ /** "Accept all": every OFFERED category granted. */
80
+ declare function grantAllDecision(offered: readonly ConsentCategory[], anonymousId: string, policyVersion: string, now?: Date): ConsentDecision;
81
+ /** "Reject all": every OFFERED category recorded as an explicit no. */
82
+ declare function denyAllDecision(offered: readonly ConsentCategory[], anonymousId: string, policyVersion: string, now?: Date): ConsentDecision;
83
+ /**
84
+ * One category set explicitly (the preferences modal's per-category buttons). Carries every
85
+ * other category of the PREVIOUS decision forward; with no previous decision the rest stay
86
+ * unrecorded — the prompt logic still owes the visitor a first-layer ask.
87
+ */
88
+ declare function setCategoryDecision(previous: ConsentDecision | null | undefined, category: ConsentCategory, granted: boolean, anonymousId: string, policyVersion: string, now?: Date): ConsentDecision;
89
+ /** The per-site config the SSR server embeds for the prompt + the post-grant loader (C3). */
90
+ interface ConsentSiteConfig {
91
+ policyVersion: string;
92
+ /** The categories the org offers beyond `necessary`. */
93
+ categories: readonly ConsentCategory[];
94
+ /** Site-tag id so a grant can inject the loader WITHOUT a reload; null = none configured. */
95
+ measurementId?: string | null;
96
+ /** Route of the org's policy page, for the banner's link; null = not chosen (C5 blocks publish). */
97
+ policyRoute?: string | null;
98
+ }
99
+ /** The window global the engine's runtime reads the config from. */
100
+ declare const CONSENT_CONFIG_GLOBAL = "__ioxConsentCfg";
101
+ /**
102
+ * The inline `<head>` embed carrying the config to the browser. Values are JSON-escaped and
103
+ * `<` is unicode-escaped so user-set strings (a policy version is author input) can never
104
+ * close the script tag.
105
+ */
106
+ declare function consentConfigJs(config: ConsentSiteConfig): string;
107
+ /** The gtag loader URL for a measurement id — one spelling for SSR injection and client inject. */
108
+ declare function gtagLoaderUrl(measurementId: string): string | null;
109
+
110
+ export { CONSENT_CONFIG_GLOBAL, CONSENT_COOKIE_NAME, CONSENT_DEFAULTS_DENIED, CONSENT_MODE_PARAMS, ConsentCategory, DEFAULT_OFFERED_CATEGORIES, analyticsInjectionHtml, consentConfigJs, consentModeBootstrapJs, cookieAttributes, decode, denyAllDecision, emptyDecision, encode, grantAllDecision, gtagLoaderUrl, hasConsent, isExpired, needsPrompt, setCategoryDecision, toConsentModeSignals };
111
+ export type { ConsentDecision, ConsentModeSignal, ConsentSiteConfig };
@@ -91,6 +91,21 @@ const ELEMENT_ACTION_DESCRIPTORS = Object.freeze([
91
91
  { type: 'clearContext', labelKey: 'builder.actions.clearContext', kind: 'instant', group: 'data', builderBehaviour: 'run' },
92
92
  { type: 'copyToClipboard', labelKey: 'builder.actions.copyToClipboard', kind: 'instant', group: 'data', builderBehaviour: 'run',
93
93
  params: [{ name: 'text', type: 'string', label: 'Text' }] },
94
+ // ── Consent (C3 — the banner's vocabulary; all pure writes of the consent cookie) ──
95
+ // The canvas SIMULATES the recording ones: the builder runs in the ADMIN's browser, and an
96
+ // admin's cookie is not a visitor's decision. Opening the preferences dialog is honest to run.
97
+ { type: 'grantConsent', labelKey: 'builder.actions.grantConsent', kind: 'instant', group: 'consent', builderBehaviour: 'simulate' },
98
+ { type: 'denyConsent', labelKey: 'builder.actions.denyConsent', kind: 'instant', group: 'consent', builderBehaviour: 'simulate' },
99
+ { type: 'setConsentCategory', labelKey: 'builder.actions.setConsentCategory', kind: 'instant', group: 'consent', builderBehaviour: 'simulate',
100
+ params: [
101
+ { name: 'category', type: 'string', label: 'Category', options: [
102
+ { value: 'analytics', labelKey: 'builder.actions.params.categoryAnalytics' },
103
+ { value: 'marketing', labelKey: 'builder.actions.params.categoryMarketing' },
104
+ ] },
105
+ { name: 'granted', type: 'boolean', label: 'Granted' },
106
+ ] },
107
+ { type: 'openConsentPreferences', labelKey: 'builder.actions.openConsentPreferences', kind: 'instant', group: 'consent', builderBehaviour: 'run',
108
+ target: { required: true, types: ['Dialog'] } },
94
109
  // ── Services (D-A5: visible, greyed, with the reason — never hidden, never live) ──
95
110
  { type: 'signIn', labelKey: 'builder.actions.signIn', kind: 'server', group: 'service', builderBehaviour: 'inert',
96
111
  planned: true, entitlement: 'visitorIdentity' },
@@ -100,8 +115,11 @@ const ELEMENT_ACTION_DESCRIPTORS = Object.freeze([
100
115
  /** The Tier-A behaviour executors' dispatch set (A1c) — the engines branch on it beside
101
116
  * DIALOG/SWIPER. Dialog actions have their own set; context/visibility stay in the legacy path. */
102
117
  const BEHAVIOUR_ACTION_TYPES = new Set(['navigate', 'scrollTo', 'submitForm', 'copyToClipboard']);
118
+ /** The consent actions' dispatch set (C3) — the engines branch on it beside DIALOG/BEHAVIOUR.
119
+ * `openConsentPreferences` is dialog-opening by nature and included so ONE branch owns consent. */
120
+ const CONSENT_ACTION_TYPES = new Set(['grantConsent', 'denyConsent', 'setConsentCategory', 'openConsentPreferences']);
103
121
  /** Picker order — most-used groups first, services last (they are the shop window, not the shop). */
104
- const ELEMENT_ACTION_GROUP_ORDER = Object.freeze(['motion', 'visibility', 'navigate', 'dialog', 'form', 'data', 'service']);
122
+ const ELEMENT_ACTION_GROUP_ORDER = Object.freeze(['motion', 'visibility', 'navigate', 'dialog', 'form', 'data', 'consent', 'service']);
105
123
  /** The catalog, grouped and ordered for the picker — one derivation, no hand-kept second list. */
106
124
  function groupedElementActions() {
107
125
  return ELEMENT_ACTION_GROUP_ORDER
@@ -124,5 +142,5 @@ function groupedElementActions() {
124
142
  * Generated bundle index. Do not edit.
125
143
  */
126
144
 
127
- export { BEHAVIOUR_ACTION_TYPES, ELEMENT_ACTION_DESCRIPTORS, ELEMENT_ACTION_GROUP_ORDER, FORM_SUBMIT_ACTION_DESCRIPTORS, addableActions, groupedElementActions, resolveActionAvailability };
145
+ export { BEHAVIOUR_ACTION_TYPES, CONSENT_ACTION_TYPES, ELEMENT_ACTION_DESCRIPTORS, ELEMENT_ACTION_GROUP_ORDER, FORM_SUBMIT_ACTION_DESCRIPTORS, addableActions, groupedElementActions, resolveActionAvailability };
128
146
  //# sourceMappingURL=vectoriox-iox-ui-actions-core.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"vectoriox-iox-ui-actions-core.mjs","sources":["../../../projects/iox-ui/src/actions-core/action.model.ts","../../../projects/iox-ui/src/actions-core/availability.ts","../../../projects/iox-ui/src/actions-core/element-actions.ts","../../../projects/iox-ui/src/actions-core/public-api.ts","../../../projects/iox-ui/src/actions-core/vectoriox-iox-ui-actions-core.ts"],"sourcesContent":["/**\n * The ONE descriptor shape for everything an author can put in an ordered action list —\n * a form's after-submit steps today, element behaviour actions (A1) next. Promoted out of\n * `forms-core` because a button action importing from the forms module would be a lie; the\n * forms catalog lives here now and `forms-core` re-exports it unchanged.\n * See iox-ai-guidance `architecture/builder/actions-framework-plan.md` §4 (A0).\n */\n\n/** How a step executes — decides which editor fields appear (timing only for `animated`). */\nexport type ActionKind = 'animated' | 'instant' | 'server';\n\n/** Picker grouping. Flat lists die at ~10 entries; the group is data, never a second list. */\nexport type ActionGroup =\n | 'motion' | 'visibility' | 'navigate' | 'dialog' | 'form' | 'data' | 'service';\n\n/**\n * What a step does inside the live builder canvas (the canvas RUNS interactions):\n * `run` for real (open the dialog), `simulate` announce-only (navigate), `inert` with a\n * \"runs on the live site\" note (anything server-side or costing money).\n */\nexport type BuilderBehaviour = 'run' | 'simulate' | 'inert';\n\n/** A configurable parameter, same shape ComponentMethodDef.params already uses. */\nexport interface ActionParamSpec {\n name: string;\n type: 'number' | 'string' | 'boolean' | 'page' | 'node' | 'form';\n label: string;\n}\n\n/** Does this action point at another node — and may it be any node or only certain types? */\nexport interface ActionTargetSpec {\n required: boolean;\n types?: string[];\n}\n\n/**\n * One row of an action catalog. Every field beyond `type`/`labelKey` is optional so the\n * pre-A0 forms descriptors remain valid verbatim.\n */\nexport interface ActionDescriptor {\n type: string;\n labelKey: string;\n kind?: ActionKind;\n group?: ActionGroup;\n builderBehaviour?: BuilderBehaviour;\n target?: ActionTargetSpec;\n params?: ActionParamSpec[];\n /** The connector that must be configured & healthy before this action is offerable. */\n connector?: string;\n /** Plan-entitlement key (AI plan D5). Absent = never gated by plan. */\n entitlement?: string;\n /** Always present in the list and not removable (forms: `store`). */\n locked?: boolean;\n /** Not offered yet — shown greyed with a \"coming soon\" note, so the roadmap is visible. */\n planned?: boolean;\n}\n\n/**\n * The FORM after-submit catalog (moved verbatim from forms-core; `forms-core` re-exports it\n * under its old name `ACTION_DESCRIPTORS`). A1's element catalog is a SEPARATE constant —\n * the two lists share this shape, never one bucket.\n */\nexport const FORM_SUBMIT_ACTION_DESCRIPTORS: readonly ActionDescriptor[] = Object.freeze([\n { type: 'store', labelKey: 'forms.actions.store', locked: true, kind: 'server', group: 'data', builderBehaviour: 'inert' },\n { type: 'email', labelKey: 'forms.actions.email', kind: 'server', group: 'service', builderBehaviour: 'inert', connector: 'mailer' },\n { type: 'redirect', labelKey: 'forms.actions.redirect', kind: 'instant', group: 'navigate', builderBehaviour: 'simulate' },\n { type: 'createItem', labelKey: 'forms.actions.createItem', planned: true, kind: 'server', group: 'data', builderBehaviour: 'inert' },\n { type: 'webhook', labelKey: 'forms.actions.webhook', planned: true, kind: 'server', group: 'service', builderBehaviour: 'inert' },\n { type: 'autoReply', labelKey: 'forms.actions.autoReply', planned: true, kind: 'server', group: 'service', builderBehaviour: 'inert', connector: 'mailer' },\n]);\n\n/** Steps that can still be added to a list — each at most once, and never a planned one. */\nexport function addableActions(\n existing: readonly { type: string }[] = [],\n catalog: readonly ActionDescriptor[] = FORM_SUBMIT_ACTION_DESCRIPTORS,\n): ActionDescriptor[] {\n const used = new Set(existing.map(action => action.type));\n return catalog.filter(action => !action.locked && !action.planned && !used.has(action.type));\n}\n","import { ActionDescriptor } from './action.model';\n\n/**\n * K4 of the actions plan: four different reasons a catalog row is unavailable, four different\n * sentences — *coming soon* · *always on* · *connect X first* · *upgrade*. One resolver feeds\n * the picker AND the AI composer's validation, so they can never disagree.\n */\nexport type ActionAvailabilityState =\n | 'available'\n | 'planned' // not built yet — greyed, \"coming soon\"\n | 'locked' // always present, not addable/removable\n | 'connector-missing' // the named connector is not configured/healthy\n | 'entitlement-missing'; // the plan does not include it\n\nexport interface ActionAvailability {\n state: ActionAvailabilityState;\n /** Set when state is connector-missing / entitlement-missing — what to name in the message. */\n connector?: string;\n entitlement?: string;\n}\n\nexport interface ActionAvailabilityContext {\n /** Connector health by name. An absent map, or an absent key, means \"not known\" → allowed. */\n connectors?: Partial<Record<string, boolean>>;\n /**\n * Plan entitlements by key. `Plan.entitlements` does not exist yet anywhere (the known gap),\n * so an ABSENT map means \"no gating\" and every entitlement passes — the hook is live the day\n * the control plane starts sending them.\n */\n entitlements?: Partial<Record<string, boolean>>;\n}\n\nexport function resolveActionAvailability(\n descriptor: ActionDescriptor,\n ctx: ActionAvailabilityContext = {},\n): ActionAvailability {\n if (descriptor.planned) return { state: 'planned' };\n if (descriptor.locked) return { state: 'locked' };\n if (descriptor.connector && ctx.connectors && ctx.connectors[descriptor.connector] === false) {\n return { state: 'connector-missing', connector: descriptor.connector };\n }\n if (descriptor.entitlement && ctx.entitlements && ctx.entitlements[descriptor.entitlement] === false) {\n return { state: 'entitlement-missing', entitlement: descriptor.entitlement };\n }\n return { state: 'available' };\n}\n","import { ActionDescriptor } from './action.model';\n\n/**\n * The ELEMENT action catalog (actions plan A1) — what a node can DO, as data. One list feeds the\n * grouped picker, the two interaction engines, and the AI composer's validation (§9: the model\n * picks actions BY NAME from this catalog and can never invent behaviour).\n *\n * Shipped rows (motion/visibility/context/swiper) are listed so the catalog is the WHOLE truth;\n * their `kind`/`builderBehaviour` make the long-standing implicit rules explicit — the canvas is\n * a live runtime (feedback_builder_live_runtime), navigate must SIMULATE, services are INERT.\n * `signIn`/`startCheckout` ship greyed with the honest reason (D-A5): no visitor identity, no\n * payments — a subsystem and a product, not dropdown entries.\n */\nexport const ELEMENT_ACTION_DESCRIPTORS: readonly ActionDescriptor[] = Object.freeze([\n // ── Motion (shipped WAAPI actions — timing row applies) ──────────────────\n { type: 'fadeIn', labelKey: 'builder.actions.fadeIn', kind: 'animated', group: 'motion', builderBehaviour: 'run' },\n { type: 'fadeOut', labelKey: 'builder.actions.fadeOut', kind: 'animated', group: 'motion', builderBehaviour: 'run' },\n { type: 'moveUp', labelKey: 'builder.actions.moveUp', kind: 'animated', group: 'motion', builderBehaviour: 'run' },\n { type: 'moveDown', labelKey: 'builder.actions.moveDown', kind: 'animated', group: 'motion', builderBehaviour: 'run' },\n { type: 'moveLeft', labelKey: 'builder.actions.moveLeft', kind: 'animated', group: 'motion', builderBehaviour: 'run' },\n { type: 'moveRight',labelKey: 'builder.actions.moveRight',kind: 'animated', group: 'motion', builderBehaviour: 'run' },\n { type: 'scaleIn', labelKey: 'builder.actions.scaleIn', kind: 'animated', group: 'motion', builderBehaviour: 'run' },\n { type: 'scaleOut', labelKey: 'builder.actions.scaleOut', kind: 'animated', group: 'motion', builderBehaviour: 'run' },\n { type: 'rotate', labelKey: 'builder.actions.rotate', kind: 'animated', group: 'motion', builderBehaviour: 'run',\n params: [{ name: 'degrees', type: 'number', label: 'Degrees' }] },\n\n // ── Visibility (shipped) ─────────────────────────────────────────────────\n { type: 'show', labelKey: 'builder.actions.show', kind: 'instant', group: 'visibility', builderBehaviour: 'run' },\n { type: 'hide', labelKey: 'builder.actions.hide', kind: 'instant', group: 'visibility', builderBehaviour: 'run' },\n { type: 'toggleVisibility', labelKey: 'builder.actions.toggleVisibility', kind: 'instant', group: 'visibility', builderBehaviour: 'run' },\n\n // ── Navigate (A1 — Tier A) ───────────────────────────────────────────────\n { type: 'navigate', labelKey: 'builder.actions.navigate', kind: 'instant', group: 'navigate', builderBehaviour: 'simulate',\n params: [\n { name: 'navigateTo', type: 'string', label: 'Destination' }, // 'page' | 'url' | 'back'\n { name: 'page', type: 'page', label: 'Page' },\n { name: 'url', type: 'string', label: 'URL' },\n ] },\n { type: 'scrollTo', labelKey: 'builder.actions.scrollTo', kind: 'instant', group: 'navigate', builderBehaviour: 'run',\n target: { required: false } },\n\n // ── Dialog (A1 — needs the Dialog component, A1b) ───────────────────────\n { type: 'openDialog', labelKey: 'builder.actions.openDialog', kind: 'instant', group: 'dialog', builderBehaviour: 'run',\n target: { required: true, types: ['Dialog'] } },\n { type: 'closeDialog', labelKey: 'builder.actions.closeDialog', kind: 'instant', group: 'dialog', builderBehaviour: 'run',\n target: { required: true, types: ['Dialog'] } },\n { type: 'toggleDialog', labelKey: 'builder.actions.toggleDialog', kind: 'instant', group: 'dialog', builderBehaviour: 'run',\n target: { required: true, types: ['Dialog'] } },\n\n // ── Form (A1 — Tier A; the submit transport is the forms module's, I7 of its plan) ──\n // simulate in the CANVAS: the builder's Form is a container twin with no real <form>,\n // so there is nothing honest to run — the live engine requestSubmit()s the real one.\n { type: 'submitForm', labelKey: 'builder.actions.submitForm', kind: 'instant', group: 'form', builderBehaviour: 'simulate',\n target: { required: true, types: ['Form'] } },\n\n // ── Data (shipped context actions + the one trivial newcomer) ───────────\n { type: 'setContext', labelKey: 'builder.actions.setContext', kind: 'instant', group: 'data', builderBehaviour: 'run' },\n { type: 'clearContext', labelKey: 'builder.actions.clearContext', kind: 'instant', group: 'data', builderBehaviour: 'run' },\n { type: 'copyToClipboard', labelKey: 'builder.actions.copyToClipboard', kind: 'instant', group: 'data', builderBehaviour: 'run',\n params: [{ name: 'text', type: 'string', label: 'Text' }] },\n\n // ── Services (D-A5: visible, greyed, with the reason — never hidden, never live) ──\n { type: 'signIn', labelKey: 'builder.actions.signIn', kind: 'server', group: 'service', builderBehaviour: 'inert',\n planned: true, entitlement: 'visitorIdentity' },\n { type: 'startCheckout', labelKey: 'builder.actions.startCheckout', kind: 'server', group: 'service', builderBehaviour: 'inert',\n planned: true, connector: 'payment', entitlement: 'commerce' },\n]);\n\n/** The Tier-A behaviour executors' dispatch set (A1c) — the engines branch on it beside\n * DIALOG/SWIPER. Dialog actions have their own set; context/visibility stay in the legacy path. */\nexport const BEHAVIOUR_ACTION_TYPES: ReadonlySet<string> =\n new Set(['navigate', 'scrollTo', 'submitForm', 'copyToClipboard']);\n\n/** Picker order — most-used groups first, services last (they are the shop window, not the shop). */\nexport const ELEMENT_ACTION_GROUP_ORDER: readonly string[] =\n Object.freeze(['motion', 'visibility', 'navigate', 'dialog', 'form', 'data', 'service']);\n\n/** The catalog, grouped and ordered for the picker — one derivation, no hand-kept second list. */\nexport function groupedElementActions(): ReadonlyArray<{ group: string; actions: ActionDescriptor[] }> {\n return ELEMENT_ACTION_GROUP_ORDER\n .map(group => ({ group, actions: ELEMENT_ACTION_DESCRIPTORS.filter(d => d.group === group) }))\n .filter(g => g.actions.length > 0);\n}\n","/**\n * @vectoriox/iox-ui/actions-core — the shared, FRAMEWORK-FREE action vocabulary.\n *\n * One `ActionDescriptor` shape for every ordered action list in the product: the forms\n * after-submit steps (whose catalog moved here; `forms-core` re-exports it unchanged) and\n * the element behaviour actions of A1. Framework-free for the same reason as `forms-core`\n * and `seo-core`: `iox-cms` consumes it under plain Node.\n * ⚠️ Consumers wire the usual trio — ng-package raw-ts assets, jest moduleNameMapper, and\n * tsconfig paths — or `nest start` dies with ERR_REQUIRE_ESM while jest and build pass.\n */\nexport * from './action.model';\nexport * from './availability';\nexport * from './element-actions';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"vectoriox-iox-ui-actions-core.mjs","sources":["../../../projects/iox-ui/src/actions-core/action.model.ts","../../../projects/iox-ui/src/actions-core/availability.ts","../../../projects/iox-ui/src/actions-core/element-actions.ts","../../../projects/iox-ui/src/actions-core/public-api.ts","../../../projects/iox-ui/src/actions-core/vectoriox-iox-ui-actions-core.ts"],"sourcesContent":["/**\n * The ONE descriptor shape for everything an author can put in an ordered action list —\n * a form's after-submit steps today, element behaviour actions (A1) next. Promoted out of\n * `forms-core` because a button action importing from the forms module would be a lie; the\n * forms catalog lives here now and `forms-core` re-exports it unchanged.\n * See iox-ai-guidance `architecture/builder/actions-framework-plan.md` §4 (A0).\n */\n\n/** How a step executes — decides which editor fields appear (timing only for `animated`). */\nexport type ActionKind = 'animated' | 'instant' | 'server';\n\n/** Picker grouping. Flat lists die at ~10 entries; the group is data, never a second list. */\nexport type ActionGroup =\n | 'motion' | 'visibility' | 'navigate' | 'dialog' | 'form' | 'data' | 'consent' | 'service';\n\n/**\n * What a step does inside the live builder canvas (the canvas RUNS interactions):\n * `run` for real (open the dialog), `simulate` announce-only (navigate), `inert` with a\n * \"runs on the live site\" note (anything server-side or costing money).\n */\nexport type BuilderBehaviour = 'run' | 'simulate' | 'inert';\n\n/** A configurable parameter, same shape ComponentMethodDef.params already uses. */\nexport interface ActionParamSpec {\n name: string;\n type: 'number' | 'string' | 'boolean' | 'page' | 'node' | 'form';\n label: string;\n /** Closed value set — the editor renders a select instead of a free text input. */\n options?: readonly { value: string; labelKey: string }[];\n}\n\n/** Does this action point at another node — and may it be any node or only certain types? */\nexport interface ActionTargetSpec {\n required: boolean;\n types?: string[];\n}\n\n/**\n * One row of an action catalog. Every field beyond `type`/`labelKey` is optional so the\n * pre-A0 forms descriptors remain valid verbatim.\n */\nexport interface ActionDescriptor {\n type: string;\n labelKey: string;\n kind?: ActionKind;\n group?: ActionGroup;\n builderBehaviour?: BuilderBehaviour;\n target?: ActionTargetSpec;\n params?: ActionParamSpec[];\n /** The connector that must be configured & healthy before this action is offerable. */\n connector?: string;\n /** Plan-entitlement key (AI plan D5). Absent = never gated by plan. */\n entitlement?: string;\n /** Always present in the list and not removable (forms: `store`). */\n locked?: boolean;\n /** Not offered yet — shown greyed with a \"coming soon\" note, so the roadmap is visible. */\n planned?: boolean;\n}\n\n/**\n * The FORM after-submit catalog (moved verbatim from forms-core; `forms-core` re-exports it\n * under its old name `ACTION_DESCRIPTORS`). A1's element catalog is a SEPARATE constant —\n * the two lists share this shape, never one bucket.\n */\nexport const FORM_SUBMIT_ACTION_DESCRIPTORS: readonly ActionDescriptor[] = Object.freeze([\n { type: 'store', labelKey: 'forms.actions.store', locked: true, kind: 'server', group: 'data', builderBehaviour: 'inert' },\n { type: 'email', labelKey: 'forms.actions.email', kind: 'server', group: 'service', builderBehaviour: 'inert', connector: 'mailer' },\n { type: 'redirect', labelKey: 'forms.actions.redirect', kind: 'instant', group: 'navigate', builderBehaviour: 'simulate' },\n { type: 'createItem', labelKey: 'forms.actions.createItem', planned: true, kind: 'server', group: 'data', builderBehaviour: 'inert' },\n { type: 'webhook', labelKey: 'forms.actions.webhook', planned: true, kind: 'server', group: 'service', builderBehaviour: 'inert' },\n { type: 'autoReply', labelKey: 'forms.actions.autoReply', planned: true, kind: 'server', group: 'service', builderBehaviour: 'inert', connector: 'mailer' },\n]);\n\n/** Steps that can still be added to a list — each at most once, and never a planned one. */\nexport function addableActions(\n existing: readonly { type: string }[] = [],\n catalog: readonly ActionDescriptor[] = FORM_SUBMIT_ACTION_DESCRIPTORS,\n): ActionDescriptor[] {\n const used = new Set(existing.map(action => action.type));\n return catalog.filter(action => !action.locked && !action.planned && !used.has(action.type));\n}\n","import { ActionDescriptor } from './action.model';\n\n/**\n * K4 of the actions plan: four different reasons a catalog row is unavailable, four different\n * sentences — *coming soon* · *always on* · *connect X first* · *upgrade*. One resolver feeds\n * the picker AND the AI composer's validation, so they can never disagree.\n */\nexport type ActionAvailabilityState =\n | 'available'\n | 'planned' // not built yet — greyed, \"coming soon\"\n | 'locked' // always present, not addable/removable\n | 'connector-missing' // the named connector is not configured/healthy\n | 'entitlement-missing'; // the plan does not include it\n\nexport interface ActionAvailability {\n state: ActionAvailabilityState;\n /** Set when state is connector-missing / entitlement-missing — what to name in the message. */\n connector?: string;\n entitlement?: string;\n}\n\nexport interface ActionAvailabilityContext {\n /** Connector health by name. An absent map, or an absent key, means \"not known\" → allowed. */\n connectors?: Partial<Record<string, boolean>>;\n /**\n * Plan entitlements by key. `Plan.entitlements` does not exist yet anywhere (the known gap),\n * so an ABSENT map means \"no gating\" and every entitlement passes — the hook is live the day\n * the control plane starts sending them.\n */\n entitlements?: Partial<Record<string, boolean>>;\n}\n\nexport function resolveActionAvailability(\n descriptor: ActionDescriptor,\n ctx: ActionAvailabilityContext = {},\n): ActionAvailability {\n if (descriptor.planned) return { state: 'planned' };\n if (descriptor.locked) return { state: 'locked' };\n if (descriptor.connector && ctx.connectors && ctx.connectors[descriptor.connector] === false) {\n return { state: 'connector-missing', connector: descriptor.connector };\n }\n if (descriptor.entitlement && ctx.entitlements && ctx.entitlements[descriptor.entitlement] === false) {\n return { state: 'entitlement-missing', entitlement: descriptor.entitlement };\n }\n return { state: 'available' };\n}\n","import { ActionDescriptor } from './action.model';\n\n/**\n * The ELEMENT action catalog (actions plan A1) — what a node can DO, as data. One list feeds the\n * grouped picker, the two interaction engines, and the AI composer's validation (§9: the model\n * picks actions BY NAME from this catalog and can never invent behaviour).\n *\n * Shipped rows (motion/visibility/context/swiper) are listed so the catalog is the WHOLE truth;\n * their `kind`/`builderBehaviour` make the long-standing implicit rules explicit — the canvas is\n * a live runtime (feedback_builder_live_runtime), navigate must SIMULATE, services are INERT.\n * `signIn`/`startCheckout` ship greyed with the honest reason (D-A5): no visitor identity, no\n * payments — a subsystem and a product, not dropdown entries.\n */\nexport const ELEMENT_ACTION_DESCRIPTORS: readonly ActionDescriptor[] = Object.freeze([\n // ── Motion (shipped WAAPI actions — timing row applies) ──────────────────\n { type: 'fadeIn', labelKey: 'builder.actions.fadeIn', kind: 'animated', group: 'motion', builderBehaviour: 'run' },\n { type: 'fadeOut', labelKey: 'builder.actions.fadeOut', kind: 'animated', group: 'motion', builderBehaviour: 'run' },\n { type: 'moveUp', labelKey: 'builder.actions.moveUp', kind: 'animated', group: 'motion', builderBehaviour: 'run' },\n { type: 'moveDown', labelKey: 'builder.actions.moveDown', kind: 'animated', group: 'motion', builderBehaviour: 'run' },\n { type: 'moveLeft', labelKey: 'builder.actions.moveLeft', kind: 'animated', group: 'motion', builderBehaviour: 'run' },\n { type: 'moveRight',labelKey: 'builder.actions.moveRight',kind: 'animated', group: 'motion', builderBehaviour: 'run' },\n { type: 'scaleIn', labelKey: 'builder.actions.scaleIn', kind: 'animated', group: 'motion', builderBehaviour: 'run' },\n { type: 'scaleOut', labelKey: 'builder.actions.scaleOut', kind: 'animated', group: 'motion', builderBehaviour: 'run' },\n { type: 'rotate', labelKey: 'builder.actions.rotate', kind: 'animated', group: 'motion', builderBehaviour: 'run',\n params: [{ name: 'degrees', type: 'number', label: 'Degrees' }] },\n\n // ── Visibility (shipped) ─────────────────────────────────────────────────\n { type: 'show', labelKey: 'builder.actions.show', kind: 'instant', group: 'visibility', builderBehaviour: 'run' },\n { type: 'hide', labelKey: 'builder.actions.hide', kind: 'instant', group: 'visibility', builderBehaviour: 'run' },\n { type: 'toggleVisibility', labelKey: 'builder.actions.toggleVisibility', kind: 'instant', group: 'visibility', builderBehaviour: 'run' },\n\n // ── Navigate (A1 — Tier A) ───────────────────────────────────────────────\n { type: 'navigate', labelKey: 'builder.actions.navigate', kind: 'instant', group: 'navigate', builderBehaviour: 'simulate',\n params: [\n { name: 'navigateTo', type: 'string', label: 'Destination' }, // 'page' | 'url' | 'back'\n { name: 'page', type: 'page', label: 'Page' },\n { name: 'url', type: 'string', label: 'URL' },\n ] },\n { type: 'scrollTo', labelKey: 'builder.actions.scrollTo', kind: 'instant', group: 'navigate', builderBehaviour: 'run',\n target: { required: false } },\n\n // ── Dialog (A1 — needs the Dialog component, A1b) ───────────────────────\n { type: 'openDialog', labelKey: 'builder.actions.openDialog', kind: 'instant', group: 'dialog', builderBehaviour: 'run',\n target: { required: true, types: ['Dialog'] } },\n { type: 'closeDialog', labelKey: 'builder.actions.closeDialog', kind: 'instant', group: 'dialog', builderBehaviour: 'run',\n target: { required: true, types: ['Dialog'] } },\n { type: 'toggleDialog', labelKey: 'builder.actions.toggleDialog', kind: 'instant', group: 'dialog', builderBehaviour: 'run',\n target: { required: true, types: ['Dialog'] } },\n\n // ── Form (A1 — Tier A; the submit transport is the forms module's, I7 of its plan) ──\n // simulate in the CANVAS: the builder's Form is a container twin with no real <form>,\n // so there is nothing honest to run — the live engine requestSubmit()s the real one.\n { type: 'submitForm', labelKey: 'builder.actions.submitForm', kind: 'instant', group: 'form', builderBehaviour: 'simulate',\n target: { required: true, types: ['Form'] } },\n\n // ── Data (shipped context actions + the one trivial newcomer) ───────────\n { type: 'setContext', labelKey: 'builder.actions.setContext', kind: 'instant', group: 'data', builderBehaviour: 'run' },\n { type: 'clearContext', labelKey: 'builder.actions.clearContext', kind: 'instant', group: 'data', builderBehaviour: 'run' },\n { type: 'copyToClipboard', labelKey: 'builder.actions.copyToClipboard', kind: 'instant', group: 'data', builderBehaviour: 'run',\n params: [{ name: 'text', type: 'string', label: 'Text' }] },\n\n // ── Consent (C3 — the banner's vocabulary; all pure writes of the consent cookie) ──\n // The canvas SIMULATES the recording ones: the builder runs in the ADMIN's browser, and an\n // admin's cookie is not a visitor's decision. Opening the preferences dialog is honest to run.\n { type: 'grantConsent', labelKey: 'builder.actions.grantConsent', kind: 'instant', group: 'consent', builderBehaviour: 'simulate' },\n { type: 'denyConsent', labelKey: 'builder.actions.denyConsent', kind: 'instant', group: 'consent', builderBehaviour: 'simulate' },\n { type: 'setConsentCategory', labelKey: 'builder.actions.setConsentCategory', kind: 'instant', group: 'consent', builderBehaviour: 'simulate',\n params: [\n { name: 'category', type: 'string', label: 'Category', options: [\n { value: 'analytics', labelKey: 'builder.actions.params.categoryAnalytics' },\n { value: 'marketing', labelKey: 'builder.actions.params.categoryMarketing' },\n ] },\n { name: 'granted', type: 'boolean', label: 'Granted' },\n ] },\n { type: 'openConsentPreferences', labelKey: 'builder.actions.openConsentPreferences', kind: 'instant', group: 'consent', builderBehaviour: 'run',\n target: { required: true, types: ['Dialog'] } },\n\n // ── Services (D-A5: visible, greyed, with the reason — never hidden, never live) ──\n { type: 'signIn', labelKey: 'builder.actions.signIn', kind: 'server', group: 'service', builderBehaviour: 'inert',\n planned: true, entitlement: 'visitorIdentity' },\n { type: 'startCheckout', labelKey: 'builder.actions.startCheckout', kind: 'server', group: 'service', builderBehaviour: 'inert',\n planned: true, connector: 'payment', entitlement: 'commerce' },\n]);\n\n/** The Tier-A behaviour executors' dispatch set (A1c) — the engines branch on it beside\n * DIALOG/SWIPER. Dialog actions have their own set; context/visibility stay in the legacy path. */\nexport const BEHAVIOUR_ACTION_TYPES: ReadonlySet<string> =\n new Set(['navigate', 'scrollTo', 'submitForm', 'copyToClipboard']);\n\n/** The consent actions' dispatch set (C3) — the engines branch on it beside DIALOG/BEHAVIOUR.\n * `openConsentPreferences` is dialog-opening by nature and included so ONE branch owns consent. */\nexport const CONSENT_ACTION_TYPES: ReadonlySet<string> =\n new Set(['grantConsent', 'denyConsent', 'setConsentCategory', 'openConsentPreferences']);\n\n/** Picker order — most-used groups first, services last (they are the shop window, not the shop). */\nexport const ELEMENT_ACTION_GROUP_ORDER: readonly string[] =\n Object.freeze(['motion', 'visibility', 'navigate', 'dialog', 'form', 'data', 'consent', 'service']);\n\n/** The catalog, grouped and ordered for the picker — one derivation, no hand-kept second list. */\nexport function groupedElementActions(): ReadonlyArray<{ group: string; actions: ActionDescriptor[] }> {\n return ELEMENT_ACTION_GROUP_ORDER\n .map(group => ({ group, actions: ELEMENT_ACTION_DESCRIPTORS.filter(d => d.group === group) }))\n .filter(g => g.actions.length > 0);\n}\n","/**\n * @vectoriox/iox-ui/actions-core — the shared, FRAMEWORK-FREE action vocabulary.\n *\n * One `ActionDescriptor` shape for every ordered action list in the product: the forms\n * after-submit steps (whose catalog moved here; `forms-core` re-exports it unchanged) and\n * the element behaviour actions of A1. Framework-free for the same reason as `forms-core`\n * and `seo-core`: `iox-cms` consumes it under plain Node.\n * ⚠️ Consumers wire the usual trio — ng-package raw-ts assets, jest moduleNameMapper, and\n * tsconfig paths — or `nest start` dies with ERR_REQUIRE_ESM while jest and build pass.\n */\nexport * from './action.model';\nexport * from './availability';\nexport * from './element-actions';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
@@ -88,10 +88,91 @@ function decode(raw) {
88
88
  return null;
89
89
  }
90
90
  }
91
- /** `Set-Cookie` attributes for the decision: a NECESSARY cookie, 12 months, first-party. */
92
- function cookieAttributes(maxMonths = 12) {
91
+ /**
92
+ * `Set-Cookie` attributes for the decision: a NECESSARY cookie, 12 months, first-party.
93
+ * `secure` is a parameter because a Secure cookie is silently DROPPED on plain http — the
94
+ * engine passes `location.protocol === 'https:'` so dev servers and the e2e still record.
95
+ */
96
+ function cookieAttributes(maxMonths = 12, secure = true) {
93
97
  const maxAge = Math.floor(maxMonths * 30.44 * 24 * 60 * 60);
94
- return `Path=/; Max-Age=${maxAge}; SameSite=Lax; Secure`;
98
+ return `Path=/; Max-Age=${maxAge}; SameSite=Lax${secure ? '; Secure' : ''}`;
99
+ }
100
+
101
+ /**
102
+ * Decision construction for the banner's actions (consent plan C3). Pure on purpose: the builder
103
+ * canvas SIMULATES these (it is the admin's browser, not a visitor's), the engine executes them,
104
+ * and the e2e asserts the cookies they produce — one implementation of what each button means.
105
+ *
106
+ * `necessary` is always granted and never asked. A deny writes an EXPLICIT false per offered
107
+ * category — a recorded "no" is a decision (`needsPrompt` contract, C1), never an absence.
108
+ */
109
+ /** What an org offers when it has not chosen yet — the two non-necessary categories. */
110
+ const DEFAULT_OFFERED_CATEGORIES = Object.freeze([ConsentCategory.Analytics, ConsentCategory.Marketing]);
111
+ function base(anonymousId, policyVersion, now) {
112
+ return {
113
+ categories: { [ConsentCategory.Necessary]: true },
114
+ policyVersion,
115
+ decidedAt: now.toISOString(),
116
+ anonymousId,
117
+ };
118
+ }
119
+ /** "Accept all": every OFFERED category granted. */
120
+ function grantAllDecision(offered, anonymousId, policyVersion, now = new Date()) {
121
+ const decision = base(anonymousId, policyVersion, now);
122
+ for (const category of offered) {
123
+ if (category !== ConsentCategory.Necessary)
124
+ decision.categories[category] = true;
125
+ }
126
+ return decision;
127
+ }
128
+ /** "Reject all": every OFFERED category recorded as an explicit no. */
129
+ function denyAllDecision(offered, anonymousId, policyVersion, now = new Date()) {
130
+ const decision = base(anonymousId, policyVersion, now);
131
+ for (const category of offered) {
132
+ if (category !== ConsentCategory.Necessary)
133
+ decision.categories[category] = false;
134
+ }
135
+ return decision;
136
+ }
137
+ /**
138
+ * One category set explicitly (the preferences modal's per-category buttons). Carries every
139
+ * other category of the PREVIOUS decision forward; with no previous decision the rest stay
140
+ * unrecorded — the prompt logic still owes the visitor a first-layer ask.
141
+ */
142
+ function setCategoryDecision(previous, category, granted, anonymousId, policyVersion, now = new Date()) {
143
+ const decision = base(anonymousId, policyVersion, now);
144
+ if (previous?.categories) {
145
+ for (const [key, value] of Object.entries(previous.categories)) {
146
+ decision.categories[key] = value === true;
147
+ }
148
+ decision.categories[ConsentCategory.Necessary] = true;
149
+ }
150
+ if (category !== ConsentCategory.Necessary)
151
+ decision.categories[category] = granted;
152
+ return decision;
153
+ }
154
+ /** The window global the engine's runtime reads the config from. */
155
+ const CONSENT_CONFIG_GLOBAL = '__ioxConsentCfg';
156
+ /**
157
+ * The inline `<head>` embed carrying the config to the browser. Values are JSON-escaped and
158
+ * `<` is unicode-escaped so user-set strings (a policy version is author input) can never
159
+ * close the script tag.
160
+ */
161
+ function consentConfigJs(config) {
162
+ const json = JSON.stringify({
163
+ policyVersion: config.policyVersion,
164
+ categories: config.categories,
165
+ measurementId: config.measurementId ?? null,
166
+ policyRoute: config.policyRoute ?? null,
167
+ }).replace(/</g, '\\u003c');
168
+ return `window.${CONSENT_CONFIG_GLOBAL}=${json};`;
169
+ }
170
+ /** The gtag loader URL for a measurement id — one spelling for SSR injection and client inject. */
171
+ function gtagLoaderUrl(measurementId) {
172
+ const id = (measurementId ?? '').trim();
173
+ if (!id || !/^[A-Za-z0-9-]+$/.test(id))
174
+ return null;
175
+ return `https://www.googletagmanager.com/gtag/js?id=${id}`;
95
176
  }
96
177
 
97
178
  const CONSENT_MODE_PARAMS = [
@@ -127,13 +208,14 @@ function consentModeBootstrapJs() {
127
208
  */
128
209
  function analyticsInjectionHtml(measurementId, decision) {
129
210
  const id = (measurementId ?? '').trim();
130
- if (!id || !/^[A-Za-z0-9-]+$/.test(id))
211
+ const loader = gtagLoaderUrl(id);
212
+ if (!loader)
131
213
  return '';
132
214
  if (!hasConsent(decision, ConsentCategory.Analytics))
133
215
  return '';
134
216
  const signals = JSON.stringify(toConsentModeSignals(decision));
135
217
  return `<script>gtag('consent','update',${signals});</script>` +
136
- `<script async src="https://www.googletagmanager.com/gtag/js?id=${id}"></script>` +
218
+ `<script async src="${loader}"></script>` +
137
219
  `<script>gtag('js',new Date());gtag('config','${id}');</script>`;
138
220
  }
139
221
 
@@ -153,5 +235,5 @@ function analyticsInjectionHtml(measurementId, decision) {
153
235
  * Generated bundle index. Do not edit.
154
236
  */
155
237
 
156
- export { CONSENT_COOKIE_NAME, CONSENT_DEFAULTS_DENIED, CONSENT_MODE_PARAMS, ConsentCategory, analyticsInjectionHtml, consentModeBootstrapJs, cookieAttributes, decode, emptyDecision, encode, hasConsent, isExpired, needsPrompt, toConsentModeSignals };
238
+ export { CONSENT_CONFIG_GLOBAL, CONSENT_COOKIE_NAME, CONSENT_DEFAULTS_DENIED, CONSENT_MODE_PARAMS, ConsentCategory, DEFAULT_OFFERED_CATEGORIES, analyticsInjectionHtml, consentConfigJs, consentModeBootstrapJs, cookieAttributes, decode, denyAllDecision, emptyDecision, encode, grantAllDecision, gtagLoaderUrl, hasConsent, isExpired, needsPrompt, setCategoryDecision, toConsentModeSignals };
157
239
  //# sourceMappingURL=vectoriox-iox-ui-consent-core.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"vectoriox-iox-ui-consent-core.mjs","sources":["../../../projects/iox-ui/src/consent-core/consent.model.ts","../../../projects/iox-ui/src/consent-core/storage.ts","../../../projects/iox-ui/src/consent-core/consent-mode.ts","../../../projects/iox-ui/src/consent-core/public-api.ts","../../../projects/iox-ui/src/consent-core/vectoriox-iox-ui-consent-core.ts"],"sourcesContent":["/**\n * The visitor-consent vocabulary (consent plan C1). Framework-free for the same forced reason as\n * forms-core/seo-core/actions-core: iox-cms consumes it under plain Node, the engine at SSR time,\n * the builder for design-time preview — ONE implementation of what a decision is.\n */\n\nexport enum ConsentCategory {\n /** Always on, never user-controllable — the site working at all. */\n Necessary = 'necessary',\n Analytics = 'analytics',\n Marketing = 'marketing',\n}\n\n/** A visitor's recorded decision. `necessary` is implied true and never stored as false. */\nexport interface ConsentDecision {\n categories: Partial<Record<ConsentCategory, boolean>>;\n /** Bumping the org's policy version invalidates every prior decision. */\n policyVersion: string;\n /** ISO timestamp of the decision. */\n decidedAt: string;\n anonymousId: string;\n}\n\n/** A fresh \"nothing granted\" decision skeleton — used when a visitor must be asked. */\nexport function emptyDecision(anonymousId: string, policyVersion: string, now: Date = new Date()): ConsentDecision {\n return {\n categories: { [ConsentCategory.Necessary]: true },\n policyVersion,\n decidedAt: now.toISOString(),\n anonymousId,\n };\n}\n\n/** `null` (no decision yet) grants nothing; `necessary` is always granted. */\nexport function hasConsent(decision: ConsentDecision | null | undefined, category: ConsentCategory): boolean {\n if (category === ConsentCategory.Necessary) return true;\n if (!decision) return false;\n return decision.categories?.[category] === true;\n}\n\n/** A decision older than `maxMonths` no longer counts (default re-ask: 12 months, D-C6). */\nexport function isExpired(decision: ConsentDecision, now: Date, maxMonths: number): boolean {\n const decided = new Date(decision.decidedAt);\n if (Number.isNaN(decided.getTime())) return true;\n const expiry = new Date(decided);\n expiry.setMonth(expiry.getMonth() + maxMonths);\n return now >= expiry;\n}\n\n/**\n * The function the dead fork got wrong: **a recorded \"no\" is a decision.** The banner shows only\n * when there is no decision, the decision expired, or the policy version moved on — never because\n * the visitor declined.\n */\nexport function needsPrompt(\n decision: ConsentDecision | null | undefined,\n policyVersion: string,\n now: Date = new Date(),\n maxMonths = 12,\n): boolean {\n if (!decision) return true;\n if (decision.policyVersion !== policyVersion) return true;\n return isExpired(decision, now, maxMonths);\n}\n","import { ConsentCategory, ConsentDecision } from './consent.model';\n\n/** First-party cookie name — the visitor's browser carries the proof (D-C1). */\nexport const CONSENT_COOKIE_NAME = 'iox-consent';\n\n/** Cookie payload. Versioned so a future shape change can read old cookies tolerantly. */\nexport function encode(decision: ConsentDecision): string {\n return JSON.stringify({ v: 1, ...decision });\n}\n\n/**\n * Tolerant by contract: bad JSON, a foreign shape, a tampered cookie — all read as `null`\n * (\"no decision\"), never a throw and never a guess. The old fork threw its way to a re-prompt.\n */\nexport function decode(raw: string | null | undefined): ConsentDecision | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw);\n if (!parsed || typeof parsed !== 'object') return null;\n if (typeof parsed.anonymousId !== 'string' || !parsed.anonymousId) return null;\n if (typeof parsed.policyVersion !== 'string') return null;\n if (!parsed.categories || typeof parsed.categories !== 'object' || Array.isArray(parsed.categories)) return null;\n const categories: Partial<Record<ConsentCategory, boolean>> = {};\n for (const [k, v] of Object.entries(parsed.categories)) {\n categories[k as ConsentCategory] = v === true;\n }\n categories[ConsentCategory.Necessary] = true;\n return {\n categories,\n policyVersion: parsed.policyVersion,\n decidedAt: typeof parsed.decidedAt === 'string' ? parsed.decidedAt : '',\n anonymousId: parsed.anonymousId,\n };\n } catch {\n return null;\n }\n}\n\n/** `Set-Cookie` attributes for the decision: a NECESSARY cookie, 12 months, first-party. */\nexport function cookieAttributes(maxMonths = 12): string {\n const maxAge = Math.floor(maxMonths * 30.44 * 24 * 60 * 60);\n return `Path=/; Max-Age=${maxAge}; SameSite=Lax; Secure`;\n}\n","import { ConsentCategory, ConsentDecision, hasConsent } from './consent.model';\n\n/**\n * Google Consent Mode v2 (consent plan §4): four parameters, mandatory for EEA/UK measurement\n * since March 2024. Our categories map onto them; `necessary` maps to none.\n */\nexport type ConsentModeSignal = 'granted' | 'denied';\n\nexport const CONSENT_MODE_PARAMS = [\n 'analytics_storage', 'ad_storage', 'ad_user_data', 'ad_personalization',\n] as const;\n\n/** What the SSR head declares BEFORE any tag can load (invariant I1): everything denied. */\nexport const CONSENT_DEFAULTS_DENIED: Readonly<Record<(typeof CONSENT_MODE_PARAMS)[number], ConsentModeSignal>> =\n Object.freeze({\n analytics_storage: 'denied',\n ad_storage: 'denied',\n ad_user_data: 'denied',\n ad_personalization: 'denied',\n });\n\n/** A decision → the four v2 signals. `null` (undecided) is indistinguishable from denied. */\nexport function toConsentModeSignals(decision: ConsentDecision | null | undefined): Record<string, ConsentModeSignal> {\n const analytics: ConsentModeSignal = hasConsent(decision ?? null, ConsentCategory.Analytics) ? 'granted' : 'denied';\n const marketing: ConsentModeSignal = hasConsent(decision ?? null, ConsentCategory.Marketing) ? 'granted' : 'denied';\n return {\n analytics_storage: analytics,\n ad_storage: marketing,\n ad_user_data: marketing,\n ad_personalization: marketing,\n };\n}\n\n/** The inline `<head>` bootstrap (basic mode, D-C2): defaults denied before anything loads. */\nexport function consentModeBootstrapJs(): string {\n return \"window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}\" +\n `gtag('consent','default',${JSON.stringify(CONSENT_DEFAULTS_DENIED)});`;\n}\n\n/**\n * The gated injection, as HTML (C2). Emitted by the SSR server ONLY when the request's cookie\n * grants analytics AND the org has a measurementId — the bootstrap above is unconditional, this\n * is not. Order inside: consent UPDATE before the loader, config after it.\n */\nexport function analyticsInjectionHtml(measurementId: string, decision: ConsentDecision): string {\n const id = (measurementId ?? '').trim();\n if (!id || !/^[A-Za-z0-9-]+$/.test(id)) return '';\n if (!hasConsent(decision, ConsentCategory.Analytics)) return '';\n const signals = JSON.stringify(toConsentModeSignals(decision));\n return `<script>gtag('consent','update',${signals});</script>` +\n `<script async src=\"https://www.googletagmanager.com/gtag/js?id=${id}\"></script>` +\n `<script>gtag('js',new Date());gtag('config','${id}');</script>`;\n}\n","/**\n * @vectoriox/iox-ui/consent-core — the shared, FRAMEWORK-FREE core of visitor consent.\n *\n * What a decision IS (`needsPrompt`: a recorded \"no\" is a decision), how it travels (a tolerant\n * first-party cookie), and what it tells Google (Consent Mode v2 signals, denied-by-default).\n * Consumed by the SSR engine (the gate, C2), the builder (banner preview, C3) and iox-cms (the\n * optional per-org log, C4) — the fourth entry point on the forms-core pattern, replacing the\n * dead byte-identical fork C0 deleted.\n * ⚠️ Consumers wire the usual trio — ng-package raw-ts assets, jest moduleNameMapper, tsconfig\n * paths — or `nest start` dies with ERR_REQUIRE_ESM while jest and build both pass.\n */\nexport * from './consent.model';\nexport * from './storage';\nexport * from './consent-mode';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"vectoriox-iox-ui-consent-core.mjs","sources":["../../../projects/iox-ui/src/consent-core/consent.model.ts","../../../projects/iox-ui/src/consent-core/storage.ts","../../../projects/iox-ui/src/consent-core/decisions.ts","../../../projects/iox-ui/src/consent-core/consent-mode.ts","../../../projects/iox-ui/src/consent-core/public-api.ts","../../../projects/iox-ui/src/consent-core/vectoriox-iox-ui-consent-core.ts"],"sourcesContent":["/**\n * The visitor-consent vocabulary (consent plan C1). Framework-free for the same forced reason as\n * forms-core/seo-core/actions-core: iox-cms consumes it under plain Node, the engine at SSR time,\n * the builder for design-time preview — ONE implementation of what a decision is.\n */\n\nexport enum ConsentCategory {\n /** Always on, never user-controllable — the site working at all. */\n Necessary = 'necessary',\n Analytics = 'analytics',\n Marketing = 'marketing',\n}\n\n/** A visitor's recorded decision. `necessary` is implied true and never stored as false. */\nexport interface ConsentDecision {\n categories: Partial<Record<ConsentCategory, boolean>>;\n /** Bumping the org's policy version invalidates every prior decision. */\n policyVersion: string;\n /** ISO timestamp of the decision. */\n decidedAt: string;\n anonymousId: string;\n}\n\n/** A fresh \"nothing granted\" decision skeleton — used when a visitor must be asked. */\nexport function emptyDecision(anonymousId: string, policyVersion: string, now: Date = new Date()): ConsentDecision {\n return {\n categories: { [ConsentCategory.Necessary]: true },\n policyVersion,\n decidedAt: now.toISOString(),\n anonymousId,\n };\n}\n\n/** `null` (no decision yet) grants nothing; `necessary` is always granted. */\nexport function hasConsent(decision: ConsentDecision | null | undefined, category: ConsentCategory): boolean {\n if (category === ConsentCategory.Necessary) return true;\n if (!decision) return false;\n return decision.categories?.[category] === true;\n}\n\n/** A decision older than `maxMonths` no longer counts (default re-ask: 12 months, D-C6). */\nexport function isExpired(decision: ConsentDecision, now: Date, maxMonths: number): boolean {\n const decided = new Date(decision.decidedAt);\n if (Number.isNaN(decided.getTime())) return true;\n const expiry = new Date(decided);\n expiry.setMonth(expiry.getMonth() + maxMonths);\n return now >= expiry;\n}\n\n/**\n * The function the dead fork got wrong: **a recorded \"no\" is a decision.** The banner shows only\n * when there is no decision, the decision expired, or the policy version moved on — never because\n * the visitor declined.\n */\nexport function needsPrompt(\n decision: ConsentDecision | null | undefined,\n policyVersion: string,\n now: Date = new Date(),\n maxMonths = 12,\n): boolean {\n if (!decision) return true;\n if (decision.policyVersion !== policyVersion) return true;\n return isExpired(decision, now, maxMonths);\n}\n","import { ConsentCategory, ConsentDecision } from './consent.model';\n\n/** First-party cookie name — the visitor's browser carries the proof (D-C1). */\nexport const CONSENT_COOKIE_NAME = 'iox-consent';\n\n/** Cookie payload. Versioned so a future shape change can read old cookies tolerantly. */\nexport function encode(decision: ConsentDecision): string {\n return JSON.stringify({ v: 1, ...decision });\n}\n\n/**\n * Tolerant by contract: bad JSON, a foreign shape, a tampered cookie — all read as `null`\n * (\"no decision\"), never a throw and never a guess. The old fork threw its way to a re-prompt.\n */\nexport function decode(raw: string | null | undefined): ConsentDecision | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw);\n if (!parsed || typeof parsed !== 'object') return null;\n if (typeof parsed.anonymousId !== 'string' || !parsed.anonymousId) return null;\n if (typeof parsed.policyVersion !== 'string') return null;\n if (!parsed.categories || typeof parsed.categories !== 'object' || Array.isArray(parsed.categories)) return null;\n const categories: Partial<Record<ConsentCategory, boolean>> = {};\n for (const [k, v] of Object.entries(parsed.categories)) {\n categories[k as ConsentCategory] = v === true;\n }\n categories[ConsentCategory.Necessary] = true;\n return {\n categories,\n policyVersion: parsed.policyVersion,\n decidedAt: typeof parsed.decidedAt === 'string' ? parsed.decidedAt : '',\n anonymousId: parsed.anonymousId,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * `Set-Cookie` attributes for the decision: a NECESSARY cookie, 12 months, first-party.\n * `secure` is a parameter because a Secure cookie is silently DROPPED on plain http — the\n * engine passes `location.protocol === 'https:'` so dev servers and the e2e still record.\n */\nexport function cookieAttributes(maxMonths = 12, secure = true): string {\n const maxAge = Math.floor(maxMonths * 30.44 * 24 * 60 * 60);\n return `Path=/; Max-Age=${maxAge}; SameSite=Lax${secure ? '; Secure' : ''}`;\n}\n","import { ConsentCategory, ConsentDecision } from './consent.model';\n\n/**\n * Decision construction for the banner's actions (consent plan C3). Pure on purpose: the builder\n * canvas SIMULATES these (it is the admin's browser, not a visitor's), the engine executes them,\n * and the e2e asserts the cookies they produce — one implementation of what each button means.\n *\n * `necessary` is always granted and never asked. A deny writes an EXPLICIT false per offered\n * category — a recorded \"no\" is a decision (`needsPrompt` contract, C1), never an absence.\n */\n\n/** What an org offers when it has not chosen yet — the two non-necessary categories. */\nexport const DEFAULT_OFFERED_CATEGORIES: readonly ConsentCategory[] =\n Object.freeze([ConsentCategory.Analytics, ConsentCategory.Marketing]);\n\nfunction base(anonymousId: string, policyVersion: string, now: Date): ConsentDecision {\n return {\n categories: { [ConsentCategory.Necessary]: true },\n policyVersion,\n decidedAt: now.toISOString(),\n anonymousId,\n };\n}\n\n/** \"Accept all\": every OFFERED category granted. */\nexport function grantAllDecision(\n offered: readonly ConsentCategory[],\n anonymousId: string,\n policyVersion: string,\n now: Date = new Date(),\n): ConsentDecision {\n const decision = base(anonymousId, policyVersion, now);\n for (const category of offered) {\n if (category !== ConsentCategory.Necessary) decision.categories[category] = true;\n }\n return decision;\n}\n\n/** \"Reject all\": every OFFERED category recorded as an explicit no. */\nexport function denyAllDecision(\n offered: readonly ConsentCategory[],\n anonymousId: string,\n policyVersion: string,\n now: Date = new Date(),\n): ConsentDecision {\n const decision = base(anonymousId, policyVersion, now);\n for (const category of offered) {\n if (category !== ConsentCategory.Necessary) decision.categories[category] = false;\n }\n return decision;\n}\n\n/**\n * One category set explicitly (the preferences modal's per-category buttons). Carries every\n * other category of the PREVIOUS decision forward; with no previous decision the rest stay\n * unrecorded — the prompt logic still owes the visitor a first-layer ask.\n */\nexport function setCategoryDecision(\n previous: ConsentDecision | null | undefined,\n category: ConsentCategory,\n granted: boolean,\n anonymousId: string,\n policyVersion: string,\n now: Date = new Date(),\n): ConsentDecision {\n const decision = base(anonymousId, policyVersion, now);\n if (previous?.categories) {\n for (const [key, value] of Object.entries(previous.categories)) {\n decision.categories[key as ConsentCategory] = value === true;\n }\n decision.categories[ConsentCategory.Necessary] = true;\n }\n if (category !== ConsentCategory.Necessary) decision.categories[category] = granted;\n return decision;\n}\n\n/** The per-site config the SSR server embeds for the prompt + the post-grant loader (C3). */\nexport interface ConsentSiteConfig {\n policyVersion: string;\n /** The categories the org offers beyond `necessary`. */\n categories: readonly ConsentCategory[];\n /** Site-tag id so a grant can inject the loader WITHOUT a reload; null = none configured. */\n measurementId?: string | null;\n /** Route of the org's policy page, for the banner's link; null = not chosen (C5 blocks publish). */\n policyRoute?: string | null;\n}\n\n/** The window global the engine's runtime reads the config from. */\nexport const CONSENT_CONFIG_GLOBAL = '__ioxConsentCfg';\n\n/**\n * The inline `<head>` embed carrying the config to the browser. Values are JSON-escaped and\n * `<` is unicode-escaped so user-set strings (a policy version is author input) can never\n * close the script tag.\n */\nexport function consentConfigJs(config: ConsentSiteConfig): string {\n const json = JSON.stringify({\n policyVersion: config.policyVersion,\n categories: config.categories,\n measurementId: config.measurementId ?? null,\n policyRoute: config.policyRoute ?? null,\n }).replace(/</g, '\\\\u003c');\n return `window.${CONSENT_CONFIG_GLOBAL}=${json};`;\n}\n\n/** The gtag loader URL for a measurement id — one spelling for SSR injection and client inject. */\nexport function gtagLoaderUrl(measurementId: string): string | null {\n const id = (measurementId ?? '').trim();\n if (!id || !/^[A-Za-z0-9-]+$/.test(id)) return null;\n return `https://www.googletagmanager.com/gtag/js?id=${id}`;\n}\n","import { ConsentCategory, ConsentDecision, hasConsent } from './consent.model';\nimport { gtagLoaderUrl } from './decisions';\n\n/**\n * Google Consent Mode v2 (consent plan §4): four parameters, mandatory for EEA/UK measurement\n * since March 2024. Our categories map onto them; `necessary` maps to none.\n */\nexport type ConsentModeSignal = 'granted' | 'denied';\n\nexport const CONSENT_MODE_PARAMS = [\n 'analytics_storage', 'ad_storage', 'ad_user_data', 'ad_personalization',\n] as const;\n\n/** What the SSR head declares BEFORE any tag can load (invariant I1): everything denied. */\nexport const CONSENT_DEFAULTS_DENIED: Readonly<Record<(typeof CONSENT_MODE_PARAMS)[number], ConsentModeSignal>> =\n Object.freeze({\n analytics_storage: 'denied',\n ad_storage: 'denied',\n ad_user_data: 'denied',\n ad_personalization: 'denied',\n });\n\n/** A decision → the four v2 signals. `null` (undecided) is indistinguishable from denied. */\nexport function toConsentModeSignals(decision: ConsentDecision | null | undefined): Record<string, ConsentModeSignal> {\n const analytics: ConsentModeSignal = hasConsent(decision ?? null, ConsentCategory.Analytics) ? 'granted' : 'denied';\n const marketing: ConsentModeSignal = hasConsent(decision ?? null, ConsentCategory.Marketing) ? 'granted' : 'denied';\n return {\n analytics_storage: analytics,\n ad_storage: marketing,\n ad_user_data: marketing,\n ad_personalization: marketing,\n };\n}\n\n/** The inline `<head>` bootstrap (basic mode, D-C2): defaults denied before anything loads. */\nexport function consentModeBootstrapJs(): string {\n return \"window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}\" +\n `gtag('consent','default',${JSON.stringify(CONSENT_DEFAULTS_DENIED)});`;\n}\n\n/**\n * The gated injection, as HTML (C2). Emitted by the SSR server ONLY when the request's cookie\n * grants analytics AND the org has a measurementId — the bootstrap above is unconditional, this\n * is not. Order inside: consent UPDATE before the loader, config after it.\n */\nexport function analyticsInjectionHtml(measurementId: string, decision: ConsentDecision): string {\n const id = (measurementId ?? '').trim();\n const loader = gtagLoaderUrl(id);\n if (!loader) return '';\n if (!hasConsent(decision, ConsentCategory.Analytics)) return '';\n const signals = JSON.stringify(toConsentModeSignals(decision));\n return `<script>gtag('consent','update',${signals});</script>` +\n `<script async src=\"${loader}\"></script>` +\n `<script>gtag('js',new Date());gtag('config','${id}');</script>`;\n}\n","/**\n * @vectoriox/iox-ui/consent-core — the shared, FRAMEWORK-FREE core of visitor consent.\n *\n * What a decision IS (`needsPrompt`: a recorded \"no\" is a decision), how it travels (a tolerant\n * first-party cookie), and what it tells Google (Consent Mode v2 signals, denied-by-default).\n * Consumed by the SSR engine (the gate, C2), the builder (banner preview, C3) and iox-cms (the\n * optional per-org log, C4) — the fourth entry point on the forms-core pattern, replacing the\n * dead byte-identical fork C0 deleted.\n * ⚠️ Consumers wire the usual trio — ng-package raw-ts assets, jest moduleNameMapper, tsconfig\n * paths — or `nest start` dies with ERR_REQUIRE_ESM while jest and build both pass.\n */\nexport * from './consent.model';\nexport * from './storage';\nexport * from './consent-mode';\nexport * from './decisions';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.5.2",
3
+ "version": "5.6.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
  }