@aglyn/plugins-marketing 1.0.0-beta.229 → 1.0.0-beta.231

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.
Files changed (51) hide show
  1. package/README.md +2 -2
  2. package/package.json +16 -15
  3. package/src/lib/components/campaign-conversions-card.js +23 -11
  4. package/src/lib/components/campaign-conversions-card.js.map +1 -1
  5. package/src/lib/components/campaign-list-zones.d.ts +33 -0
  6. package/src/lib/components/campaign-list-zones.js +30 -0
  7. package/src/lib/components/campaign-list-zones.js.map +1 -0
  8. package/src/lib/components/campaign-report-card.js +20 -3
  9. package/src/lib/components/campaign-report-card.js.map +1 -1
  10. package/src/lib/components/campaigns-card.js +42 -13
  11. package/src/lib/components/campaigns-card.js.map +1 -1
  12. package/src/lib/components/experiment-zones.d.ts +11 -0
  13. package/src/lib/components/experiment-zones.js.map +1 -1
  14. package/src/lib/components/host-experiments-card.component.js +5 -1
  15. package/src/lib/components/host-experiments-card.component.js.map +1 -1
  16. package/src/lib/components/host-overlays-card.component.js +139 -24
  17. package/src/lib/components/host-overlays-card.component.js.map +1 -1
  18. package/src/lib/components/marketing-insight-zones.d.ts +28 -0
  19. package/src/lib/components/marketing-insight-zones.js +30 -0
  20. package/src/lib/components/marketing-insight-zones.js.map +1 -0
  21. package/src/lib/components/overlay-zones.d.ts +84 -0
  22. package/src/lib/components/overlay-zones.js +38 -0
  23. package/src/lib/components/overlay-zones.js.map +1 -0
  24. package/src/lib/components/site-runtime.js +10 -1
  25. package/src/lib/components/site-runtime.js.map +1 -1
  26. package/src/lib/model/campaign-list-query.d.ts +2 -2
  27. package/src/lib/model/campaign-list-query.js.map +1 -1
  28. package/src/lib/model/index.d.ts +1 -0
  29. package/src/lib/model/index.js +1 -0
  30. package/src/lib/model/index.js.map +1 -1
  31. package/src/lib/model/overlay-drafts.d.ts +97 -0
  32. package/src/lib/model/overlay-drafts.js +192 -0
  33. package/src/lib/model/overlay-drafts.js.map +1 -0
  34. package/src/lib/plugin.js +12 -0
  35. package/src/lib/plugin.js.map +1 -1
  36. package/src/lib/server/email-events.js +8 -0
  37. package/src/lib/server/email-events.js.map +1 -1
  38. package/src/lib/server/marketing-ai-capabilities.d.ts +24 -0
  39. package/src/lib/server/marketing-ai-capabilities.js +225 -0
  40. package/src/lib/server/marketing-ai-capabilities.js.map +1 -0
  41. package/src/lib/server/marketing-drafts.d.ts +86 -0
  42. package/src/lib/server/marketing-drafts.js +354 -0
  43. package/src/lib/server/marketing-drafts.js.map +1 -0
  44. package/src/lib/server/marketing-figures.d.ts +18 -0
  45. package/src/lib/server/marketing-figures.js +259 -1
  46. package/src/lib/server/marketing-figures.js.map +1 -1
  47. package/src/lib/server/overlay-figures.d.ts +36 -0
  48. package/src/lib/server/overlay-figures.js +158 -0
  49. package/src/lib/server/overlay-figures.js.map +1 -0
  50. package/src/lib/server.js +10 -0
  51. package/src/lib/server.js.map +1 -1
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { HostOverlay } from './overlays';
18
+ /**
19
+ * Copy proposed for an announcement bar or a popup by something other than a
20
+ * person typing it — a widget in the overlay zones — and the rules it is held
21
+ * to before it reaches an overlay.
22
+ *
23
+ * A proposal is never trusted to be the right length or to name a trigger the
24
+ * site runtime knows: {@link overlayCopyFromProposal} cuts every field to
25
+ * {@link OVERLAY_COPY_LIMITS} and keeps a trigger only when it is one of
26
+ * {@link OVERLAY_POPUP_TRIGGERS}, with its value inside that trigger's range.
27
+ * Links are never proposed: a call to action's destination is a page of the
28
+ * person's own site, which they pick.
29
+ */
30
+ /**
31
+ * How many overlays a site's list reads and can reorder (AGL-2501), and so
32
+ * the most a site keeps before something else may add one (AGL-3616).
33
+ */
34
+ export declare const OVERLAY_LIST_CEILING = 50;
35
+ /** The longest each overlay field a proposal fills may be, in characters. */
36
+ export declare const OVERLAY_COPY_LIMITS: {
37
+ /** The internal name shown in the overlays list. */
38
+ readonly name: 80;
39
+ /** An announcement bar's one line. */
40
+ readonly text: 160;
41
+ /** A popup's headline. */
42
+ readonly headline: 90;
43
+ /** A popup's body. */
44
+ readonly body: 400;
45
+ /** A popup's button label; the single popup card cuts the label to the same. */
46
+ readonly ctaLabel: 60;
47
+ };
48
+ export type OverlayCopyField = keyof typeof OVERLAY_COPY_LIMITS;
49
+ /** One way a popup opens, with the unit and the range of the value it reads. */
50
+ export interface OverlayPopupTrigger {
51
+ id: NonNullable<NonNullable<HostOverlay['popup']>['trigger']>;
52
+ /** How the editor names it. */
53
+ label: string;
54
+ /** What `triggerValue` counts, or `null` for a trigger that reads none. */
55
+ unit: 'seconds' | 'percent' | null;
56
+ min: number;
57
+ max: number;
58
+ }
59
+ /**
60
+ * Every trigger the site runtime opens a popup on: after a delay, once the
61
+ * visitor has scrolled a share of the page, or as they move to leave it.
62
+ */
63
+ export declare const OVERLAY_POPUP_TRIGGERS: readonly OverlayPopupTrigger[];
64
+ /** What a widget proposes for one overlay. Every field is optional. */
65
+ export interface OverlayCopyProposal {
66
+ name?: string;
67
+ /** An announcement bar's text. */
68
+ text?: string;
69
+ headline?: string;
70
+ body?: string;
71
+ ctaLabel?: string;
72
+ trigger?: string;
73
+ triggerValue?: number;
74
+ }
75
+ /** A proposal as it may be written: cut to the limits, with a known trigger or none. */
76
+ export type OverlayCopyValues = Omit<OverlayCopyProposal, 'trigger'> & {
77
+ trigger?: OverlayPopupTrigger['id'];
78
+ };
79
+ /**
80
+ * A proposal held to the overlay's own rules, keeping only the fields the
81
+ * overlay's kind has: a bar has text, a popup a headline, a body, a button
82
+ * label and a trigger.
83
+ */
84
+ export declare function overlayCopyFromProposal(kind: HostOverlay['kind'], proposal: OverlayCopyProposal): OverlayCopyValues;
85
+ /**
86
+ * A new overlay from a proposal, written SWITCHED OFF: a visitor sees nothing
87
+ * until a person reads it and turns it on. `null` when the proposal has
88
+ * nothing the overlay needs to exist — a bar's text, a popup's body.
89
+ */
90
+ export declare function overlayDraftFromProposal(kind: HostOverlay['kind'], proposal: OverlayCopyProposal): HostOverlay | null;
91
+ /**
92
+ * What is wrong with an overlay draft's content as another plugin hands it
93
+ * over (AGL-3616), held to the same limits and trigger catalog a proposal is,
94
+ * but REFUSED rather than cut: a caller that writes a draft through the
95
+ * marketing plugin's writer is told what to fix. Empty when nothing is.
96
+ */
97
+ export declare function overlayDraftContentProblems(content: Readonly<Record<string, unknown>>): string[];
@@ -0,0 +1,192 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ /**
18
+ * Copy proposed for an announcement bar or a popup by something other than a
19
+ * person typing it — a widget in the overlay zones — and the rules it is held
20
+ * to before it reaches an overlay.
21
+ *
22
+ * A proposal is never trusted to be the right length or to name a trigger the
23
+ * site runtime knows: {@link overlayCopyFromProposal} cuts every field to
24
+ * {@link OVERLAY_COPY_LIMITS} and keeps a trigger only when it is one of
25
+ * {@link OVERLAY_POPUP_TRIGGERS}, with its value inside that trigger's range.
26
+ * Links are never proposed: a call to action's destination is a page of the
27
+ * person's own site, which they pick.
28
+ */ /**
29
+ * How many overlays a site's list reads and can reorder (AGL-2501), and so
30
+ * the most a site keeps before something else may add one (AGL-3616).
31
+ */ export const OVERLAY_LIST_CEILING = 50;
32
+ /** The longest each overlay field a proposal fills may be, in characters. */ export const OVERLAY_COPY_LIMITS = {
33
+ /** The internal name shown in the overlays list. */ name: 80,
34
+ /** An announcement bar's one line. */ text: 160,
35
+ /** A popup's headline. */ headline: 90,
36
+ /** A popup's body. */ body: 400,
37
+ /** A popup's button label; the single popup card cuts the label to the same. */ ctaLabel: 60
38
+ };
39
+ /**
40
+ * Every trigger the site runtime opens a popup on: after a delay, once the
41
+ * visitor has scrolled a share of the page, or as they move to leave it.
42
+ */ export const OVERLAY_POPUP_TRIGGERS = [
43
+ {
44
+ id: 'delay',
45
+ label: 'After a delay',
46
+ unit: 'seconds',
47
+ min: 0,
48
+ max: 120
49
+ },
50
+ {
51
+ id: 'scroll',
52
+ label: 'On scroll',
53
+ unit: 'percent',
54
+ min: 1,
55
+ max: 100
56
+ },
57
+ {
58
+ id: 'exit',
59
+ label: 'On exit intent',
60
+ unit: null,
61
+ min: 0,
62
+ max: 0
63
+ }
64
+ ];
65
+ /** One line of copy, on one line, cut to its field's limit. */ function clean(value, field, multiline = false) {
66
+ if (typeof value !== 'string') return undefined;
67
+ const folded = multiline ? value.replace(/\r\n?/g, '\n').replace(/[^\S\n]+/g, ' ').replace(/\n{3,}/g, '\n\n') : value.replace(/\s+/g, ' ');
68
+ const trimmed = folded.trim().slice(0, OVERLAY_COPY_LIMITS[field]).trim();
69
+ return trimmed || undefined;
70
+ }
71
+ /**
72
+ * A proposal held to the overlay's own rules, keeping only the fields the
73
+ * overlay's kind has: a bar has text, a popup a headline, a body, a button
74
+ * label and a trigger.
75
+ */ export function overlayCopyFromProposal(kind, proposal) {
76
+ const values = {};
77
+ const name = clean(proposal.name, 'name');
78
+ if (name) values.name = name;
79
+ if (kind === 'bar') {
80
+ const text = clean(proposal.text, 'text');
81
+ if (text) values.text = text;
82
+ return values;
83
+ }
84
+ const headline = clean(proposal.headline, 'headline');
85
+ const body = clean(proposal.body, 'body', true);
86
+ const ctaLabel = clean(proposal.ctaLabel, 'ctaLabel');
87
+ if (headline) values.headline = headline;
88
+ if (body) values.body = body;
89
+ if (ctaLabel) values.ctaLabel = ctaLabel;
90
+ const trigger = OVERLAY_POPUP_TRIGGERS.find((entry)=>entry.id === proposal.trigger);
91
+ if (trigger) {
92
+ values.trigger = trigger.id;
93
+ if (trigger.unit) {
94
+ const raw = Number(proposal.triggerValue);
95
+ if (Number.isFinite(raw)) {
96
+ values.triggerValue = Math.min(trigger.max, Math.max(trigger.min, Math.round(raw)));
97
+ }
98
+ }
99
+ }
100
+ return values;
101
+ }
102
+ /**
103
+ * A new overlay from a proposal, written SWITCHED OFF: a visitor sees nothing
104
+ * until a person reads it and turns it on. `null` when the proposal has
105
+ * nothing the overlay needs to exist — a bar's text, a popup's body.
106
+ */ export function overlayDraftFromProposal(kind, proposal) {
107
+ var _values_trigger, _values_triggerValue;
108
+ const values = overlayCopyFromProposal(kind, proposal);
109
+ if (kind === 'bar') {
110
+ if (!values.text) return null;
111
+ return _extends({
112
+ kind: 'bar',
113
+ enabled: false
114
+ }, values.name ? {
115
+ name: values.name
116
+ } : {}, {
117
+ bar: {
118
+ text: values.text,
119
+ dismissible: true
120
+ }
121
+ });
122
+ }
123
+ if (!values.body) return null;
124
+ const trigger = (_values_trigger = values.trigger) != null ? _values_trigger : 'delay';
125
+ return _extends({
126
+ kind: 'popup',
127
+ enabled: false
128
+ }, values.name ? {
129
+ name: values.name
130
+ } : {}, {
131
+ popup: _extends({}, values.headline ? {
132
+ headline: values.headline
133
+ } : {}, {
134
+ body: values.body
135
+ }, values.ctaLabel ? {
136
+ ctaLabel: values.ctaLabel
137
+ } : {}, {
138
+ trigger
139
+ }, trigger === 'exit' ? {} : {
140
+ triggerValue: (_values_triggerValue = values.triggerValue) != null ? _values_triggerValue : 3
141
+ }, {
142
+ frequencyDays: 7
143
+ })
144
+ });
145
+ }
146
+ /**
147
+ * What is wrong with an overlay draft's content as another plugin hands it
148
+ * over (AGL-3616), held to the same limits and trigger catalog a proposal is,
149
+ * but REFUSED rather than cut: a caller that writes a draft through the
150
+ * marketing plugin's writer is told what to fix. Empty when nothing is.
151
+ */ export function overlayDraftContentProblems(content) {
152
+ const problems = [];
153
+ const kind = content['kind'];
154
+ if (kind !== 'bar' && kind !== 'popup') return [
155
+ 'An overlay is a bar or a popup'
156
+ ];
157
+ const fields = kind === 'bar' ? [
158
+ 'name',
159
+ 'text'
160
+ ] : [
161
+ 'name',
162
+ 'headline',
163
+ 'body',
164
+ 'ctaLabel'
165
+ ];
166
+ for (const field of fields){
167
+ const value = content[field];
168
+ if (value === undefined || value === null) continue;
169
+ if (typeof value !== 'string') problems.push(`The ${field} must be text`);
170
+ else if (value.trim().length > OVERLAY_COPY_LIMITS[field]) {
171
+ problems.push(`The ${field} is longer than ${OVERLAY_COPY_LIMITS[field]} characters`);
172
+ }
173
+ }
174
+ const required = kind === 'bar' ? 'text' : 'body';
175
+ if (typeof content[required] !== 'string' || !String(content[required]).trim()) {
176
+ problems.push(kind === 'bar' ? 'An announcement bar needs its text' : 'A popup needs its body');
177
+ }
178
+ if (kind === 'popup' && content['trigger'] !== undefined && content['trigger'] !== null) {
179
+ const trigger = OVERLAY_POPUP_TRIGGERS.find((entry)=>entry.id === content['trigger']);
180
+ if (!trigger) {
181
+ problems.push(`A popup opens ${OVERLAY_POPUP_TRIGGERS.map((entry)=>entry.id).join(', ')}`);
182
+ } else if (trigger.unit && content['triggerValue'] !== undefined && content['triggerValue'] !== null) {
183
+ const value = Number(content['triggerValue']);
184
+ if (!Number.isFinite(value) || value < trigger.min || value > trigger.max) {
185
+ problems.push(`The ${trigger.label.toLowerCase()} value must be from ${trigger.min} to ${trigger.max}`);
186
+ }
187
+ }
188
+ }
189
+ return problems;
190
+ }
191
+
192
+ //# sourceMappingURL=overlay-drafts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/overlay-drafts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostOverlay } from './overlays'\n\n/**\n * Copy proposed for an announcement bar or a popup by something other than a\n * person typing it — a widget in the overlay zones — and the rules it is held\n * to before it reaches an overlay.\n *\n * A proposal is never trusted to be the right length or to name a trigger the\n * site runtime knows: {@link overlayCopyFromProposal} cuts every field to\n * {@link OVERLAY_COPY_LIMITS} and keeps a trigger only when it is one of\n * {@link OVERLAY_POPUP_TRIGGERS}, with its value inside that trigger's range.\n * Links are never proposed: a call to action's destination is a page of the\n * person's own site, which they pick.\n */\n\n/**\n * How many overlays a site's list reads and can reorder (AGL-2501), and so\n * the most a site keeps before something else may add one (AGL-3616).\n */\nexport const OVERLAY_LIST_CEILING = 50\n\n/** The longest each overlay field a proposal fills may be, in characters. */\nexport const OVERLAY_COPY_LIMITS = {\n /** The internal name shown in the overlays list. */\n name: 80,\n /** An announcement bar's one line. */\n text: 160,\n /** A popup's headline. */\n headline: 90,\n /** A popup's body. */\n body: 400,\n /** A popup's button label; the single popup card cuts the label to the same. */\n ctaLabel: 60,\n} as const\n\nexport type OverlayCopyField = keyof typeof OVERLAY_COPY_LIMITS\n\n/** One way a popup opens, with the unit and the range of the value it reads. */\nexport interface OverlayPopupTrigger {\n id: NonNullable<NonNullable<HostOverlay['popup']>['trigger']>\n /** How the editor names it. */\n label: string\n /** What `triggerValue` counts, or `null` for a trigger that reads none. */\n unit: 'seconds' | 'percent' | null\n min: number\n max: number\n}\n\n/**\n * Every trigger the site runtime opens a popup on: after a delay, once the\n * visitor has scrolled a share of the page, or as they move to leave it.\n */\nexport const OVERLAY_POPUP_TRIGGERS: readonly OverlayPopupTrigger[] = [\n { id: 'delay', label: 'After a delay', unit: 'seconds', min: 0, max: 120 },\n { id: 'scroll', label: 'On scroll', unit: 'percent', min: 1, max: 100 },\n { id: 'exit', label: 'On exit intent', unit: null, min: 0, max: 0 },\n]\n\n/** What a widget proposes for one overlay. Every field is optional. */\nexport interface OverlayCopyProposal {\n name?: string\n /** An announcement bar's text. */\n text?: string\n headline?: string\n body?: string\n ctaLabel?: string\n trigger?: string\n triggerValue?: number\n}\n\n/** A proposal as it may be written: cut to the limits, with a known trigger or none. */\nexport type OverlayCopyValues = Omit<OverlayCopyProposal, 'trigger'> & {\n trigger?: OverlayPopupTrigger['id']\n}\n\n/** One line of copy, on one line, cut to its field's limit. */\nfunction clean(value: unknown, field: OverlayCopyField, multiline = false): string | undefined {\n if (typeof value !== 'string') return undefined\n const folded = multiline\n ? value.replace(/\\r\\n?/g, '\\n').replace(/[^\\S\\n]+/g, ' ').replace(/\\n{3,}/g, '\\n\\n')\n : value.replace(/\\s+/g, ' ')\n const trimmed = folded.trim().slice(0, OVERLAY_COPY_LIMITS[field]).trim()\n return trimmed || undefined\n}\n\n/**\n * A proposal held to the overlay's own rules, keeping only the fields the\n * overlay's kind has: a bar has text, a popup a headline, a body, a button\n * label and a trigger.\n */\nexport function overlayCopyFromProposal(\n kind: HostOverlay['kind'],\n proposal: OverlayCopyProposal,\n): OverlayCopyValues {\n const values: OverlayCopyValues = {}\n const name = clean(proposal.name, 'name')\n if (name) values.name = name\n if (kind === 'bar') {\n const text = clean(proposal.text, 'text')\n if (text) values.text = text\n return values\n }\n const headline = clean(proposal.headline, 'headline')\n const body = clean(proposal.body, 'body', true)\n const ctaLabel = clean(proposal.ctaLabel, 'ctaLabel')\n if (headline) values.headline = headline\n if (body) values.body = body\n if (ctaLabel) values.ctaLabel = ctaLabel\n const trigger = OVERLAY_POPUP_TRIGGERS.find((entry) => entry.id === proposal.trigger)\n if (trigger) {\n values.trigger = trigger.id\n if (trigger.unit) {\n const raw = Number(proposal.triggerValue)\n if (Number.isFinite(raw)) {\n values.triggerValue = Math.min(trigger.max, Math.max(trigger.min, Math.round(raw)))\n }\n }\n }\n return values\n}\n\n/**\n * A new overlay from a proposal, written SWITCHED OFF: a visitor sees nothing\n * until a person reads it and turns it on. `null` when the proposal has\n * nothing the overlay needs to exist — a bar's text, a popup's body.\n */\nexport function overlayDraftFromProposal(\n kind: HostOverlay['kind'],\n proposal: OverlayCopyProposal,\n): HostOverlay | null {\n const values = overlayCopyFromProposal(kind, proposal)\n if (kind === 'bar') {\n if (!values.text) return null\n return {\n kind: 'bar',\n enabled: false,\n ...(values.name ? { name: values.name } : {}),\n bar: { text: values.text, dismissible: true },\n }\n }\n if (!values.body) return null\n const trigger = values.trigger ?? 'delay'\n return {\n kind: 'popup',\n enabled: false,\n ...(values.name ? { name: values.name } : {}),\n popup: {\n ...(values.headline ? { headline: values.headline } : {}),\n body: values.body,\n ...(values.ctaLabel ? { ctaLabel: values.ctaLabel } : {}),\n trigger,\n ...(trigger === 'exit' ? {} : { triggerValue: values.triggerValue ?? 3 }),\n frequencyDays: 7,\n },\n }\n}\n\n/**\n * What is wrong with an overlay draft's content as another plugin hands it\n * over (AGL-3616), held to the same limits and trigger catalog a proposal is,\n * but REFUSED rather than cut: a caller that writes a draft through the\n * marketing plugin's writer is told what to fix. Empty when nothing is.\n */\nexport function overlayDraftContentProblems(content: Readonly<Record<string, unknown>>): string[] {\n const problems: string[] = []\n const kind = content['kind']\n if (kind !== 'bar' && kind !== 'popup') return ['An overlay is a bar or a popup']\n const fields: OverlayCopyField[] =\n kind === 'bar' ? ['name', 'text'] : ['name', 'headline', 'body', 'ctaLabel']\n for (const field of fields) {\n const value = content[field]\n if (value === undefined || value === null) continue\n if (typeof value !== 'string') problems.push(`The ${field} must be text`)\n else if (value.trim().length > OVERLAY_COPY_LIMITS[field]) {\n problems.push(`The ${field} is longer than ${OVERLAY_COPY_LIMITS[field]} characters`)\n }\n }\n const required = kind === 'bar' ? 'text' : 'body'\n if (typeof content[required] !== 'string' || !String(content[required]).trim()) {\n problems.push(kind === 'bar' ? 'An announcement bar needs its text' : 'A popup needs its body')\n }\n if (kind === 'popup' && content['trigger'] !== undefined && content['trigger'] !== null) {\n const trigger = OVERLAY_POPUP_TRIGGERS.find((entry) => entry.id === content['trigger'])\n if (!trigger) {\n problems.push(`A popup opens ${OVERLAY_POPUP_TRIGGERS.map((entry) => entry.id).join(', ')}`)\n } else if (trigger.unit && content['triggerValue'] !== undefined && content['triggerValue'] !== null) {\n const value = Number(content['triggerValue'])\n if (!Number.isFinite(value) || value < trigger.min || value > trigger.max) {\n problems.push(`The ${trigger.label.toLowerCase()} value must be from ${trigger.min} to ${trigger.max}`)\n }\n }\n }\n return problems\n}\n"],"names":["OVERLAY_LIST_CEILING","OVERLAY_COPY_LIMITS","name","text","headline","body","ctaLabel","OVERLAY_POPUP_TRIGGERS","id","label","unit","min","max","clean","value","field","multiline","undefined","folded","replace","trimmed","trim","slice","overlayCopyFromProposal","kind","proposal","values","trigger","find","entry","raw","Number","triggerValue","isFinite","Math","round","overlayDraftFromProposal","enabled","bar","dismissible","popup","frequencyDays","overlayDraftContentProblems","content","problems","fields","push","length","required","String","map","join","toLowerCase"],"mappings":"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"}
package/src/lib/plugin.js CHANGED
@@ -19,6 +19,9 @@ import { mdiBullhornOutline } from "@aglyn/shared-data-mdi";
19
19
  import { lazy } from "react";
20
20
  import { CAMPAIGN_DESIGN_CREATE_ZONE, CAMPAIGN_DESIGN_PREVIEW_ZONE, CAMPAIGN_SENDER_EDITOR_ZONE, CAMPAIGN_TOPIC_OPTIONS_ZONE, CAMPAIGN_TOPIC_SELECT_ZONE } from "./components/campaign-email-zones.js";
21
21
  import { EXPERIMENT_RESULT_ZONE, EXPERIMENT_VARIANTS_ZONE } from "./components/experiment-zones.js";
22
+ import { HOST_OVERLAYS_ZONE, OVERLAY_EDITOR_ZONE } from "./components/overlay-zones.js";
23
+ import { HOST_CAMPAIGNS_ZONE } from "./components/campaign-list-zones.js";
24
+ import { MARKETING_INSIGHTS_ZONE } from "./components/marketing-insight-zones.js";
22
25
  import { MARKETING_CONSOLE_SECTIONS, MARKETING_ORG_CONSOLE_SECTIONS } from "./components/marketing-console-sections.js";
23
26
  import { registerPluginTransferResourceUi } from "@aglyn/aglyn/plugin-manager/plugin-transfer-resources";
24
27
  import { BUNDLE_ID } from "./constants/bundle-common.js";
@@ -97,6 +100,15 @@ import { registerMarketingRecordRoutes } from "./model/marketing-record-routes.j
97
100
  bareZone(CAMPAIGN_SENDER_EDITOR_ZONE, 'Add a sender while composing', 'Mounted by the campaign composer once its author asks to add a sender. A widget here is that editor, open in its add mode; `onSaved` names the sender it created.');
98
101
  bareZone(CAMPAIGN_DESIGN_CREATE_ZONE, 'A design for one email', 'In the campaign composer, beside the design picker. A widget here is the control that mints a design document and names it through `onCreated`; the composer records the choice on the campaign and opens the editor.');
99
102
  bareZone(CAMPAIGN_DESIGN_PREVIEW_ZONE, 'A sent email as an inbox receives it', 'On one message’s page. A widget here renders the stored design, or the plain-text body, through the renderer the send path uses.');
103
+ /*
104
+ * The overlays list's two positions (AGL-3603): another way to start an
105
+ * overlay beside New bar and New popup, and copy proposed among the
106
+ * editor's fields. What each hands a widget is carried on its token in
107
+ * `components/overlay-zones`; neither lets a widget write.
108
+ */ bareZone(HOST_OVERLAYS_ZONE, 'Start an overlay', 'On a site’s Overlays section, beside New bar and New popup: another way to start an announcement bar or a popup. A widget hands proposed copy to `createOverlayDraft`; the list writes the overlay switched off and opens it in the editor.');
109
+ bareZone(OVERLAY_EDITOR_ZONE, 'An overlay’s copy', 'Among the overlay editor’s fields. A widget is handed the copy as the editor holds it, the field limits and the popup triggers, and proposes copy through `proposeValues`, which fills the fields unsaved; the editor’s Save is the write.');
110
+ bareZone(HOST_CAMPAIGNS_ZONE, 'Start a campaign', 'On the Campaigns section, a site’s or the organization’s, beside Create campaign and again in the empty list: another way to start a campaign. A widget is handed the site, the organization and, on the organization’s hub, the sites a campaign could be placed on; it writes nothing through the list.');
111
+ bareZone(MARKETING_INSIGHTS_ZONE, 'A marketing report’s figures, in words', 'In the header of the Conversions section and of one campaign’s report, beside the page’s own actions. A widget is handed the site the figures are one site’s and what the report is about, reads the figures this plugin publishes as figure readers, and writes nothing.');
100
112
  registerMarketingRecordRoutes();
101
113
  // The site's overlays, for another plugin's picker to list (AGL-3080).
102
114
  registerMarketingRecordLists();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/marketing/src/lib/plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n registerPluginZone,\n type PluginZone,\n} from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { mdiBullhornOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport {\n CAMPAIGN_DESIGN_CREATE_ZONE,\n CAMPAIGN_DESIGN_PREVIEW_ZONE,\n CAMPAIGN_SENDER_EDITOR_ZONE,\n CAMPAIGN_TOPIC_OPTIONS_ZONE,\n CAMPAIGN_TOPIC_SELECT_ZONE,\n} from './components/campaign-email-zones'\nimport {\n EXPERIMENT_RESULT_ZONE,\n EXPERIMENT_VARIANTS_ZONE,\n} from './components/experiment-zones'\nimport {\n MARKETING_CONSOLE_SECTIONS,\n MARKETING_ORG_CONSOLE_SECTIONS,\n} from './components/marketing-console-sections'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { StaffOrgEmailCard } from './components/staff-org-email-card.component'\nimport { registerMarketingRecordLists } from './model/overlay-record-list'\nimport { registerMarketingRecordRoutes } from './model/marketing-record-routes'\n\n/** Code-split: the Marketing console page only loads when opened. */\nconst MarketingConsolePage = lazy(\n () => import('./components/marketing-console-page'),\n)\n\n/** Dashboard glance card, loaded only where the shell renders the slot. */\nconst CampaignGlanceCard = lazy(\n () => import('./components/campaign-glance-card.component'),\n)\n\n/**\n * The campaigns drawn in the Inbox's Campaigns section: one site's, or every\n * site's on the organization's Inbox.\n */\nconst InboxCampaignsWidget = lazy(\n () => import('./components/inbox-campaigns-widget'),\n)\n\n/** The Emails page's Messages section: the list, one message, its composer. */\nconst EmailMessagesWidget = lazy(\n () => import('./components/email-messages-widget'),\n)\n\n/** What the sends built from one template did, on the template's page. */\nconst EmailTemplateReportCard = lazy(\n () => import('./components/email-template-report-card'),\n)\n\n/** Who received the sends built from one template, on the template's page. */\nconst EmailRecipientsCard = lazy(\n () => import('./components/email-recipients-card'),\n)\n\n/** Loaded where a record page draws one of the attribution zones. */\nconst RecordAttributionWidget = lazy(\n () => import('./components/record-attribution-widget'),\n)\n\n/** The site's section experiments, reported to the besigner's Interactions section. */\nconst BesignerSectionExperiments = lazy(\n () => import('./components/besigner-section-experiments'),\n)\n\n/**\n * Marketing feature plugin (AGL-395). Console-only — overlays and popups\n * render on published sites through the tenant runtime, not a canvas\n * element of their own, so there is no UI bundle. The console half declares\n * the Marketing nav + page through the ConsoleExtension registry (always-on;\n * the surface itself is not release-flagged — its overlays/A-B cards run\n * their own per-plan checks off the passed `org`). The popup image picker\n * uses the shell's media browser via `useMediaPicker`.\n */\nexport function registerMarketingConsole(): void {\n // How the Import & export hub names campaigns in a workspace package (AGL-3535).\n registerPluginTransferResourceUi('marketing.campaigns', { label: 'Campaigns' }, { pluginId: BUNDLE_ID })\n /*\n * The two positions the A/B testing card hosts (AGL-2914), declared before\n * the extension that draws the card. An id another plugin has already taken\n * is refused, naming both. What each zone hands a widget is carried on its\n * token, in `components/experiment-zones`, so a widget elsewhere is written\n * against the same shape without importing this plugin.\n */\n registerPluginZone(\n {\n zone: EXPERIMENT_VARIANTS_ZONE,\n label: 'A/B test variants',\n surface: 'console',\n description:\n 'In the experiment editor, beside the variants. A widget here proposes copy for the variants the editor holds; the dialog’s Save is the write, and nothing a widget returns starts or changes a test.',\n },\n // Named rather than left to the loader's marker, because this function is\n // also called directly — by a spec, and by an app that loads the plugin\n // without the loader — and a zone with no owner is refused.\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: EXPERIMENT_RESULT_ZONE,\n label: 'A/B test result',\n surface: 'console',\n description:\n 'Below the results of one test. A widget here explains what the figures show. There is nothing to apply, and a widget may not declare a winner the card has not.',\n },\n { pluginId: BUNDLE_ID },\n )\n /*\n * What a campaign email asks of whichever plugin keeps the mail itself —\n * the topic catalog, the sending identities, the design document and its\n * renderer. Each is a zone the composer or a message's page hosts; see\n * `components/campaign-email-zones` for what each hands a widget. All are\n * `bare`: a widget there is a field, a button, a drawer or nothing at all,\n * and the stack's spacing around it would be a gap in a form.\n */\n const bareZone = <Props,>(\n zone: PluginZone<Props>,\n label: string,\n description: string,\n ) =>\n registerPluginZone(\n { zone, label, description, surface: 'console', layout: 'bare' },\n { pluginId: BUNDLE_ID },\n )\n bareZone(\n CAMPAIGN_TOPIC_SELECT_ZONE,\n 'An email’s topic',\n 'In the campaign composer’s field stack. A widget here is the picker for the stream the email belongs to: it is handed the chosen id and reports a new one through `onChange`. The composer records it; the widget writes nothing.',\n )\n bareZone(\n CAMPAIGN_TOPIC_OPTIONS_ZONE,\n 'The topics a campaign can open on',\n 'Beside the drawers that create and edit a campaign. A widget here draws nothing: while `enabled` it reads the streams a recipient can still leave and answers through `onTopics`.',\n )\n bareZone(\n CAMPAIGN_SENDER_EDITOR_ZONE,\n 'Add a sender while composing',\n 'Mounted by the campaign composer once its author asks to add a sender. A widget here is that editor, open in its add mode; `onSaved` names the sender it created.',\n )\n bareZone(\n CAMPAIGN_DESIGN_CREATE_ZONE,\n 'A design for one email',\n 'In the campaign composer, beside the design picker. A widget here is the control that mints a design document and names it through `onCreated`; the composer records the choice on the campaign and opens the editor.',\n )\n bareZone(\n CAMPAIGN_DESIGN_PREVIEW_ZONE,\n 'A sent email as an inbox receives it',\n 'On one message’s page. A widget here renders the stored design, or the plain-text body, through the renderer the send path uses.',\n )\n registerMarketingRecordRoutes()\n // The site's overlays, for another plugin's picker to list (AGL-3080).\n registerMarketingRecordLists()\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Marketing',\n /*\n * The host dashboard's `Last campaign` card (AGL-433). A widget rather\n * than a direct import by the dashboard page, so it appears only on\n * workspaces that have the plugin that owns the campaign history — a card\n * about campaigns on a console with no campaigns page could only ever be\n * blank, and its header links straight into `/marketing/campaigns`.\n */\n widgets: [\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n // The id a reader's dashboard preferences already name — it is what\n // `isDashboardWidgetHidden` and the customize order are keyed on, so\n // it is persisted and does not follow the plugin.\n widgetId: 'email-campaign-glance',\n title: 'Last campaign',\n Component: CampaignGlanceCard,\n },\n // Which campaign or link brought a person, drawn where the plugins that\n // keep and list people host a zone for it. They hand over what the\n // record is and its id; neither imports this plugin.\n {\n slot: 'crmRecordAttribution',\n widgetId: 'marketing-crm-record-attribution',\n title: 'Campaign attribution',\n Component: RecordAttributionWidget,\n },\n // The Emails page hosts its Messages section as a zone: a message is\n // one send of a campaign, and every action on it is this plugin's route.\n {\n slot: 'emailMessages',\n widgetId: 'marketing-email-messages',\n title: 'Messages',\n Component: EmailMessagesWidget,\n },\n // A template's page: what the sends built from it did, then who\n // received them. The template is the Email plugin's; the sends are ours.\n {\n slot: 'emailTemplateReport',\n widgetId: 'marketing-email-template-report',\n title: 'Sent from this template',\n Component: EmailTemplateReportCard,\n },\n {\n slot: 'emailTemplateRecipients',\n widgetId: 'marketing-email-template-recipients',\n title: 'Recipients',\n Component: EmailRecipientsCard,\n },\n // The Inbox's Campaigns tab is a zone; this is the card that fills it.\n {\n slot: 'inboxCampaigns',\n widgetId: 'marketing-inbox-campaigns',\n title: 'Campaigns',\n Component: InboxCampaignsWidget,\n },\n {\n slot: 'inboxRecordAttribution',\n widgetId: 'marketing-inbox-record-attribution',\n title: 'Campaign attribution',\n Component: RecordAttributionWidget,\n },\n // The section experiments the besigner's Interactions section badges an\n // element with and starts on a page's element; reported, never drawn.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.besignerInteractions,\n widgetId: 'marketing-besigner-section-experiments',\n title: 'Section experiments',\n Component: BesignerSectionExperiments,\n },\n // The staff organization page's sends and campaigns (AGL-3380), read\n // only: staff see what the organization sent without joining it.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.adminOrgDetail,\n widgetId: 'marketing-staff-org-email',\n title: 'Email campaigns',\n Component: StaffOrgEmailCard,\n },\n ],\n navItems: [\n {\n label: 'Marketing',\n href: '/marketing',\n // Sections as ROUTES (AGL-2501): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read.\n sections: MARKETING_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-marketing',\n icon: { path: mdiBullhornOutline.path },\n header: {\n title: 'Marketing',\n icon: { path: mdiBullhornOutline.path },\n docsTopic: 'marketingOverlays',\n },\n Component: MarketingConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's Marketing hub, at `/[orgSlug]/marketing`: every\n * section the site hub has, over every site. Campaigns and their sequence\n * rollups belong to the organization, so this is where one campaign is\n * placed across several sites; overlays, tests and conversions stay each\n * site's and are read across them. Every site's mail is on the\n * organization's Emails page, as a site's is on its own. The page is the\n * same component; handed no site, it renders the org sections.\n *\n * It carries the SITE tab's id on purpose, the way the org CRM tab does:\n * `release_marketing` names `nav-tab-marketing`, so one flag holds both\n * halves of the surface, and the org half cannot ship while the site half\n * is switched off.\n */\n orgNavItems: [\n {\n label: 'Marketing',\n href: '/marketing',\n sections: MARKETING_ORG_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-marketing',\n icon: { path: mdiBullhornOutline.path },\n header: {\n title: 'Marketing',\n icon: { path: mdiBullhornOutline.path },\n docsTopic: 'marketingOverlays',\n },\n Component: MarketingConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","registerPluginZone","mdiBullhornOutline","lazy","CAMPAIGN_DESIGN_CREATE_ZONE","CAMPAIGN_DESIGN_PREVIEW_ZONE","CAMPAIGN_SENDER_EDITOR_ZONE","CAMPAIGN_TOPIC_OPTIONS_ZONE","CAMPAIGN_TOPIC_SELECT_ZONE","EXPERIMENT_RESULT_ZONE","EXPERIMENT_VARIANTS_ZONE","MARKETING_CONSOLE_SECTIONS","MARKETING_ORG_CONSOLE_SECTIONS","registerPluginTransferResourceUi","BUNDLE_ID","StaffOrgEmailCard","registerMarketingRecordLists","registerMarketingRecordRoutes","MarketingConsolePage","CampaignGlanceCard","InboxCampaignsWidget","EmailMessagesWidget","EmailTemplateReportCard","EmailRecipientsCard","RecordAttributionWidget","BesignerSectionExperiments","registerMarketingConsole","label","pluginId","zone","surface","description","bareZone","layout","registerConsoleExtension","displayName","widgets","slot","CONSOLE_WIDGET_SLOTS","hostDashboard","widgetId","title","Component","besignerInteractions","adminOrgDetail","navItems","href","sections","navTabId","icon","path","header","docsTopic","orgNavItems"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/marketing/src/lib/plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n registerPluginZone,\n type PluginZone,\n} from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { mdiBullhornOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport {\n CAMPAIGN_DESIGN_CREATE_ZONE,\n CAMPAIGN_DESIGN_PREVIEW_ZONE,\n CAMPAIGN_SENDER_EDITOR_ZONE,\n CAMPAIGN_TOPIC_OPTIONS_ZONE,\n CAMPAIGN_TOPIC_SELECT_ZONE,\n} from './components/campaign-email-zones'\nimport {\n EXPERIMENT_RESULT_ZONE,\n EXPERIMENT_VARIANTS_ZONE,\n} from './components/experiment-zones'\nimport { HOST_OVERLAYS_ZONE, OVERLAY_EDITOR_ZONE } from './components/overlay-zones'\nimport { HOST_CAMPAIGNS_ZONE } from './components/campaign-list-zones'\nimport { MARKETING_INSIGHTS_ZONE } from './components/marketing-insight-zones'\nimport {\n MARKETING_CONSOLE_SECTIONS,\n MARKETING_ORG_CONSOLE_SECTIONS,\n} from './components/marketing-console-sections'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { StaffOrgEmailCard } from './components/staff-org-email-card.component'\nimport { registerMarketingRecordLists } from './model/overlay-record-list'\nimport { registerMarketingRecordRoutes } from './model/marketing-record-routes'\n\n/** Code-split: the Marketing console page only loads when opened. */\nconst MarketingConsolePage = lazy(\n () => import('./components/marketing-console-page'),\n)\n\n/** Dashboard glance card, loaded only where the shell renders the slot. */\nconst CampaignGlanceCard = lazy(\n () => import('./components/campaign-glance-card.component'),\n)\n\n/**\n * The campaigns drawn in the Inbox's Campaigns section: one site's, or every\n * site's on the organization's Inbox.\n */\nconst InboxCampaignsWidget = lazy(\n () => import('./components/inbox-campaigns-widget'),\n)\n\n/** The Emails page's Messages section: the list, one message, its composer. */\nconst EmailMessagesWidget = lazy(\n () => import('./components/email-messages-widget'),\n)\n\n/** What the sends built from one template did, on the template's page. */\nconst EmailTemplateReportCard = lazy(\n () => import('./components/email-template-report-card'),\n)\n\n/** Who received the sends built from one template, on the template's page. */\nconst EmailRecipientsCard = lazy(\n () => import('./components/email-recipients-card'),\n)\n\n/** Loaded where a record page draws one of the attribution zones. */\nconst RecordAttributionWidget = lazy(\n () => import('./components/record-attribution-widget'),\n)\n\n/** The site's section experiments, reported to the besigner's Interactions section. */\nconst BesignerSectionExperiments = lazy(\n () => import('./components/besigner-section-experiments'),\n)\n\n/**\n * Marketing feature plugin (AGL-395). Console-only — overlays and popups\n * render on published sites through the tenant runtime, not a canvas\n * element of their own, so there is no UI bundle. The console half declares\n * the Marketing nav + page through the ConsoleExtension registry (always-on;\n * the surface itself is not release-flagged — its overlays/A-B cards run\n * their own per-plan checks off the passed `org`). The popup image picker\n * uses the shell's media browser via `useMediaPicker`.\n */\nexport function registerMarketingConsole(): void {\n // How the Import & export hub names campaigns in a workspace package (AGL-3535).\n registerPluginTransferResourceUi('marketing.campaigns', { label: 'Campaigns' }, { pluginId: BUNDLE_ID })\n /*\n * The two positions the A/B testing card hosts (AGL-2914), declared before\n * the extension that draws the card. An id another plugin has already taken\n * is refused, naming both. What each zone hands a widget is carried on its\n * token, in `components/experiment-zones`, so a widget elsewhere is written\n * against the same shape without importing this plugin.\n */\n registerPluginZone(\n {\n zone: EXPERIMENT_VARIANTS_ZONE,\n label: 'A/B test variants',\n surface: 'console',\n description:\n 'In the experiment editor, beside the variants. A widget here proposes copy for the variants the editor holds; the dialog’s Save is the write, and nothing a widget returns starts or changes a test.',\n },\n // Named rather than left to the loader's marker, because this function is\n // also called directly — by a spec, and by an app that loads the plugin\n // without the loader — and a zone with no owner is refused.\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: EXPERIMENT_RESULT_ZONE,\n label: 'A/B test result',\n surface: 'console',\n description:\n 'Below the results of one test. A widget here explains what the figures show. There is nothing to apply, and a widget may not declare a winner the card has not.',\n },\n { pluginId: BUNDLE_ID },\n )\n /*\n * What a campaign email asks of whichever plugin keeps the mail itself —\n * the topic catalog, the sending identities, the design document and its\n * renderer. Each is a zone the composer or a message's page hosts; see\n * `components/campaign-email-zones` for what each hands a widget. All are\n * `bare`: a widget there is a field, a button, a drawer or nothing at all,\n * and the stack's spacing around it would be a gap in a form.\n */\n const bareZone = <Props,>(\n zone: PluginZone<Props>,\n label: string,\n description: string,\n ) =>\n registerPluginZone(\n { zone, label, description, surface: 'console', layout: 'bare' },\n { pluginId: BUNDLE_ID },\n )\n bareZone(\n CAMPAIGN_TOPIC_SELECT_ZONE,\n 'An email’s topic',\n 'In the campaign composer’s field stack. A widget here is the picker for the stream the email belongs to: it is handed the chosen id and reports a new one through `onChange`. The composer records it; the widget writes nothing.',\n )\n bareZone(\n CAMPAIGN_TOPIC_OPTIONS_ZONE,\n 'The topics a campaign can open on',\n 'Beside the drawers that create and edit a campaign. A widget here draws nothing: while `enabled` it reads the streams a recipient can still leave and answers through `onTopics`.',\n )\n bareZone(\n CAMPAIGN_SENDER_EDITOR_ZONE,\n 'Add a sender while composing',\n 'Mounted by the campaign composer once its author asks to add a sender. A widget here is that editor, open in its add mode; `onSaved` names the sender it created.',\n )\n bareZone(\n CAMPAIGN_DESIGN_CREATE_ZONE,\n 'A design for one email',\n 'In the campaign composer, beside the design picker. A widget here is the control that mints a design document and names it through `onCreated`; the composer records the choice on the campaign and opens the editor.',\n )\n bareZone(\n CAMPAIGN_DESIGN_PREVIEW_ZONE,\n 'A sent email as an inbox receives it',\n 'On one message’s page. A widget here renders the stored design, or the plain-text body, through the renderer the send path uses.',\n )\n /*\n * The overlays list's two positions (AGL-3603): another way to start an\n * overlay beside New bar and New popup, and copy proposed among the\n * editor's fields. What each hands a widget is carried on its token in\n * `components/overlay-zones`; neither lets a widget write.\n */\n bareZone(\n HOST_OVERLAYS_ZONE,\n 'Start an overlay',\n 'On a site’s Overlays section, beside New bar and New popup: another way to start an announcement bar or a popup. A widget hands proposed copy to `createOverlayDraft`; the list writes the overlay switched off and opens it in the editor.',\n )\n bareZone(\n OVERLAY_EDITOR_ZONE,\n 'An overlay’s copy',\n 'Among the overlay editor’s fields. A widget is handed the copy as the editor holds it, the field limits and the popup triggers, and proposes copy through `proposeValues`, which fills the fields unsaved; the editor’s Save is the write.',\n )\n bareZone(\n HOST_CAMPAIGNS_ZONE,\n 'Start a campaign',\n 'On the Campaigns section, a site’s or the organization’s, beside Create campaign and again in the empty list: another way to start a campaign. A widget is handed the site, the organization and, on the organization’s hub, the sites a campaign could be placed on; it writes nothing through the list.',\n )\n bareZone(\n MARKETING_INSIGHTS_ZONE,\n 'A marketing report’s figures, in words',\n 'In the header of the Conversions section and of one campaign’s report, beside the page’s own actions. A widget is handed the site the figures are one site’s and what the report is about, reads the figures this plugin publishes as figure readers, and writes nothing.',\n )\n registerMarketingRecordRoutes()\n // The site's overlays, for another plugin's picker to list (AGL-3080).\n registerMarketingRecordLists()\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Marketing',\n /*\n * The host dashboard's `Last campaign` card (AGL-433). A widget rather\n * than a direct import by the dashboard page, so it appears only on\n * workspaces that have the plugin that owns the campaign history — a card\n * about campaigns on a console with no campaigns page could only ever be\n * blank, and its header links straight into `/marketing/campaigns`.\n */\n widgets: [\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n // The id a reader's dashboard preferences already name — it is what\n // `isDashboardWidgetHidden` and the customize order are keyed on, so\n // it is persisted and does not follow the plugin.\n widgetId: 'email-campaign-glance',\n title: 'Last campaign',\n Component: CampaignGlanceCard,\n },\n // Which campaign or link brought a person, drawn where the plugins that\n // keep and list people host a zone for it. They hand over what the\n // record is and its id; neither imports this plugin.\n {\n slot: 'crmRecordAttribution',\n widgetId: 'marketing-crm-record-attribution',\n title: 'Campaign attribution',\n Component: RecordAttributionWidget,\n },\n // The Emails page hosts its Messages section as a zone: a message is\n // one send of a campaign, and every action on it is this plugin's route.\n {\n slot: 'emailMessages',\n widgetId: 'marketing-email-messages',\n title: 'Messages',\n Component: EmailMessagesWidget,\n },\n // A template's page: what the sends built from it did, then who\n // received them. The template is the Email plugin's; the sends are ours.\n {\n slot: 'emailTemplateReport',\n widgetId: 'marketing-email-template-report',\n title: 'Sent from this template',\n Component: EmailTemplateReportCard,\n },\n {\n slot: 'emailTemplateRecipients',\n widgetId: 'marketing-email-template-recipients',\n title: 'Recipients',\n Component: EmailRecipientsCard,\n },\n // The Inbox's Campaigns tab is a zone; this is the card that fills it.\n {\n slot: 'inboxCampaigns',\n widgetId: 'marketing-inbox-campaigns',\n title: 'Campaigns',\n Component: InboxCampaignsWidget,\n },\n {\n slot: 'inboxRecordAttribution',\n widgetId: 'marketing-inbox-record-attribution',\n title: 'Campaign attribution',\n Component: RecordAttributionWidget,\n },\n // The section experiments the besigner's Interactions section badges an\n // element with and starts on a page's element; reported, never drawn.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.besignerInteractions,\n widgetId: 'marketing-besigner-section-experiments',\n title: 'Section experiments',\n Component: BesignerSectionExperiments,\n },\n // The staff organization page's sends and campaigns (AGL-3380), read\n // only: staff see what the organization sent without joining it.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.adminOrgDetail,\n widgetId: 'marketing-staff-org-email',\n title: 'Email campaigns',\n Component: StaffOrgEmailCard,\n },\n ],\n navItems: [\n {\n label: 'Marketing',\n href: '/marketing',\n // Sections as ROUTES (AGL-2501): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read.\n sections: MARKETING_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-marketing',\n icon: { path: mdiBullhornOutline.path },\n header: {\n title: 'Marketing',\n icon: { path: mdiBullhornOutline.path },\n docsTopic: 'marketingOverlays',\n },\n Component: MarketingConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's Marketing hub, at `/[orgSlug]/marketing`: every\n * section the site hub has, over every site. Campaigns and their sequence\n * rollups belong to the organization, so this is where one campaign is\n * placed across several sites; overlays, tests and conversions stay each\n * site's and are read across them. Every site's mail is on the\n * organization's Emails page, as a site's is on its own. The page is the\n * same component; handed no site, it renders the org sections.\n *\n * It carries the SITE tab's id on purpose, the way the org CRM tab does:\n * `release_marketing` names `nav-tab-marketing`, so one flag holds both\n * halves of the surface, and the org half cannot ship while the site half\n * is switched off.\n */\n orgNavItems: [\n {\n label: 'Marketing',\n href: '/marketing',\n sections: MARKETING_ORG_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-marketing',\n icon: { path: mdiBullhornOutline.path },\n header: {\n title: 'Marketing',\n icon: { path: mdiBullhornOutline.path },\n docsTopic: 'marketingOverlays',\n },\n Component: MarketingConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","registerPluginZone","mdiBullhornOutline","lazy","CAMPAIGN_DESIGN_CREATE_ZONE","CAMPAIGN_DESIGN_PREVIEW_ZONE","CAMPAIGN_SENDER_EDITOR_ZONE","CAMPAIGN_TOPIC_OPTIONS_ZONE","CAMPAIGN_TOPIC_SELECT_ZONE","EXPERIMENT_RESULT_ZONE","EXPERIMENT_VARIANTS_ZONE","HOST_OVERLAYS_ZONE","OVERLAY_EDITOR_ZONE","HOST_CAMPAIGNS_ZONE","MARKETING_INSIGHTS_ZONE","MARKETING_CONSOLE_SECTIONS","MARKETING_ORG_CONSOLE_SECTIONS","registerPluginTransferResourceUi","BUNDLE_ID","StaffOrgEmailCard","registerMarketingRecordLists","registerMarketingRecordRoutes","MarketingConsolePage","CampaignGlanceCard","InboxCampaignsWidget","EmailMessagesWidget","EmailTemplateReportCard","EmailRecipientsCard","RecordAttributionWidget","BesignerSectionExperiments","registerMarketingConsole","label","pluginId","zone","surface","description","bareZone","layout","registerConsoleExtension","displayName","widgets","slot","CONSOLE_WIDGET_SLOTS","hostDashboard","widgetId","title","Component","besignerInteractions","adminOrgDetail","navItems","href","sections","navTabId","icon","path","header","docsTopic","orgNavItems"],"mappings":"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"}
@@ -47,6 +47,7 @@ import { recordEmailDeliveryEvents, recordPersonEngagement } from "@aglyn/tenant
47
47
  import { recordEmailCampaignTouch } from "./email-campaign-touch.js";
48
48
  // The contact's own stamp is the record system's, asked through the core's
49
49
  // seam: what the record page, the list and the re-engagement audience read.
50
+ import { runPluginEventHandlers } from "@aglyn/aglyn/plugin-manager/plugin-events";
50
51
  import { stampRecordEmailEngagement } from "@aglyn/aglyn/plugin-manager/plugin-record-email-state";
51
52
  import { isDocumentId } from "@aglyn/tenant-data-admin/server/document-id";
52
53
  import { getOrgForHost } from "@aglyn/tenant-data-admin/server/organizations";
@@ -458,6 +459,13 @@ import { suppressionId } from "./campaign-send.js";
458
459
  hostId: hostRef.id,
459
460
  events: outcomes
460
461
  }).catch(()=>null);
462
+ // The same first opens and clicks, for any plugin that counts a site's
463
+ // engagement of its own — a funnel's email step (AGL-3605). Isolated:
464
+ // a subscriber's failure never reaches the webhook's answer.
465
+ await runPluginEventHandlers('host.email.engaged', {
466
+ hostId: hostRef.id,
467
+ events: outcomes.filter((one)=>one.type === 'opened' || one.type === 'clicked')
468
+ }).catch(()=>null);
461
469
  }
462
470
  /*==========================================
463
471
  * THE ONE-TO-ONE EMAIL'S TIMELINE ENTRY (AGL-2615, AGL-3080).