@aglyn/plugins-marketing 1.0.0-beta.143

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 (183) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +56 -0
  4. package/src/index.d.ts +19 -0
  5. package/src/index.js +20 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/announcement-bar-card.component.d.ts +20 -0
  8. package/src/lib/components/announcement-bar-card.component.js +292 -0
  9. package/src/lib/components/announcement-bar-card.component.js.map +1 -0
  10. package/src/lib/components/campaign-composer.d.ts +84 -0
  11. package/src/lib/components/campaign-composer.js +1643 -0
  12. package/src/lib/components/campaign-composer.js.map +1 -0
  13. package/src/lib/components/campaign-conversions-card.d.ts +22 -0
  14. package/src/lib/components/campaign-conversions-card.js +642 -0
  15. package/src/lib/components/campaign-conversions-card.js.map +1 -0
  16. package/src/lib/components/campaign-detail-card.d.ts +55 -0
  17. package/src/lib/components/campaign-detail-card.js +771 -0
  18. package/src/lib/components/campaign-detail-card.js.map +1 -0
  19. package/src/lib/components/campaign-edit-drawer.d.ts +60 -0
  20. package/src/lib/components/campaign-edit-drawer.js +252 -0
  21. package/src/lib/components/campaign-edit-drawer.js.map +1 -0
  22. package/src/lib/components/campaign-email-zones.d.ts +111 -0
  23. package/src/lib/components/campaign-email-zones.js +54 -0
  24. package/src/lib/components/campaign-email-zones.js.map +1 -0
  25. package/src/lib/components/campaign-glance-card.component.d.ts +18 -0
  26. package/src/lib/components/campaign-glance-card.component.js +151 -0
  27. package/src/lib/components/campaign-glance-card.component.js.map +1 -0
  28. package/src/lib/components/campaign-members-section.d.ts +87 -0
  29. package/src/lib/components/campaign-members-section.js +499 -0
  30. package/src/lib/components/campaign-members-section.js.map +1 -0
  31. package/src/lib/components/campaign-reach-sections.d.ts +137 -0
  32. package/src/lib/components/campaign-reach-sections.js +694 -0
  33. package/src/lib/components/campaign-reach-sections.js.map +1 -0
  34. package/src/lib/components/campaign-report-card.d.ts +91 -0
  35. package/src/lib/components/campaign-report-card.js +667 -0
  36. package/src/lib/components/campaign-report-card.js.map +1 -0
  37. package/src/lib/components/campaign-test-send-drawer.d.ts +45 -0
  38. package/src/lib/components/campaign-test-send-drawer.js +238 -0
  39. package/src/lib/components/campaign-test-send-drawer.js.map +1 -0
  40. package/src/lib/components/campaigns-card.d.ts +31 -0
  41. package/src/lib/components/campaigns-card.js +667 -0
  42. package/src/lib/components/campaigns-card.js.map +1 -0
  43. package/src/lib/components/conversion-attribution.component.d.ts +62 -0
  44. package/src/lib/components/conversion-attribution.component.js +127 -0
  45. package/src/lib/components/conversion-attribution.component.js.map +1 -0
  46. package/src/lib/components/email-compose-card.d.ts +12 -0
  47. package/src/lib/components/email-compose-card.js +157 -0
  48. package/src/lib/components/email-compose-card.js.map +1 -0
  49. package/src/lib/components/email-detail.d.ts +40 -0
  50. package/src/lib/components/email-detail.js +1180 -0
  51. package/src/lib/components/email-detail.js.map +1 -0
  52. package/src/lib/components/email-edit-drawer.d.ts +54 -0
  53. package/src/lib/components/email-edit-drawer.js +172 -0
  54. package/src/lib/components/email-edit-drawer.js.map +1 -0
  55. package/src/lib/components/email-messages-widget.d.ts +22 -0
  56. package/src/lib/components/email-messages-widget.js +49 -0
  57. package/src/lib/components/email-messages-widget.js.map +1 -0
  58. package/src/lib/components/email-recipients-card.d.ts +50 -0
  59. package/src/lib/components/email-recipients-card.js +338 -0
  60. package/src/lib/components/email-recipients-card.js.map +1 -0
  61. package/src/lib/components/emails-list-card.d.ts +36 -0
  62. package/src/lib/components/emails-list-card.js +544 -0
  63. package/src/lib/components/emails-list-card.js.map +1 -0
  64. package/src/lib/components/experiment-zones.d.ts +78 -0
  65. package/src/lib/components/experiment-zones.js +65 -0
  66. package/src/lib/components/experiment-zones.js.map +1 -0
  67. package/src/lib/components/host-experiments-card.component.d.ts +17 -0
  68. package/src/lib/components/host-experiments-card.component.js +887 -0
  69. package/src/lib/components/host-experiments-card.component.js.map +1 -0
  70. package/src/lib/components/host-marketing-summary-card.component.d.ts +15 -0
  71. package/src/lib/components/host-marketing-summary-card.component.js +161 -0
  72. package/src/lib/components/host-marketing-summary-card.component.js.map +1 -0
  73. package/src/lib/components/host-overlays-card.component.d.ts +19 -0
  74. package/src/lib/components/host-overlays-card.component.js +783 -0
  75. package/src/lib/components/host-overlays-card.component.js.map +1 -0
  76. package/src/lib/components/marketing-console-page.d.ts +28 -0
  77. package/src/lib/components/marketing-console-page.js +166 -0
  78. package/src/lib/components/marketing-console-page.js.map +1 -0
  79. package/src/lib/components/marketing-console-sections.d.ts +34 -0
  80. package/src/lib/components/marketing-console-sections.js +77 -0
  81. package/src/lib/components/marketing-console-sections.js.map +1 -0
  82. package/src/lib/components/overlay-stats-row.component.d.ts +15 -0
  83. package/src/lib/components/overlay-stats-row.component.js +78 -0
  84. package/src/lib/components/overlay-stats-row.component.js.map +1 -0
  85. package/src/lib/components/popup-card.component.d.ts +18 -0
  86. package/src/lib/components/popup-card.component.js +462 -0
  87. package/src/lib/components/popup-card.component.js.map +1 -0
  88. package/src/lib/components/record-attribution-widget.d.ts +35 -0
  89. package/src/lib/components/record-attribution-widget.js +33 -0
  90. package/src/lib/components/record-attribution-widget.js.map +1 -0
  91. package/src/lib/components/site-runtime.d.ts +9 -0
  92. package/src/lib/components/site-runtime.js +1081 -0
  93. package/src/lib/components/site-runtime.js.map +1 -0
  94. package/src/lib/components/use-campaign-send-api.d.ts +35 -0
  95. package/src/lib/components/use-campaign-send-api.js +99 -0
  96. package/src/lib/components/use-campaign-send-api.js.map +1 -0
  97. package/src/lib/components/use-campaign-topic-options.d.ts +20 -0
  98. package/src/lib/components/use-campaign-topic-options.js +44 -0
  99. package/src/lib/components/use-campaign-topic-options.js.map +1 -0
  100. package/src/lib/components/use-emails-hub-path.d.ts +39 -0
  101. package/src/lib/components/use-emails-hub-path.js +70 -0
  102. package/src/lib/components/use-emails-hub-path.js.map +1 -0
  103. package/src/lib/components/use-marketing-hub-path.d.ts +21 -0
  104. package/src/lib/components/use-marketing-hub-path.js +50 -0
  105. package/src/lib/components/use-marketing-hub-path.js.map +1 -0
  106. package/src/lib/components/use-overlay-copy-editor.d.ts +79 -0
  107. package/src/lib/components/use-overlay-copy-editor.js +110 -0
  108. package/src/lib/components/use-overlay-copy-editor.js.map +1 -0
  109. package/src/lib/constants/bundle-common.d.ts +8 -0
  110. package/src/lib/constants/bundle-common.js +9 -0
  111. package/src/lib/constants/bundle-common.js.map +1 -0
  112. package/src/lib/model/campaign-membership-figures.d.ts +109 -0
  113. package/src/lib/model/campaign-membership-figures.js +113 -0
  114. package/src/lib/model/campaign-membership-figures.js.map +1 -0
  115. package/src/lib/model/compile-client-automations.d.ts +65 -0
  116. package/src/lib/model/compile-client-automations.js +95 -0
  117. package/src/lib/model/compile-client-automations.js.map +1 -0
  118. package/src/lib/model/experiments.d.ts +130 -0
  119. package/src/lib/model/experiments.js +186 -0
  120. package/src/lib/model/experiments.js.map +1 -0
  121. package/src/lib/model/index.d.ts +26 -0
  122. package/src/lib/model/index.js +26 -0
  123. package/src/lib/model/index.js.map +1 -0
  124. package/src/lib/model/marketing-record-routes.d.ts +6 -0
  125. package/src/lib/model/marketing-record-routes.js +46 -0
  126. package/src/lib/model/marketing-record-routes.js.map +1 -0
  127. package/src/lib/model/overlay-copy.d.ts +62 -0
  128. package/src/lib/model/overlay-copy.js +78 -0
  129. package/src/lib/model/overlay-copy.js.map +1 -0
  130. package/src/lib/model/overlays.d.ts +113 -0
  131. package/src/lib/model/overlays.js +93 -0
  132. package/src/lib/model/overlays.js.map +1 -0
  133. package/src/lib/model/site-contract.d.ts +100 -0
  134. package/src/lib/model/site-contract.js +23 -0
  135. package/src/lib/model/site-contract.js.map +1 -0
  136. package/src/lib/plugin.d.ts +27 -0
  137. package/src/lib/plugin.js +168 -0
  138. package/src/lib/plugin.js.map +1 -0
  139. package/src/lib/preview-props.d.ts +31 -0
  140. package/src/lib/preview-props.js +54 -0
  141. package/src/lib/preview-props.js.map +1 -0
  142. package/src/lib/server/campaign-manage.d.ts +62 -0
  143. package/src/lib/server/campaign-manage.js +829 -0
  144. package/src/lib/server/campaign-manage.js.map +1 -0
  145. package/src/lib/server/campaign-process-scheduled.d.ts +26 -0
  146. package/src/lib/server/campaign-process-scheduled.js +198 -0
  147. package/src/lib/server/campaign-process-scheduled.js.map +1 -0
  148. package/src/lib/server/campaign-recipients.d.ts +55 -0
  149. package/src/lib/server/campaign-recipients.js +219 -0
  150. package/src/lib/server/campaign-recipients.js.map +1 -0
  151. package/src/lib/server/campaign-send.d.ts +487 -0
  152. package/src/lib/server/campaign-send.js +3157 -0
  153. package/src/lib/server/campaign-send.js.map +1 -0
  154. package/src/lib/server/email-campaign-reach.d.ts +174 -0
  155. package/src/lib/server/email-campaign-reach.js +254 -0
  156. package/src/lib/server/email-campaign-reach.js.map +1 -0
  157. package/src/lib/server/email-events.d.ts +27 -0
  158. package/src/lib/server/email-events.js +714 -0
  159. package/src/lib/server/email-events.js.map +1 -0
  160. package/src/lib/server/get-client-automations.d.ts +60 -0
  161. package/src/lib/server/get-client-automations.js +87 -0
  162. package/src/lib/server/get-client-automations.js.map +1 -0
  163. package/src/lib/server/get-overlays.d.ts +28 -0
  164. package/src/lib/server/get-overlays.js +65 -0
  165. package/src/lib/server/get-overlays.js.map +1 -0
  166. package/src/lib/server/get-screen-experiments.d.ts +42 -0
  167. package/src/lib/server/get-screen-experiments.js +82 -0
  168. package/src/lib/server/get-screen-experiments.js.map +1 -0
  169. package/src/lib/server/lists-materialize.d.ts +18 -0
  170. package/src/lib/server/lists-materialize.js +130 -0
  171. package/src/lib/server/lists-materialize.js.map +1 -0
  172. package/src/lib/server/marketing-figures.d.ts +37 -0
  173. package/src/lib/server/marketing-figures.js +248 -0
  174. package/src/lib/server/marketing-figures.js.map +1 -0
  175. package/src/lib/server/site-page-enricher.d.ts +26 -0
  176. package/src/lib/server/site-page-enricher.js +210 -0
  177. package/src/lib/server/site-page-enricher.js.map +1 -0
  178. package/src/lib/server.d.ts +20 -0
  179. package/src/lib/server.js +152 -0
  180. package/src/lib/server.js.map +1 -0
  181. package/src/lib/site.d.ts +22 -0
  182. package/src/lib/site.js +48 -0
  183. package/src/lib/site.js.map +1 -0
@@ -0,0 +1,79 @@
1
+ import { type HostTokenSource } from '@aglyn/aglyn';
2
+ export interface OverlayCopyEditor {
3
+ /**
4
+ * Stored copy as the author reads it in the text field: a variable's
5
+ * `{{var:id}}` shows as the `{{name}}` they typed.
6
+ */
7
+ editable: (text: string | undefined) => string;
8
+ /**
9
+ * Typed copy as it is stored: each `{{name}}` of a variable on this site
10
+ * becomes its rename-safe `{{var:id}}`, the only variable form a published
11
+ * page resolves. A name no variable carries is left as typed.
12
+ */
13
+ stored: (text: string | undefined) => string | undefined;
14
+ /**
15
+ * {@link stored}, capped at `max` characters as the author reads them — a
16
+ * cap applied to the stored form could cut an id token in half, and half a
17
+ * token reaches the visitor as typed.
18
+ */
19
+ storedWithin: (text: string | undefined, max: number) => string;
20
+ /** What a visitor reads for this copy on the published site. */
21
+ preview: (text: string | undefined) => string;
22
+ /**
23
+ * Tokens in this copy that a visitor would read exactly as typed. Empty
24
+ * until the variables have been read, because until then a name the site
25
+ * does have would be reported too.
26
+ */
27
+ shownAsTyped: (text: string | undefined) => string[];
28
+ /**
29
+ * Whether any of this copy holds a token while the site's variables are
30
+ * still being read. Stored now, a name the site does have would be saved in
31
+ * the form no published page resolves.
32
+ */
33
+ pending: (...texts: Array<string | undefined>) => boolean;
34
+ }
35
+ /**
36
+ * Overlay copy — an announcement bar's text, a popup's headline and body —
37
+ * edited against the site's variables (AGL-2885).
38
+ *
39
+ * A published page resolves a variable only in its `{{var:id}}` form, while
40
+ * an author types `{{saleEndsAt}}`. So a name is rewritten to its id token on
41
+ * the way into storage and back on the way into the text field, the same
42
+ * normalization the Besigner applies when an element saves, and a token that
43
+ * would still reach a visitor as typed is reported rather than saved in
44
+ * silence.
45
+ *
46
+ * Callers hold copy in its STORED form and show {@link OverlayCopyEditor.editable}
47
+ * of it, converting each change with {@link OverlayCopyEditor.stored}. A
48
+ * field seeded before the variables arrive then catches up on its own, with no
49
+ * second seed to race the author's typing.
50
+ *
51
+ * The variables are read only once some copy holds a token, since most bars
52
+ * and popups are plain text, and stay read after that, so deleting a token and
53
+ * typing it again does not read them twice.
54
+ */
55
+ export declare function useOverlayCopyEditor(options: {
56
+ hostId: string;
57
+ /**
58
+ * The site document, for the details `preview` fills in. Optional for an
59
+ * editor with no preview: a `{{host.*}}` token always renders as the detail
60
+ * or as nothing, so whether a token shows as typed never depends on it.
61
+ */
62
+ host?: HostTokenSource | null;
63
+ /** Whether any copy this editor holds contains a `{{` token. */
64
+ usesTokens: boolean;
65
+ }): OverlayCopyEditor;
66
+ /**
67
+ * What a copy field says about variables while nothing in it is wrong.
68
+ *
69
+ * A placeholder rather than an example name: an example names a variable most
70
+ * sites do not have, and a token for a variable the site does not have is
71
+ * exactly what reaches a visitor as typed.
72
+ */
73
+ export declare const OVERLAY_COPY_HELPER_TEXT = "To show a variable from Logic, type its name in double braces, like {{name}}";
74
+ /**
75
+ * The helper text a copy field shows when some of its tokens would reach a
76
+ * visitor as typed, or undefined when none would.
77
+ */
78
+ export declare function shownAsTypedHelperText(tokens: string[]): string | undefined;
79
+ export default useOverlayCopyEditor;
@@ -0,0 +1,110 @@
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
+ */ 'use client';
17
+ import { editableBindingTokens, normalizeBindingTokens } from "@aglyn/aglyn";
18
+ import { collection, limit, query } from "firebase/firestore";
19
+ import { useMemo, useState } from "react";
20
+ import { useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-instance";
21
+ import { overlayCopyTokensShownAsTyped, resolveOverlayCopy } from "../model/overlay-copy.js";
22
+ /**
23
+ * How many variables an editor reads: the ceiling the published page reads
24
+ * them under, so the editor never recognizes a name the page cannot fill in.
25
+ */ const VARIABLES_CEILING = 100;
26
+ /**
27
+ * Overlay copy — an announcement bar's text, a popup's headline and body —
28
+ * edited against the site's variables (AGL-2885).
29
+ *
30
+ * A published page resolves a variable only in its `{{var:id}}` form, while
31
+ * an author types `{{saleEndsAt}}`. So a name is rewritten to its id token on
32
+ * the way into storage and back on the way into the text field, the same
33
+ * normalization the Besigner applies when an element saves, and a token that
34
+ * would still reach a visitor as typed is reported rather than saved in
35
+ * silence.
36
+ *
37
+ * Callers hold copy in its STORED form and show {@link OverlayCopyEditor.editable}
38
+ * of it, converting each change with {@link OverlayCopyEditor.stored}. A
39
+ * field seeded before the variables arrive then catches up on its own, with no
40
+ * second seed to race the author's typing.
41
+ *
42
+ * The variables are read only once some copy holds a token, since most bars
43
+ * and popups are plain text, and stay read after that, so deleting a token and
44
+ * typing it again does not read them twice.
45
+ */ export function useOverlayCopyEditor(options) {
46
+ const { hostId, host, usesTokens } = options;
47
+ const firestore = useFirestore();
48
+ // Latched during render, React's pattern for state derived from an earlier
49
+ // render: once any copy has held a token, the read stays open.
50
+ const [wanted, setWanted] = useState(usesTokens);
51
+ if (usesTokens && !wanted) setWanted(true);
52
+ const { data: variableDocs, status } = useFirestoreCollection(()=>wanted ? query(collection(firestore, 'hosts', hostId, 'variables'), limit(VARIABLES_CEILING)) : null, [
53
+ firestore,
54
+ hostId,
55
+ wanted
56
+ ], {
57
+ idField: '$id'
58
+ });
59
+ const settled = wanted && status !== 'loading';
60
+ return useMemo(()=>{
61
+ // Keyed by id for resolution, as the published page keys them, and by
62
+ // name for the typed form. A deleted variable is neither: the page does
63
+ // not read one.
64
+ const byId = {};
65
+ const byName = {};
66
+ for (const variable of variableDocs != null ? variableDocs : []){
67
+ if (!(variable == null ? void 0 : variable.name) || variable.deletedAt) continue;
68
+ byId[variable.$id] = variable;
69
+ byName[variable.name] = variable;
70
+ }
71
+ const editable = (text)=>typeof text === 'string' ? editableBindingTokens(text, byName) : '';
72
+ const stored = (text)=>typeof text === 'string' && text.includes('{{') ? normalizeBindingTokens(text, byName) : text;
73
+ return {
74
+ editable,
75
+ stored,
76
+ storedWithin: (text, max)=>{
77
+ var _stored;
78
+ return (_stored = stored(editable(text).slice(0, max))) != null ? _stored : '';
79
+ },
80
+ preview: (text)=>{
81
+ var _stored;
82
+ return resolveOverlayCopy((_stored = stored(text)) != null ? _stored : '', byId, host);
83
+ },
84
+ shownAsTyped: (text)=>settled ? overlayCopyTokensShownAsTyped(stored(text), byId, host) : [],
85
+ pending: (...texts)=>!settled && texts.some((text)=>Boolean(text == null ? void 0 : text.includes('{{')))
86
+ };
87
+ }, [
88
+ variableDocs,
89
+ settled,
90
+ host
91
+ ]);
92
+ }
93
+ /**
94
+ * What a copy field says about variables while nothing in it is wrong.
95
+ *
96
+ * A placeholder rather than an example name: an example names a variable most
97
+ * sites do not have, and a token for a variable the site does not have is
98
+ * exactly what reaches a visitor as typed.
99
+ */ export const OVERLAY_COPY_HELPER_TEXT = 'To show a variable from Logic, type its name in double braces, like {{name}}';
100
+ /**
101
+ * The helper text a copy field shows when some of its tokens would reach a
102
+ * visitor as typed, or undefined when none would.
103
+ */ export function shownAsTypedHelperText(tokens) {
104
+ if (!tokens.length) return undefined;
105
+ const list = tokens.join(', ');
106
+ return `Visitors would see ${list} exactly as typed. Use the name of a ` + 'variable from Logic, or remove the braces.';
107
+ }
108
+ export default useOverlayCopyEditor;
109
+
110
+ //# sourceMappingURL=use-overlay-copy-editor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/use-overlay-copy-editor.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'use client'\n\nimport {\n type BindingDocRef,\n editableBindingTokens,\n type HostTokenSource,\n type HostVariable,\n normalizeBindingTokens,\n} from '@aglyn/aglyn'\nimport { collection, limit, query } from 'firebase/firestore'\nimport { useMemo, useState } from 'react'\nimport {\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport {\n overlayCopyTokensShownAsTyped,\n resolveOverlayCopy,\n} from '../model/overlay-copy'\n\n/**\n * How many variables an editor reads: the ceiling the published page reads\n * them under, so the editor never recognizes a name the page cannot fill in.\n */\nconst VARIABLES_CEILING = 100\n\ntype VariableDoc = HostVariable & { $id: string; deletedAt?: unknown }\n\nexport interface OverlayCopyEditor {\n /**\n * Stored copy as the author reads it in the text field: a variable's\n * `{{var:id}}` shows as the `{{name}}` they typed.\n */\n editable: (text: string | undefined) => string\n /**\n * Typed copy as it is stored: each `{{name}}` of a variable on this site\n * becomes its rename-safe `{{var:id}}`, the only variable form a published\n * page resolves. A name no variable carries is left as typed.\n */\n stored: (text: string | undefined) => string | undefined\n /**\n * {@link stored}, capped at `max` characters as the author reads them — a\n * cap applied to the stored form could cut an id token in half, and half a\n * token reaches the visitor as typed.\n */\n storedWithin: (text: string | undefined, max: number) => string\n /** What a visitor reads for this copy on the published site. */\n preview: (text: string | undefined) => string\n /**\n * Tokens in this copy that a visitor would read exactly as typed. Empty\n * until the variables have been read, because until then a name the site\n * does have would be reported too.\n */\n shownAsTyped: (text: string | undefined) => string[]\n /**\n * Whether any of this copy holds a token while the site's variables are\n * still being read. Stored now, a name the site does have would be saved in\n * the form no published page resolves.\n */\n pending: (...texts: Array<string | undefined>) => boolean\n}\n\n/**\n * Overlay copy — an announcement bar's text, a popup's headline and body —\n * edited against the site's variables (AGL-2885).\n *\n * A published page resolves a variable only in its `{{var:id}}` form, while\n * an author types `{{saleEndsAt}}`. So a name is rewritten to its id token on\n * the way into storage and back on the way into the text field, the same\n * normalization the Besigner applies when an element saves, and a token that\n * would still reach a visitor as typed is reported rather than saved in\n * silence.\n *\n * Callers hold copy in its STORED form and show {@link OverlayCopyEditor.editable}\n * of it, converting each change with {@link OverlayCopyEditor.stored}. A\n * field seeded before the variables arrive then catches up on its own, with no\n * second seed to race the author's typing.\n *\n * The variables are read only once some copy holds a token, since most bars\n * and popups are plain text, and stay read after that, so deleting a token and\n * typing it again does not read them twice.\n */\nexport function useOverlayCopyEditor(options: {\n hostId: string\n /**\n * The site document, for the details `preview` fills in. Optional for an\n * editor with no preview: a `{{host.*}}` token always renders as the detail\n * or as nothing, so whether a token shows as typed never depends on it.\n */\n host?: HostTokenSource | null\n /** Whether any copy this editor holds contains a `{{` token. */\n usesTokens: boolean\n}): OverlayCopyEditor {\n const { hostId, host, usesTokens } = options\n const firestore = useFirestore()\n // Latched during render, React's pattern for state derived from an earlier\n // render: once any copy has held a token, the read stays open.\n const [wanted, setWanted] = useState(usesTokens)\n if (usesTokens && !wanted) setWanted(true)\n const { data: variableDocs, status } = useFirestoreCollection<VariableDoc>(\n () =>\n wanted\n ? query(\n collection(firestore, 'hosts', hostId, 'variables'),\n limit(VARIABLES_CEILING),\n )\n : null,\n [firestore, hostId, wanted],\n { idField: '$id' },\n )\n const settled = wanted && status !== 'loading'\n\n return useMemo(() => {\n // Keyed by id for resolution, as the published page keys them, and by\n // name for the typed form. A deleted variable is neither: the page does\n // not read one.\n const byId: Record<string, HostVariable> = {}\n const byName: Record<string, BindingDocRef> = {}\n for (const variable of variableDocs ?? []) {\n if (!variable?.name || variable.deletedAt) continue\n byId[variable.$id] = variable\n byName[variable.name] = variable\n }\n const editable = (text: string | undefined) =>\n typeof text === 'string' ? editableBindingTokens(text, byName) : ''\n const stored = (text: string | undefined) =>\n typeof text === 'string' && text.includes('{{')\n ? normalizeBindingTokens(text, byName)\n : text\n return {\n editable,\n stored,\n storedWithin: (text, max) => stored(editable(text).slice(0, max)) ?? '',\n preview: (text) => resolveOverlayCopy(stored(text) ?? '', byId, host),\n shownAsTyped: (text) =>\n settled ? overlayCopyTokensShownAsTyped(stored(text), byId, host) : [],\n pending: (...texts) =>\n !settled && texts.some((text) => Boolean(text?.includes('{{'))),\n }\n }, [variableDocs, settled, host])\n}\n\n/**\n * What a copy field says about variables while nothing in it is wrong.\n *\n * A placeholder rather than an example name: an example names a variable most\n * sites do not have, and a token for a variable the site does not have is\n * exactly what reaches a visitor as typed.\n */\nexport const OVERLAY_COPY_HELPER_TEXT =\n 'To show a variable from Logic, type its name in double braces, like {{name}}'\n\n/**\n * The helper text a copy field shows when some of its tokens would reach a\n * visitor as typed, or undefined when none would.\n */\nexport function shownAsTypedHelperText(tokens: string[]): string | undefined {\n if (!tokens.length) return undefined\n const list = tokens.join(', ')\n return (\n `Visitors would see ${list} exactly as typed. Use the name of a ` +\n 'variable from Logic, or remove the braces.'\n )\n}\n\nexport default useOverlayCopyEditor\n"],"names":["editableBindingTokens","normalizeBindingTokens","collection","limit","query","useMemo","useState","useFirestore","useFirestoreCollection","overlayCopyTokensShownAsTyped","resolveOverlayCopy","VARIABLES_CEILING","useOverlayCopyEditor","options","hostId","host","usesTokens","firestore","wanted","setWanted","data","variableDocs","status","idField","settled","byId","byName","variable","name","deletedAt","$id","editable","text","stored","includes","storedWithin","max","slice","preview","shownAsTyped","pending","texts","some","Boolean","OVERLAY_COPY_HELPER_TEXT","shownAsTypedHelperText","tokens","length","undefined","list","join"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAEEA,qBAAqB,EAGrBC,sBAAsB,QACjB,eAAc;AACrB,SAASC,UAAU,EAAEC,KAAK,EAAEC,KAAK,QAAQ,qBAAoB;AAC7D,SAASC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACzC,SACEC,YAAY,EACZC,sBAAsB,QACjB,iCAAgC;AACvC,SACEC,6BAA6B,EAC7BC,kBAAkB,QACb,2BAAuB;AAE9B;;;CAGC,GACD,MAAMC,oBAAoB;AAsC1B;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASC,qBAAqBC,OAUpC;IACC,MAAM,EAAEC,MAAM,EAAEC,IAAI,EAAEC,UAAU,EAAE,GAAGH;IACrC,MAAMI,YAAYV;IAClB,2EAA2E;IAC3E,+DAA+D;IAC/D,MAAM,CAACW,QAAQC,UAAU,GAAGb,SAASU;IACrC,IAAIA,cAAc,CAACE,QAAQC,UAAU;IACrC,MAAM,EAAEC,MAAMC,YAAY,EAAEC,MAAM,EAAE,GAAGd,uBACrC,IACEU,SACId,MACEF,WAAWe,WAAW,SAASH,QAAQ,cACvCX,MAAMQ,sBAER,MACN;QAACM;QAAWH;QAAQI;KAAO,EAC3B;QAAEK,SAAS;IAAM;IAEnB,MAAMC,UAAUN,UAAUI,WAAW;IAErC,OAAOjB,QAAQ;QACb,sEAAsE;QACtE,wEAAwE;QACxE,gBAAgB;QAChB,MAAMoB,OAAqC,CAAC;QAC5C,MAAMC,SAAwC,CAAC;QAC/C,KAAK,MAAMC,YAAYN,uBAAAA,eAAgB,EAAE,CAAE;YACzC,IAAI,EAACM,4BAAAA,SAAUC,IAAI,KAAID,SAASE,SAAS,EAAE;YAC3CJ,IAAI,CAACE,SAASG,GAAG,CAAC,GAAGH;YACrBD,MAAM,CAACC,SAASC,IAAI,CAAC,GAAGD;QAC1B;QACA,MAAMI,WAAW,CAACC,OAChB,OAAOA,SAAS,WAAWhC,sBAAsBgC,MAAMN,UAAU;QACnE,MAAMO,SAAS,CAACD,OACd,OAAOA,SAAS,YAAYA,KAAKE,QAAQ,CAAC,QACtCjC,uBAAuB+B,MAAMN,UAC7BM;QACN,OAAO;YACLD;YACAE;YACAE,cAAc,CAACH,MAAMI;oBAAQH;wBAAAA,UAAAA,OAAOF,SAASC,MAAMK,KAAK,CAAC,GAAGD,iBAA/BH,UAAwC;;YACrEK,SAAS,CAACN;oBAA4BC;uBAAnBvB,oBAAmBuB,UAAAA,OAAOD,iBAAPC,UAAgB,IAAIR,MAAMV;;YAChEwB,cAAc,CAACP,OACbR,UAAUf,8BAA8BwB,OAAOD,OAAOP,MAAMV,QAAQ,EAAE;YACxEyB,SAAS,CAAC,GAAGC,QACX,CAACjB,WAAWiB,MAAMC,IAAI,CAAC,CAACV,OAASW,QAAQX,wBAAAA,KAAME,QAAQ,CAAC;QAC5D;IACF,GAAG;QAACb;QAAcG;QAAST;KAAK;AAClC;AAEA;;;;;;CAMC,GACD,OAAO,MAAM6B,2BACX,+EAA8E;AAEhF;;;CAGC,GACD,OAAO,SAASC,uBAAuBC,MAAgB;IACrD,IAAI,CAACA,OAAOC,MAAM,EAAE,OAAOC;IAC3B,MAAMC,OAAOH,OAAOI,IAAI,CAAC;IACzB,OACE,CAAC,mBAAmB,EAAED,KAAK,qCAAqC,CAAC,GACjE;AAEJ;AAEA,eAAerC,qBAAoB"}
@@ -0,0 +1,8 @@
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
+ */
8
+ export declare const BUNDLE_ID = "marketing";
@@ -0,0 +1,9 @@
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
+ */ export const BUNDLE_ID = 'marketing';
8
+
9
+ //# sourceMappingURL=bundle-common.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/constants/bundle-common.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 */\n\nexport const BUNDLE_ID = 'marketing'\n"],"names":["BUNDLE_ID"],"mappings":"AAAA;;;;;;CAMC,GAED,OAAO,MAAMA,YAAY,YAAW"}
@@ -0,0 +1,109 @@
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
+ /**
18
+ * WHAT THE RECORDS FILED UNDER A CAMPAIGN HOLD — which is not what the
19
+ * campaign caused.
20
+ *
21
+ * Every other figure on a campaign's page is ATTRIBUTION: the campaign's mail
22
+ * carried a link, a visitor followed it, and the conversion, the money or the
23
+ * landing is credited to the campaign because that chain was recorded. The
24
+ * figures this module produces are MEMBERSHIP: somebody put a form in this
25
+ * campaign, and the form has counters of its own.
26
+ *
27
+ * The two answer different questions and a reader who adds them together gets
28
+ * a wrong number in a direction that flatters the campaign. So nothing here
29
+ * is named for causation — nothing generated, nothing driven, nothing from
30
+ * this campaign — and the rollup carries the three facts a caller needs in
31
+ * order to describe the figure honestly rather than merely print it:
32
+ *
33
+ * 1. **Whether it is windowed.** A form's flat counters are lifetime and
34
+ * include everything from before it was ever filed under this campaign.
35
+ * 2. **How many members recorded it.** A counter nothing recorded is `null`,
36
+ * and a sum over three of five forms is a different statement from a sum
37
+ * over five.
38
+ * 3. **How many members are in another campaign too.** `campaignIds` holds
39
+ * up to twenty ids, so the same submissions legitimately count toward
40
+ * more than one campaign and no total here is exclusive.
41
+ */
42
+ import { type FormPeriodRange, type FormStatKind, type FormStats, type FormStatsTotals } from '@aglyn/aglyn/app-utils/forms';
43
+ /** The dates a campaign container carries, as this module reads them. */
44
+ export interface CampaignSpan {
45
+ startAtMs?: number | null;
46
+ endAtMs?: number | null;
47
+ }
48
+ /**
49
+ * The months a campaign's dates cover, for {@link formStatsTotals}.
50
+ *
51
+ * Both ends are optional in the stored campaign and stay optional here: an
52
+ * open-ended campaign is genuinely open-ended, and substituting today for a
53
+ * missing end would silently exclude a month whose figures are still moving.
54
+ *
55
+ * A campaign with NEITHER date produces an empty range, which
56
+ * {@link formStatsTotals} answers with lifetime totals. That is the correct
57
+ * answer — there is no window to confine the figures to — and it is why the
58
+ * caller must go on saying which of the two it is showing.
59
+ */
60
+ export declare function campaignPeriodRange(span: CampaignSpan): FormPeriodRange;
61
+ /** Whether a range confines anything. */
62
+ export declare function isWindowedRange(range: FormPeriodRange): boolean;
63
+ /** One member's contribution to the rollup. */
64
+ export interface CampaignFormMember {
65
+ /** The form's own counters, already summed over the range or lifetime. */
66
+ totals: FormStatsTotals;
67
+ /** Campaigns the form is filed under, this one included. */
68
+ campaigns: number;
69
+ }
70
+ /**
71
+ * One summed counter, with the population it was summed over named.
72
+ *
73
+ * The shape `CampaignAggregate` uses for the mail figures, deliberately: a
74
+ * reader meets both on the same page, and a total that names its population
75
+ * on one section and not the other teaches that the silent one is complete.
76
+ */
77
+ export interface CampaignMemberFigure {
78
+ /** `null` when no member recorded this counter at all. */
79
+ value: number | null;
80
+ /** Members that recorded it. */
81
+ recorded: number;
82
+ /** Members the sum ran over. */
83
+ members: number;
84
+ }
85
+ /** Every membership figure for one campaign's forms. */
86
+ export interface CampaignFormsRollup extends Record<FormStatKind, CampaignMemberFigure> {
87
+ /** Forms filed under this campaign. */
88
+ members: number;
89
+ /** Of those, the ones filed under at least one other campaign as well. */
90
+ shared: number;
91
+ }
92
+ /**
93
+ * Add up what a campaign's forms hold.
94
+ *
95
+ * A counter stays `null` until some member records it, so a campaign whose
96
+ * forms have never counted views reports views as unmeasured rather than as
97
+ * zero. `recorded` is what lets the caller say "across 2 of 5 forms" where
98
+ * the two disagree — the same admission the mail rollup makes when a send is
99
+ * missing a counter.
100
+ */
101
+ export declare function campaignFormsRollup(members: readonly CampaignFormMember[]): CampaignFormsRollup;
102
+ /**
103
+ * A form's counters as this campaign may report them.
104
+ *
105
+ * Thin on purpose — the windowing rule lives in `formStatsTotals` so the
106
+ * forms surface and this one cannot disagree about which months a figure
107
+ * covers.
108
+ */
109
+ export declare function campaignFormTotals(stats: FormStats | undefined | null, range: FormPeriodRange): FormStatsTotals;
@@ -0,0 +1,113 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /**
18
+ * WHAT THE RECORDS FILED UNDER A CAMPAIGN HOLD — which is not what the
19
+ * campaign caused.
20
+ *
21
+ * Every other figure on a campaign's page is ATTRIBUTION: the campaign's mail
22
+ * carried a link, a visitor followed it, and the conversion, the money or the
23
+ * landing is credited to the campaign because that chain was recorded. The
24
+ * figures this module produces are MEMBERSHIP: somebody put a form in this
25
+ * campaign, and the form has counters of its own.
26
+ *
27
+ * The two answer different questions and a reader who adds them together gets
28
+ * a wrong number in a direction that flatters the campaign. So nothing here
29
+ * is named for causation — nothing generated, nothing driven, nothing from
30
+ * this campaign — and the rollup carries the three facts a caller needs in
31
+ * order to describe the figure honestly rather than merely print it:
32
+ *
33
+ * 1. **Whether it is windowed.** A form's flat counters are lifetime and
34
+ * include everything from before it was ever filed under this campaign.
35
+ * 2. **How many members recorded it.** A counter nothing recorded is `null`,
36
+ * and a sum over three of five forms is a different statement from a sum
37
+ * over five.
38
+ * 3. **How many members are in another campaign too.** `campaignIds` holds
39
+ * up to twenty ids, so the same submissions legitimately count toward
40
+ * more than one campaign and no total here is exclusive.
41
+ */ // Import the form-stat helpers from the context-free forms subpath, NOT the
42
+ // `@aglyn/aglyn` barrel: this file is in the marketing model, which the
43
+ // plugin's SERVER graph re-exports, and the full barrel pulls client React
44
+ // contexts (createContext) that break the RSC server build (AGL-830).
45
+ import { formPeriodKey, formStatsTotals, FORM_STAT_KINDS } from "@aglyn/aglyn/app-utils/forms";
46
+ /**
47
+ * The months a campaign's dates cover, for {@link formStatsTotals}.
48
+ *
49
+ * Both ends are optional in the stored campaign and stay optional here: an
50
+ * open-ended campaign is genuinely open-ended, and substituting today for a
51
+ * missing end would silently exclude a month whose figures are still moving.
52
+ *
53
+ * A campaign with NEITHER date produces an empty range, which
54
+ * {@link formStatsTotals} answers with lifetime totals. That is the correct
55
+ * answer — there is no window to confine the figures to — and it is why the
56
+ * caller must go on saying which of the two it is showing.
57
+ */ export function campaignPeriodRange(span) {
58
+ const from = formPeriodKey(span.startAtMs);
59
+ const to = formPeriodKey(span.endAtMs);
60
+ return _extends({}, from ? {
61
+ from
62
+ } : {}, to ? {
63
+ to
64
+ } : {});
65
+ }
66
+ /** Whether a range confines anything. */ export function isWindowedRange(range) {
67
+ return Boolean(range.from || range.to);
68
+ }
69
+ /** A figure nothing has been added to yet. */ function emptyFigure(members) {
70
+ return {
71
+ value: null,
72
+ recorded: 0,
73
+ members
74
+ };
75
+ }
76
+ /**
77
+ * Add up what a campaign's forms hold.
78
+ *
79
+ * A counter stays `null` until some member records it, so a campaign whose
80
+ * forms have never counted views reports views as unmeasured rather than as
81
+ * zero. `recorded` is what lets the caller say "across 2 of 5 forms" where
82
+ * the two disagree — the same admission the mail rollup makes when a send is
83
+ * missing a counter.
84
+ */ export function campaignFormsRollup(members) {
85
+ const rollup = {
86
+ members: members.length,
87
+ shared: 0
88
+ };
89
+ for (const kind of FORM_STAT_KINDS)rollup[kind] = emptyFigure(members.length);
90
+ for (const member of members){
91
+ if (member.campaigns > 1) rollup.shared += 1;
92
+ for (const kind of FORM_STAT_KINDS){
93
+ var _figure_value;
94
+ const value = member.totals[kind];
95
+ if (value === null) continue;
96
+ const figure = rollup[kind];
97
+ figure.value = ((_figure_value = figure.value) != null ? _figure_value : 0) + value;
98
+ figure.recorded += 1;
99
+ }
100
+ }
101
+ return rollup;
102
+ }
103
+ /**
104
+ * A form's counters as this campaign may report them.
105
+ *
106
+ * Thin on purpose — the windowing rule lives in `formStatsTotals` so the
107
+ * forms surface and this one cannot disagree about which months a figure
108
+ * covers.
109
+ */ export function campaignFormTotals(stats, range) {
110
+ return formStatsTotals(stats, isWindowedRange(range) ? range : null);
111
+ }
112
+
113
+ //# sourceMappingURL=campaign-membership-figures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/model/campaign-membership-figures.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\n/**\n * WHAT THE RECORDS FILED UNDER A CAMPAIGN HOLD — which is not what the\n * campaign caused.\n *\n * Every other figure on a campaign's page is ATTRIBUTION: the campaign's mail\n * carried a link, a visitor followed it, and the conversion, the money or the\n * landing is credited to the campaign because that chain was recorded. The\n * figures this module produces are MEMBERSHIP: somebody put a form in this\n * campaign, and the form has counters of its own.\n *\n * The two answer different questions and a reader who adds them together gets\n * a wrong number in a direction that flatters the campaign. So nothing here\n * is named for causation — nothing generated, nothing driven, nothing from\n * this campaign — and the rollup carries the three facts a caller needs in\n * order to describe the figure honestly rather than merely print it:\n *\n * 1. **Whether it is windowed.** A form's flat counters are lifetime and\n * include everything from before it was ever filed under this campaign.\n * 2. **How many members recorded it.** A counter nothing recorded is `null`,\n * and a sum over three of five forms is a different statement from a sum\n * over five.\n * 3. **How many members are in another campaign too.** `campaignIds` holds\n * up to twenty ids, so the same submissions legitimately count toward\n * more than one campaign and no total here is exclusive.\n */\n\n// Import the form-stat helpers from the context-free forms subpath, NOT the\n// `@aglyn/aglyn` barrel: this file is in the marketing model, which the\n// plugin's SERVER graph re-exports, and the full barrel pulls client React\n// contexts (createContext) that break the RSC server build (AGL-830).\nimport {\n formPeriodKey,\n formStatsTotals,\n FORM_STAT_KINDS,\n type FormPeriodRange,\n type FormStatKind,\n type FormStats,\n type FormStatsTotals,\n} from '@aglyn/aglyn/app-utils/forms'\n\n/** The dates a campaign container carries, as this module reads them. */\nexport interface CampaignSpan {\n startAtMs?: number | null\n endAtMs?: number | null\n}\n\n/**\n * The months a campaign's dates cover, for {@link formStatsTotals}.\n *\n * Both ends are optional in the stored campaign and stay optional here: an\n * open-ended campaign is genuinely open-ended, and substituting today for a\n * missing end would silently exclude a month whose figures are still moving.\n *\n * A campaign with NEITHER date produces an empty range, which\n * {@link formStatsTotals} answers with lifetime totals. That is the correct\n * answer — there is no window to confine the figures to — and it is why the\n * caller must go on saying which of the two it is showing.\n */\nexport function campaignPeriodRange(span: CampaignSpan): FormPeriodRange {\n const from = formPeriodKey(span.startAtMs)\n const to = formPeriodKey(span.endAtMs)\n return { ...(from ? { from } : {}), ...(to ? { to } : {}) }\n}\n\n/** Whether a range confines anything. */\nexport function isWindowedRange(range: FormPeriodRange): boolean {\n return Boolean(range.from || range.to)\n}\n\n/** One member's contribution to the rollup. */\nexport interface CampaignFormMember {\n /** The form's own counters, already summed over the range or lifetime. */\n totals: FormStatsTotals\n /** Campaigns the form is filed under, this one included. */\n campaigns: number\n}\n\n/**\n * One summed counter, with the population it was summed over named.\n *\n * The shape `CampaignAggregate` uses for the mail figures, deliberately: a\n * reader meets both on the same page, and a total that names its population\n * on one section and not the other teaches that the silent one is complete.\n */\nexport interface CampaignMemberFigure {\n /** `null` when no member recorded this counter at all. */\n value: number | null\n /** Members that recorded it. */\n recorded: number\n /** Members the sum ran over. */\n members: number\n}\n\n/** Every membership figure for one campaign's forms. */\nexport interface CampaignFormsRollup extends Record<FormStatKind, CampaignMemberFigure> {\n /** Forms filed under this campaign. */\n members: number\n /** Of those, the ones filed under at least one other campaign as well. */\n shared: number\n}\n\n/** A figure nothing has been added to yet. */\nfunction emptyFigure(members: number): CampaignMemberFigure {\n return { value: null, recorded: 0, members }\n}\n\n/**\n * Add up what a campaign's forms hold.\n *\n * A counter stays `null` until some member records it, so a campaign whose\n * forms have never counted views reports views as unmeasured rather than as\n * zero. `recorded` is what lets the caller say \"across 2 of 5 forms\" where\n * the two disagree — the same admission the mail rollup makes when a send is\n * missing a counter.\n */\nexport function campaignFormsRollup(\n members: readonly CampaignFormMember[],\n): CampaignFormsRollup {\n const rollup = {\n members: members.length,\n shared: 0,\n } as CampaignFormsRollup\n for (const kind of FORM_STAT_KINDS) rollup[kind] = emptyFigure(members.length)\n for (const member of members) {\n if (member.campaigns > 1) rollup.shared += 1\n for (const kind of FORM_STAT_KINDS) {\n const value = member.totals[kind]\n if (value === null) continue\n const figure = rollup[kind]\n figure.value = (figure.value ?? 0) + value\n figure.recorded += 1\n }\n }\n return rollup\n}\n\n/**\n * A form's counters as this campaign may report them.\n *\n * Thin on purpose — the windowing rule lives in `formStatsTotals` so the\n * forms surface and this one cannot disagree about which months a figure\n * covers.\n */\nexport function campaignFormTotals(\n stats: FormStats | undefined | null,\n range: FormPeriodRange,\n): FormStatsTotals {\n return formStatsTotals(stats, isWindowedRange(range) ? range : null)\n}\n"],"names":["formPeriodKey","formStatsTotals","FORM_STAT_KINDS","campaignPeriodRange","span","from","startAtMs","to","endAtMs","isWindowedRange","range","Boolean","emptyFigure","members","value","recorded","campaignFormsRollup","rollup","length","shared","kind","member","campaigns","figure","totals","campaignFormTotals","stats"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GAED,4EAA4E;AAC5E,wEAAwE;AACxE,2EAA2E;AAC3E,sEAAsE;AACtE,SACEA,aAAa,EACbC,eAAe,EACfC,eAAe,QAKV,+BAA8B;AAQrC;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,oBAAoBC,IAAkB;IACpD,MAAMC,OAAOL,cAAcI,KAAKE,SAAS;IACzC,MAAMC,KAAKP,cAAcI,KAAKI,OAAO;IACrC,OAAO,aAAMH,OAAO;QAAEA;IAAK,IAAI,CAAC,GAAQE,KAAK;QAAEA;IAAG,IAAI,CAAC;AACzD;AAEA,uCAAuC,GACvC,OAAO,SAASE,gBAAgBC,KAAsB;IACpD,OAAOC,QAAQD,MAAML,IAAI,IAAIK,MAAMH,EAAE;AACvC;AAkCA,4CAA4C,GAC5C,SAASK,YAAYC,OAAe;IAClC,OAAO;QAAEC,OAAO;QAAMC,UAAU;QAAGF;IAAQ;AAC7C;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASG,oBACdH,OAAsC;IAEtC,MAAMI,SAAS;QACbJ,SAASA,QAAQK,MAAM;QACvBC,QAAQ;IACV;IACA,KAAK,MAAMC,QAAQlB,gBAAiBe,MAAM,CAACG,KAAK,GAAGR,YAAYC,QAAQK,MAAM;IAC7E,KAAK,MAAMG,UAAUR,QAAS;QAC5B,IAAIQ,OAAOC,SAAS,GAAG,GAAGL,OAAOE,MAAM,IAAI;QAC3C,KAAK,MAAMC,QAAQlB,gBAAiB;gBAIlBqB;YAHhB,MAAMT,QAAQO,OAAOG,MAAM,CAACJ,KAAK;YACjC,IAAIN,UAAU,MAAM;YACpB,MAAMS,SAASN,MAAM,CAACG,KAAK;YAC3BG,OAAOT,KAAK,GAAG,EAACS,gBAAAA,OAAOT,KAAK,YAAZS,gBAAgB,KAAKT;YACrCS,OAAOR,QAAQ,IAAI;QACrB;IACF;IACA,OAAOE;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASQ,mBACdC,KAAmC,EACnChB,KAAsB;IAEtB,OAAOT,gBAAgByB,OAAOjB,gBAAgBC,SAASA,QAAQ;AACjE"}
@@ -0,0 +1,65 @@
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 HostAction } from '@aglyn/aglyn/app-utils/actions';
18
+ import type { ClientAutomation } from './site-contract';
19
+ /** A raw `hosts/{hostId}/actions` doc paired with its id. */
20
+ export interface RawHostAction {
21
+ id: string;
22
+ action: HostAction;
23
+ }
24
+ export interface CompileClientAutomationsOptions {
25
+ /** Leading-slash page path for pathPattern targeting. */
26
+ path: string;
27
+ /**
28
+ * `actions` entitlement (AGL-577). Basic presentational steps
29
+ * (menu/drawer/show-hide/class/nav/alert) always load; advanced client
30
+ * steps (overlay/showHtml/analytics) and server steps drop when false.
31
+ */
32
+ actionsEntitled: boolean;
33
+ /** Business-tier gate for `runJs` steps (dropped when false). */
34
+ allowJs: boolean;
35
+ /**
36
+ * Ignore each action's `pathPattern` (AGL-830). The besigner Preview shows
37
+ * a single composed screen and scopes relevance by selector match, so it
38
+ * skips path filtering rather than resolve the composed path.
39
+ */
40
+ matchAllPaths?: boolean;
41
+ /**
42
+ * Drop every automation that names a `pathPattern` (AGL-2511), for a
43
+ * surface whose own path is unknown — the designed 404 body, served from a
44
+ * per-host cache that no request path belongs to.
45
+ *
46
+ * The opposite of `matchAllPaths` and deliberately so: Preview knows the
47
+ * page and only lacks its composed address, so it keeps path-scoped
48
+ * automations; this caller cannot know the page at all, and running an
49
+ * automation scoped to `/pricing` on whatever URL 404'd would be a guess
50
+ * with a side effect. Interactions authored on nodes carry no pattern, so
51
+ * a nav's hover choreography — the reason this surface is enriched — is
52
+ * unaffected.
53
+ */
54
+ dropPathScoped?: boolean;
55
+ }
56
+ /**
57
+ * Pure action→client-automation mapping (AGL-256/577, extracted AGL-830):
58
+ * the single place the trigger config + entitlement-trimmed client steps are
59
+ * shaped. Both the server page-enricher (admin fetch) and the editor Preview
60
+ * (client fetch) feed their loaded action docs through this, so live and
61
+ * preview stay byte-for-byte consistent. No I/O, no firebase — safe on the
62
+ * client. Malformed entries are skipped, never thrown.
63
+ */
64
+ export declare function compileClientAutomations(actions: RawHostAction[], options: CompileClientAutomationsOptions): ClientAutomation[];
65
+ export default compileClientAutomations;
@@ -0,0 +1,95 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ // Import the entitlement/event predicates from the context-free actions
18
+ // subpath, NOT the `@aglyn/aglyn` barrel: this file is in the marketing model,
19
+ // which the plugin's SERVER graph re-exports, and the full barrel pulls client
20
+ // React contexts (createContext) that break the RSC server build (AGL-830).
21
+ import { hostActionStepsForClient, isClientActionStep, isClientStepEntitled, isSiteEventType } from "@aglyn/aglyn/app-utils/actions";
22
+ import { overlayMatchesPath } from "./overlays.js";
23
+ /**
24
+ * Pure action→client-automation mapping (AGL-256/577, extracted AGL-830):
25
+ * the single place the trigger config + entitlement-trimmed client steps are
26
+ * shaped. Both the server page-enricher (admin fetch) and the editor Preview
27
+ * (client fetch) feed their loaded action docs through this, so live and
28
+ * preview stay byte-for-byte consistent. No I/O, no firebase — safe on the
29
+ * client. Malformed entries are skipped, never thrown.
30
+ */ export function compileClientAutomations(actions, options) {
31
+ const automations = [];
32
+ for (const { id, action } of actions){
33
+ var _ref, _action_steps;
34
+ var _action_trigger, _action_trigger_pathPattern, _action_trigger1, _action_trigger2, _action_trigger3, _action_trigger4, _action_trigger5, _action_trigger6, _action_trigger7, _action_trigger8;
35
+ if (action.deletedAt || action.enabled === false) {
36
+ continue;
37
+ }
38
+ const event = String((_ref = (_action_trigger = action.trigger) == null ? void 0 : _action_trigger.event) != null ? _ref : '');
39
+ if (!isSiteEventType(event)) continue;
40
+ const pathPattern = (_action_trigger1 = action.trigger) == null ? void 0 : (_action_trigger_pathPattern = _action_trigger1.pathPattern) == null ? void 0 : _action_trigger_pathPattern.trim();
41
+ if (pathPattern && options.dropPathScoped) continue;
42
+ if (!options.matchAllPaths && pathPattern && !overlayMatchesPath({
43
+ pathPatterns: [
44
+ pathPattern
45
+ ]
46
+ }, options.path)) {
47
+ continue;
48
+ }
49
+ /*
50
+ * TRUNCATED AT THE FIRST WAIT, before anything else is decided.
51
+ *
52
+ * The browser runs its slice of the list the moment the trigger fires, so
53
+ * a client step sitting after a `wait` would execute at once — the popup
54
+ * an author scheduled for three days later would appear immediately, the
55
+ * delay would look correct on the server, and the two halves of one
56
+ * authored flow would disagree about when they happened. Everything past
57
+ * the first wait belongs to a run the job beat has not started yet.
58
+ */ const reachableSteps = hostActionStepsForClient(action.steps);
59
+ // Step-level entitlement tiering (AGL-577): basic presentational steps
60
+ // load on every plan; advanced client steps need `actions` and runJs
61
+ // needs `webhooks` — see isClientStepEntitled.
62
+ const clientSteps = reachableSteps.filter((step)=>isClientStepEntitled(step, {
63
+ actionsEntitled: options.actionsEntitled,
64
+ allowJs: options.allowJs
65
+ }));
66
+ // Server steps only dispatch (and are only re-authorized) when the org is
67
+ // actions-entitled — never advertise them otherwise, so basic-tier pages
68
+ // don't POST to /api/events/dispatch for a no-op.
69
+ const hasServerSteps = options.actionsEntitled && ((_action_steps = action.steps) != null ? _action_steps : []).some((step)=>!isClientActionStep(step));
70
+ if (!clientSteps.length && !hasServerSteps) continue;
71
+ automations.push(_extends({
72
+ id,
73
+ event
74
+ }, ((_action_trigger2 = action.trigger) == null ? void 0 : _action_trigger2.selector) ? {
75
+ selector: action.trigger.selector
76
+ } : {}, Number((_action_trigger3 = action.trigger) == null ? void 0 : _action_trigger3.threshold) > 0 ? {
77
+ threshold: Number(action.trigger.threshold)
78
+ } : {}, ((_action_trigger4 = action.trigger) == null ? void 0 : _action_trigger4.oncePerVisitor) === true ? {
79
+ oncePerVisitor: true
80
+ } : {}, ((_action_trigger5 = action.trigger) == null ? void 0 : _action_trigger5.oncePerSession) === true ? {
81
+ oncePerSession: true
82
+ } : {}, Number((_action_trigger6 = action.trigger) == null ? void 0 : _action_trigger6.cooldownMinutes) >= 1 ? {
83
+ cooldownMinutes: Number((_action_trigger7 = action.trigger) == null ? void 0 : _action_trigger7.cooldownMinutes)
84
+ } : {}, ((_action_trigger8 = action.trigger) == null ? void 0 : _action_trigger8.everyTime) === true ? {
85
+ everyTime: true
86
+ } : {}, {
87
+ steps: clientSteps,
88
+ hasServerSteps
89
+ }));
90
+ }
91
+ return automations;
92
+ }
93
+ export default compileClientAutomations;
94
+
95
+ //# sourceMappingURL=compile-client-automations.js.map