@aglyn/plugins-marketing 1.0.0-beta.179 → 1.0.0-beta.180

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 (71) hide show
  1. package/README.md +3 -3
  2. package/package.json +15 -15
  3. package/src/lib/components/campaign-conversions-card.js +22 -4
  4. package/src/lib/components/campaign-conversions-card.js.map +1 -1
  5. package/src/lib/components/campaign-detail-card.js +3 -4
  6. package/src/lib/components/campaign-detail-card.js.map +1 -1
  7. package/src/lib/components/campaign-report-card.js +3 -3
  8. package/src/lib/components/campaign-report-card.js.map +1 -1
  9. package/src/lib/components/email-compose-card.d.ts +5 -2
  10. package/src/lib/components/email-compose-card.js +2 -3
  11. package/src/lib/components/email-compose-card.js.map +1 -1
  12. package/src/lib/components/email-detail.d.ts +5 -2
  13. package/src/lib/components/email-detail.js +4 -4
  14. package/src/lib/components/email-detail.js.map +1 -1
  15. package/src/lib/components/email-messages-widget.d.ts +17 -2
  16. package/src/lib/components/email-messages-widget.js +33 -2
  17. package/src/lib/components/email-messages-widget.js.map +1 -1
  18. package/src/lib/components/emails-list-card.d.ts +9 -6
  19. package/src/lib/components/emails-list-card.js +9 -9
  20. package/src/lib/components/emails-list-card.js.map +1 -1
  21. package/src/lib/components/host-experiments-card.component.d.ts +2 -0
  22. package/src/lib/components/host-experiments-card.component.js +20 -25
  23. package/src/lib/components/host-experiments-card.component.js.map +1 -1
  24. package/src/lib/components/host-marketing-summary-card.component.d.ts +15 -0
  25. package/src/lib/components/host-marketing-summary-card.component.js +59 -68
  26. package/src/lib/components/host-marketing-summary-card.component.js.map +1 -1
  27. package/src/lib/components/host-overlays-card.component.js +15 -37
  28. package/src/lib/components/host-overlays-card.component.js.map +1 -1
  29. package/src/lib/components/inbox-campaigns-widget.d.ts +23 -0
  30. package/src/lib/components/inbox-campaigns-widget.js +63 -0
  31. package/src/lib/components/inbox-campaigns-widget.js.map +1 -0
  32. package/src/lib/components/marketing-aggregates.d.ts +95 -0
  33. package/src/lib/components/marketing-aggregates.js +114 -0
  34. package/src/lib/components/marketing-aggregates.js.map +1 -0
  35. package/src/lib/components/marketing-console-page.js +32 -27
  36. package/src/lib/components/marketing-console-page.js.map +1 -1
  37. package/src/lib/components/marketing-console-sections.d.ts +24 -13
  38. package/src/lib/components/marketing-console-sections.js +37 -12
  39. package/src/lib/components/marketing-console-sections.js.map +1 -1
  40. package/src/lib/components/org-conversions-section.d.ts +32 -0
  41. package/src/lib/components/org-conversions-section.js +130 -0
  42. package/src/lib/components/org-conversions-section.js.map +1 -0
  43. package/src/lib/components/org-experiments-card.d.ts +32 -0
  44. package/src/lib/components/org-experiments-card.js +407 -0
  45. package/src/lib/components/org-experiments-card.js.map +1 -0
  46. package/src/lib/components/org-marketing-overview-card.d.ts +34 -0
  47. package/src/lib/components/org-marketing-overview-card.js +342 -0
  48. package/src/lib/components/org-marketing-overview-card.js.map +1 -0
  49. package/src/lib/components/org-overlays-card.d.ts +35 -0
  50. package/src/lib/components/org-overlays-card.js +345 -0
  51. package/src/lib/components/org-overlays-card.js.map +1 -0
  52. package/src/lib/components/org-site-picker-button.d.ts +26 -0
  53. package/src/lib/components/org-site-picker-button.js +104 -0
  54. package/src/lib/components/org-site-picker-button.js.map +1 -0
  55. package/src/lib/components/overlay-labels.d.ts +38 -0
  56. package/src/lib/components/overlay-labels.js +55 -0
  57. package/src/lib/components/overlay-labels.js.map +1 -0
  58. package/src/lib/components/use-emails-hub-path.d.ts +17 -8
  59. package/src/lib/components/use-emails-hub-path.js +29 -11
  60. package/src/lib/components/use-emails-hub-path.js.map +1 -1
  61. package/src/lib/components/use-one-shot-reads.d.ts +55 -0
  62. package/src/lib/components/use-one-shot-reads.js +168 -0
  63. package/src/lib/components/use-one-shot-reads.js.map +1 -0
  64. package/src/lib/model/experiments.d.ts +35 -0
  65. package/src/lib/model/experiments.js +34 -0
  66. package/src/lib/model/experiments.js.map +1 -1
  67. package/src/lib/model/overlays.d.ts +13 -2
  68. package/src/lib/model/overlays.js +18 -6
  69. package/src/lib/model/overlays.js.map +1 -1
  70. package/src/lib/plugin.js +13 -8
  71. package/src/lib/plugin.js.map +1 -1
@@ -0,0 +1,345 @@
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 { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
+ import { checkEntitlement, pluginDocsHelp } from "@aglyn/aglyn";
20
+ import { compareOverlayPrecedence, overlayStatus } from "../model/index.js";
21
+ import { AppLink, CardDisplay } from "@aglyn/shared-ui-jsx";
22
+ import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
23
+ import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
24
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
25
+ import { Alert, Chip, Stack, Switch, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material";
26
+ import { collection, doc, getDoc, getDocs, limit, orderBy, query, setDoc } from "firebase/firestore";
27
+ import { Fragment, useState } from "react";
28
+ import { useFirestore } from "@aglyn/tenant-feature-instance";
29
+ import { ceilingedWindow } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
30
+ import { useMarketingOrgMount } from "./marketing-org-mount.js";
31
+ import { OrgSitePickerButton, orgSiteSectionHref } from "./org-site-picker-button.js";
32
+ import { overlayDisplayName, overlayEngagementLabel, overlayPagesLabel, overlayWindowLabel } from "./overlay-labels.js";
33
+ import { ORG_SITES_PER_PAGE, useOrgSiteReads } from "./use-one-shot-reads.js";
34
+ /**
35
+ * How many overlays are read for each site.
36
+ *
37
+ * Smaller than the site's own ceiling on purpose: a site runs a handful of
38
+ * bars and popups, a page here holds a page of sites' worth of them, and a
39
+ * site with more says so on its row and links to its own list, where every
40
+ * one of them is — and where the order they take can be changed.
41
+ */ export const ORG_OVERLAYS_PER_SITE = 10;
42
+ /**
43
+ * One site's overlays and its defaults: the overlays ordered and ceilinged
44
+ * the way the site's own list reads them — `name` is on every overlay its
45
+ * only creator writes — and the host document for the defaults, which the
46
+ * console shell already listens to for its site list.
47
+ */ async function readSiteOverlays(firestore, hostId) {
48
+ var _site_announcementBar, _site_announcementBar1, _site_popup, _site_popup1;
49
+ const [overlays, host] = await Promise.all([
50
+ getDocs(query(collection(firestore, 'hosts', hostId, 'overlays'), orderBy('name'), limit(ORG_OVERLAYS_PER_SITE + 1))),
51
+ getDoc(doc(firestore, 'hosts', hostId)).catch(()=>null)
52
+ ]);
53
+ const { rows, truncated } = ceilingedWindow(overlays.docs.map((entry)=>_extends({}, entry.data(), {
54
+ $id: entry.id
55
+ })), ORG_OVERLAYS_PER_SITE);
56
+ const site = (host == null ? void 0 : host.exists()) ? host.data() : null;
57
+ return {
58
+ // In the order the site shows them. A window the ceiling cut short is
59
+ // the first overlays by name, and the site's line says so.
60
+ rows: [
61
+ ...rows
62
+ ].sort(compareOverlayPrecedence),
63
+ truncated,
64
+ // The same test the page enricher applies before it draws either one.
65
+ defaults: site ? {
66
+ bar: Boolean(((_site_announcementBar = site.announcementBar) == null ? void 0 : _site_announcementBar.enabled) && ((_site_announcementBar1 = site.announcementBar) == null ? void 0 : _site_announcementBar1.text)),
67
+ popup: Boolean(((_site_popup = site.popup) == null ? void 0 : _site_popup.enabled) && ((_site_popup1 = site.popup) == null ? void 0 : _site_popup1.body))
68
+ } : null
69
+ };
70
+ }
71
+ const STATUS_CHIP = {
72
+ off: {
73
+ label: 'Off',
74
+ color: 'default'
75
+ },
76
+ scheduled: {
77
+ label: 'Scheduled',
78
+ color: 'info'
79
+ },
80
+ live: {
81
+ label: 'Live',
82
+ color: 'success'
83
+ }
84
+ };
85
+ /** Columns in a row, so a site's own line can span all of them. */ const COLUMNS = 7;
86
+ /**
87
+ * Every site's announcement bars and popups, on the organization's
88
+ * Marketing hub.
89
+ *
90
+ * READ-ONLY but for the switch. An overlay is drawn on one site's pages and
91
+ * written against that site's variables, screens and order, so its editor
92
+ * stays on the site: each row's Edit and the New button open the site's own
93
+ * Overlays section. What the org hub adds is the view across sites — which
94
+ * of them are showing what, and their lifetime engagement — and the one
95
+ * change that needs no editor, turning an overlay on or off.
96
+ *
97
+ * Grouped by site, each group in the order the site shows its overlays,
98
+ * under a line naming the site and whether its default bar and popup are
99
+ * on. No 14-day analytics row: that is a document read per day per site,
100
+ * and it lives on the site.
101
+ */ export function OrgOverlaysCard(props) {
102
+ var _ref;
103
+ const { org } = props;
104
+ const mount = useMarketingOrgMount();
105
+ const firestore = useFirestore();
106
+ const { enqueueSnackbar } = useSnackbar();
107
+ const entitled = checkEntitlement(org, 'marketingOverlays');
108
+ const sites = (_ref = mount == null ? void 0 : mount.hosts) != null ? _ref : [];
109
+ const [page, setPage] = useState(0);
110
+ const pageSites = sites.slice(page * ORG_SITES_PER_PAGE, (page + 1) * ORG_SITES_PER_PAGE);
111
+ const { reads, patch } = useOrgSiteReads(entitled ? pageSites.map((site)=>site.id) : [], (hostId)=>readSiteOverlays(firestore, hostId), [
112
+ firestore,
113
+ entitled
114
+ ]);
115
+ /**
116
+ * One field, merged, on the row's own site — the write the site's own
117
+ * switch makes, and one the rules allow every editor of that site.
118
+ */ const handleToggle = async (hostId, overlay)=>{
119
+ const enabled = overlay.enabled === false;
120
+ try {
121
+ await setDoc(doc(firestore, 'hosts', hostId, 'overlays', overlay.$id), {
122
+ enabled
123
+ }, {
124
+ merge: true
125
+ });
126
+ patch(hostId, (site)=>_extends({}, site, {
127
+ rows: site.rows.map((row)=>row.$id === overlay.$id ? _extends({}, row, {
128
+ enabled
129
+ }) : row)
130
+ }));
131
+ } catch (error) {
132
+ console.error(error);
133
+ enqueueSnackbar('The overlay could not be switched', {
134
+ variant: 'error'
135
+ });
136
+ }
137
+ };
138
+ return /*#__PURE__*/ _jsx(CardDisplay, {
139
+ header: "Announcement bars & popups",
140
+ subheader: "Every site in this organization",
141
+ help: pluginDocsHelp('marketingOverlays', {
142
+ anchor: '#across-your-sites'
143
+ }),
144
+ contentGutterX: true,
145
+ contentGutterY: true,
146
+ contentBordered: "all",
147
+ children: !entitled ? /*#__PURE__*/ _jsx(Alert, {
148
+ severity: "info",
149
+ children: 'Announcement bars and popups are included from the Starter ' + 'plan — see Billing to upgrade.'
150
+ }) : !mount || !mount.hostsReady ? null : !sites.length ? /*#__PURE__*/ _jsx(Typography, {
151
+ variant: "body2",
152
+ color: "text.secondary",
153
+ children: 'This organization has no sites yet. Overlays are drawn on a ' + 'site’s pages, so they are created on a site.'
154
+ }) : /*#__PURE__*/ _jsxs(Stack, {
155
+ spacing: 2,
156
+ children: [
157
+ /*#__PURE__*/ _jsxs(Stack, {
158
+ direction: "row",
159
+ spacing: 2,
160
+ sx: {
161
+ alignItems: 'center',
162
+ justifyContent: 'space-between'
163
+ },
164
+ children: [
165
+ /*#__PURE__*/ _jsx(Typography, {
166
+ variant: "body2",
167
+ color: "text.secondary",
168
+ children: 'Each site shows the first matching bar and popup in its own ' + 'order. Edit and create them on the site; switch them on or ' + 'off here.'
169
+ }),
170
+ /*#__PURE__*/ _jsx(OrgSitePickerButton, {
171
+ mount: mount,
172
+ section: "overlays",
173
+ label: "New overlay"
174
+ })
175
+ ]
176
+ }),
177
+ /*#__PURE__*/ _jsxs(ScrollTable, {
178
+ size: "small",
179
+ "aria-label": "Overlays by site",
180
+ children: [
181
+ /*#__PURE__*/ _jsx(TableHead, {
182
+ children: /*#__PURE__*/ _jsxs(TableRow, {
183
+ children: [
184
+ /*#__PURE__*/ _jsx(TableCell, {
185
+ children: 'Overlay'
186
+ }),
187
+ /*#__PURE__*/ _jsx(TableCell, {
188
+ children: 'Kind'
189
+ }),
190
+ /*#__PURE__*/ _jsx(TableCell, {
191
+ children: 'Status'
192
+ }),
193
+ /*#__PURE__*/ _jsx(TableCell, {
194
+ children: 'Window'
195
+ }),
196
+ /*#__PURE__*/ _jsx(TableCell, {
197
+ children: 'Pages'
198
+ }),
199
+ /*#__PURE__*/ _jsx(TableCell, {
200
+ children: 'Engagement'
201
+ }),
202
+ /*#__PURE__*/ _jsx(TableCell, {
203
+ align: "right",
204
+ children: 'Actions'
205
+ })
206
+ ]
207
+ })
208
+ }),
209
+ /*#__PURE__*/ _jsx(TableBody, {
210
+ children: pageSites.map((site)=>{
211
+ var _ref, _ref1;
212
+ const read = reads.get(site.id);
213
+ const answer = (_ref = read == null ? void 0 : read.value) != null ? _ref : null;
214
+ const href = orgSiteSectionHref(mount, site.id, 'overlays');
215
+ const name = site.name || site.subdomain || site.id;
216
+ return /*#__PURE__*/ _jsxs(Fragment, {
217
+ children: [
218
+ /*#__PURE__*/ _jsx(TableRow, {
219
+ children: /*#__PURE__*/ _jsx(TableCell, {
220
+ colSpan: COLUMNS,
221
+ children: /*#__PURE__*/ _jsxs(Stack, {
222
+ direction: "row",
223
+ spacing: 2,
224
+ sx: {
225
+ alignItems: 'baseline',
226
+ flexWrap: 'wrap'
227
+ },
228
+ children: [
229
+ /*#__PURE__*/ _jsx(Typography, {
230
+ variant: "subtitle2",
231
+ children: href ? /*#__PURE__*/ _jsx(AppLink, {
232
+ href: href,
233
+ children: name
234
+ }) : name
235
+ }),
236
+ (answer == null ? void 0 : answer.defaults) ? /*#__PURE__*/ _jsx(Typography, {
237
+ variant: "caption",
238
+ color: "text.secondary",
239
+ children: `Default bar ${answer.defaults.bar ? 'on' : 'off'} · ` + `default popup ${answer.defaults.popup ? 'on' : 'off'}`
240
+ }) : null,
241
+ (answer == null ? void 0 : answer.truncated) ? /*#__PURE__*/ _jsx(Typography, {
242
+ variant: "caption",
243
+ color: "text.secondary",
244
+ children: `More than ${ORG_OVERLAYS_PER_SITE} on this ` + 'site — these are the first by name; the ' + 'site’s own list has every one.'
245
+ }) : null
246
+ ]
247
+ })
248
+ })
249
+ }),
250
+ (read == null ? void 0 : read.status) === 'error' ? /*#__PURE__*/ _jsx(TableRow, {
251
+ children: /*#__PURE__*/ _jsx(TableCell, {
252
+ colSpan: COLUMNS,
253
+ children: /*#__PURE__*/ _jsx(Typography, {
254
+ variant: "body2",
255
+ color: "text.secondary",
256
+ children: 'This site’s overlays could not be read.'
257
+ })
258
+ })
259
+ }) : answer && !answer.rows.length ? /*#__PURE__*/ _jsx(TableRow, {
260
+ children: /*#__PURE__*/ _jsx(TableCell, {
261
+ colSpan: COLUMNS,
262
+ children: /*#__PURE__*/ _jsx(Typography, {
263
+ variant: "body2",
264
+ color: "text.secondary",
265
+ children: 'No overlays on this site.'
266
+ })
267
+ })
268
+ }) : ((_ref1 = answer == null ? void 0 : answer.rows) != null ? _ref1 : []).map((overlay)=>{
269
+ const status = STATUS_CHIP[overlayStatus(overlay, Date.now())];
270
+ return /*#__PURE__*/ _jsxs(TableRow, {
271
+ children: [
272
+ /*#__PURE__*/ _jsx(TableCell, {
273
+ children: overlayDisplayName(overlay)
274
+ }),
275
+ /*#__PURE__*/ _jsx(TableCell, {
276
+ children: overlay.kind === 'bar' ? 'Bar' : 'Popup'
277
+ }),
278
+ /*#__PURE__*/ _jsx(TableCell, {
279
+ children: /*#__PURE__*/ _jsx(Chip, {
280
+ size: "small",
281
+ color: status.color,
282
+ label: status.label
283
+ })
284
+ }),
285
+ /*#__PURE__*/ _jsx(TableCell, {
286
+ children: overlayWindowLabel(overlay)
287
+ }),
288
+ /*#__PURE__*/ _jsx(TableCell, {
289
+ children: overlayPagesLabel(overlay)
290
+ }),
291
+ /*#__PURE__*/ _jsx(TableCell, {
292
+ children: /*#__PURE__*/ _jsx(Typography, {
293
+ variant: "caption",
294
+ color: "text.secondary",
295
+ children: overlayEngagementLabel(overlay.stats)
296
+ })
297
+ }),
298
+ /*#__PURE__*/ _jsxs(TableCell, {
299
+ align: "right",
300
+ children: [
301
+ /*#__PURE__*/ _jsx(Switch, {
302
+ size: "small",
303
+ checked: overlay.enabled !== false,
304
+ onChange: ()=>void handleToggle(site.id, overlay),
305
+ slotProps: {
306
+ input: {
307
+ 'aria-label': `Show ${overlayDisplayName(overlay)} on ${name}`
308
+ }
309
+ }
310
+ }),
311
+ href ? /*#__PURE__*/ _jsx(AppLink, {
312
+ componentVariant: "button",
313
+ href: href,
314
+ size: "small",
315
+ color: "primary",
316
+ "aria-label": `Edit ${overlayDisplayName(overlay)} on ${name}`,
317
+ children: 'Edit'
318
+ }) : null
319
+ ]
320
+ })
321
+ ]
322
+ }, overlay.$id);
323
+ })
324
+ ]
325
+ }, site.id);
326
+ })
327
+ })
328
+ ]
329
+ }),
330
+ /*#__PURE__*/ _jsx(ListPagination, {
331
+ page: page,
332
+ pageSize: ORG_SITES_PER_PAGE,
333
+ rowCount: pageSites.length,
334
+ count: sites.length,
335
+ onPageChange: setPage,
336
+ labelDisplayedRows: ({ from, to, count })=>`Sites ${from}–${to} of ${count}`
337
+ })
338
+ ]
339
+ })
340
+ });
341
+ }
342
+ OrgOverlaysCard.displayName = 'OrgOverlaysCard';
343
+ export default OrgOverlaysCard;
344
+
345
+ //# sourceMappingURL=org-overlays-card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/org-overlays-card.tsx"],"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 AglynOrgBilling,\n checkEntitlement,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { compareOverlayPrecedence, overlayStatus, type HostOverlay } from '../model'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Chip,\n Stack,\n Switch,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n getDoc,\n getDocs,\n limit,\n orderBy,\n query,\n setDoc,\n type Firestore,\n} from 'firebase/firestore'\nimport { Fragment, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { useMarketingOrgMount } from './marketing-org-mount'\nimport {\n OrgSitePickerButton,\n orgSiteSectionHref,\n} from './org-site-picker-button'\nimport {\n overlayDisplayName,\n overlayEngagementLabel,\n overlayPagesLabel,\n overlayWindowLabel,\n} from './overlay-labels'\nimport { ORG_SITES_PER_PAGE, useOrgSiteReads } from './use-one-shot-reads'\n\n/**\n * How many overlays are read for each site.\n *\n * Smaller than the site's own ceiling on purpose: a site runs a handful of\n * bars and popups, a page here holds a page of sites' worth of them, and a\n * site with more says so on its row and links to its own list, where every\n * one of them is — and where the order they take can be changed.\n */\nexport const ORG_OVERLAYS_PER_SITE = 10\n\ntype OverlayRow = HostOverlay & { $id: string }\n\n/** What one site's group of rows is drawn from. */\ninterface SiteOverlays {\n /** In the order the site shows them: see `compareOverlayPrecedence`. */\n rows: OverlayRow[]\n /** The site has more overlays than were read. */\n truncated: boolean\n /**\n * Whether the site's always-on default bar and popup would show — the ones\n * a matching overlay takes priority over. `null` when the site document\n * could not be read, which is drawn as nothing rather than as \"off\".\n */\n defaults: { bar: boolean; popup: boolean } | null\n}\n\n/**\n * One site's overlays and its defaults: the overlays ordered and ceilinged\n * the way the site's own list reads them — `name` is on every overlay its\n * only creator writes — and the host document for the defaults, which the\n * console shell already listens to for its site list.\n */\nasync function readSiteOverlays(\n firestore: Firestore,\n hostId: string,\n): Promise<SiteOverlays> {\n const [overlays, host] = await Promise.all([\n getDocs(\n query(\n collection(firestore, 'hosts', hostId, 'overlays'),\n orderBy('name'),\n limit(ORG_OVERLAYS_PER_SITE + 1),\n ),\n ),\n getDoc(doc(firestore, 'hosts', hostId)).catch(() => null),\n ])\n const { rows, truncated } = ceilingedWindow(\n overlays.docs.map(\n (entry) => ({ ...(entry.data() as HostOverlay), $id: entry.id }) as OverlayRow,\n ),\n ORG_OVERLAYS_PER_SITE,\n )\n const site = host?.exists() ? (host.data() as Record<string, any>) : null\n return {\n // In the order the site shows them. A window the ceiling cut short is\n // the first overlays by name, and the site's line says so.\n rows: [...rows].sort(compareOverlayPrecedence),\n truncated,\n // The same test the page enricher applies before it draws either one.\n defaults: site\n ? {\n bar: Boolean(site.announcementBar?.enabled && site.announcementBar?.text),\n popup: Boolean(site.popup?.enabled && site.popup?.body),\n }\n : null,\n }\n}\n\nconst STATUS_CHIP: Record<\n ReturnType<typeof overlayStatus>,\n { label: string; color: 'default' | 'info' | 'success' }\n> = {\n off: { label: 'Off', color: 'default' },\n scheduled: { label: 'Scheduled', color: 'info' },\n live: { label: 'Live', color: 'success' },\n}\n\n/** Columns in a row, so a site's own line can span all of them. */\nconst COLUMNS = 7\n\nexport interface OrgOverlaysCardProps {\n /** The org's plan, for the `marketingOverlays` entitlement. */\n org?: Partial<AglynOrgBilling>\n}\n\n/**\n * Every site's announcement bars and popups, on the organization's\n * Marketing hub.\n *\n * READ-ONLY but for the switch. An overlay is drawn on one site's pages and\n * written against that site's variables, screens and order, so its editor\n * stays on the site: each row's Edit and the New button open the site's own\n * Overlays section. What the org hub adds is the view across sites — which\n * of them are showing what, and their lifetime engagement — and the one\n * change that needs no editor, turning an overlay on or off.\n *\n * Grouped by site, each group in the order the site shows its overlays,\n * under a line naming the site and whether its default bar and popup are\n * on. No 14-day analytics row: that is a document read per day per site,\n * and it lives on the site.\n */\nexport function OrgOverlaysCard(props: OrgOverlaysCardProps) {\n const { org } = props\n const mount = useMarketingOrgMount()\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const entitled = checkEntitlement(org, 'marketingOverlays')\n\n const sites = mount?.hosts ?? []\n const [page, setPage] = useState(0)\n const pageSites = sites.slice(\n page * ORG_SITES_PER_PAGE,\n (page + 1) * ORG_SITES_PER_PAGE,\n )\n const { reads, patch } = useOrgSiteReads<SiteOverlays>(\n entitled ? pageSites.map((site) => site.id) : [],\n (hostId) => readSiteOverlays(firestore, hostId),\n [firestore, entitled],\n )\n\n /**\n * One field, merged, on the row's own site — the write the site's own\n * switch makes, and one the rules allow every editor of that site.\n */\n const handleToggle = async (hostId: string, overlay: OverlayRow) => {\n const enabled = overlay.enabled === false\n try {\n await setDoc(\n doc(firestore, 'hosts', hostId, 'overlays', overlay.$id),\n { enabled },\n { merge: true },\n )\n patch(hostId, (site) => ({\n ...site,\n rows: site.rows.map((row) =>\n row.$id === overlay.$id ? { ...row, enabled } : row,\n ),\n }))\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The overlay could not be switched', {\n variant: 'error',\n })\n }\n }\n\n return (\n <CardDisplay\n header=\"Announcement bars & popups\"\n subheader=\"Every site in this organization\"\n help={pluginDocsHelp('marketingOverlays', {\n anchor: '#across-your-sites',\n })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n >\n {!entitled ? (\n <Alert severity=\"info\">\n {'Announcement bars and popups are included from the Starter ' +\n 'plan — see Billing to upgrade.'}\n </Alert>\n ) : !mount || !mount.hostsReady ? null : !sites.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This organization has no sites yet. Overlays are drawn on a ' +\n 'site’s pages, so they are created on a site.'}\n </Typography>\n ) : (\n <Stack spacing={2}>\n <Stack\n direction=\"row\"\n spacing={2}\n sx={{ alignItems: 'center', justifyContent: 'space-between' }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Each site shows the first matching bar and popup in its own ' +\n 'order. Edit and create them on the site; switch them on or ' +\n 'off here.'}\n </Typography>\n <OrgSitePickerButton\n mount={mount}\n section=\"overlays\"\n label=\"New overlay\"\n />\n </Stack>\n <ScrollTable size=\"small\" aria-label=\"Overlays by site\">\n <TableHead>\n <TableRow>\n <TableCell>{'Overlay'}</TableCell>\n <TableCell>{'Kind'}</TableCell>\n <TableCell>{'Status'}</TableCell>\n <TableCell>{'Window'}</TableCell>\n <TableCell>{'Pages'}</TableCell>\n <TableCell>{'Engagement'}</TableCell>\n <TableCell align=\"right\">{'Actions'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {pageSites.map((site) => {\n const read = reads.get(site.id)\n const answer = read?.value ?? null\n const href = orgSiteSectionHref(mount, site.id, 'overlays')\n const name = site.name || site.subdomain || site.id\n return (\n <Fragment key={site.id}>\n {/* The site's own line: its name, its defaults, and what was not read. */}\n <TableRow>\n <TableCell colSpan={COLUMNS}>\n <Stack\n direction=\"row\"\n spacing={2}\n sx={{ alignItems: 'baseline', flexWrap: 'wrap' }}\n >\n <Typography variant=\"subtitle2\">\n {href ? <AppLink href={href}>{name}</AppLink> : name}\n </Typography>\n {answer?.defaults ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Default bar ${answer.defaults.bar ? 'on' : 'off'} · ` +\n `default popup ${answer.defaults.popup ? 'on' : 'off'}`}\n </Typography>\n ) : null}\n {answer?.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`More than ${ORG_OVERLAYS_PER_SITE} on this ` +\n 'site — these are the first by name; the ' +\n 'site’s own list has every one.'}\n </Typography>\n ) : null}\n </Stack>\n </TableCell>\n </TableRow>\n {read?.status === 'error' ? (\n <TableRow>\n <TableCell colSpan={COLUMNS}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This site’s overlays could not be read.'}\n </Typography>\n </TableCell>\n </TableRow>\n ) : answer && !answer.rows.length ? (\n <TableRow>\n <TableCell colSpan={COLUMNS}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No overlays on this site.'}\n </Typography>\n </TableCell>\n </TableRow>\n ) : (\n (answer?.rows ?? []).map((overlay) => {\n const status = STATUS_CHIP[overlayStatus(overlay, Date.now())]\n return (\n <TableRow key={overlay.$id}>\n <TableCell>{overlayDisplayName(overlay)}</TableCell>\n <TableCell>\n {overlay.kind === 'bar' ? 'Bar' : 'Popup'}\n </TableCell>\n <TableCell>\n <Chip\n size=\"small\"\n color={status.color}\n label={status.label}\n />\n </TableCell>\n <TableCell>{overlayWindowLabel(overlay)}</TableCell>\n <TableCell>{overlayPagesLabel(overlay)}</TableCell>\n <TableCell>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {overlayEngagementLabel(overlay.stats)}\n </Typography>\n </TableCell>\n <TableCell align=\"right\">\n <Switch\n size=\"small\"\n checked={overlay.enabled !== false}\n onChange={() => void handleToggle(site.id, overlay)}\n slotProps={{\n input: {\n 'aria-label': `Show ${overlayDisplayName(overlay)} on ${name}`,\n },\n }}\n />\n {/* A site the shell could not address has no Overlays section to open. */}\n {href ? (\n <AppLink\n componentVariant=\"button\"\n href={href}\n size=\"small\"\n color=\"primary\"\n aria-label={`Edit ${overlayDisplayName(overlay)} on ${name}`}\n >\n {'Edit'}\n </AppLink>\n ) : null}\n </TableCell>\n </TableRow>\n )\n })\n )}\n </Fragment>\n )\n })}\n </TableBody>\n </ScrollTable>\n <ListPagination\n page={page}\n pageSize={ORG_SITES_PER_PAGE}\n rowCount={pageSites.length}\n count={sites.length}\n onPageChange={setPage}\n labelDisplayedRows={({ from, to, count }) =>\n `Sites ${from}–${to} of ${count}`\n }\n />\n </Stack>\n )}\n </CardDisplay>\n )\n}\nOrgOverlaysCard.displayName = 'OrgOverlaysCard'\n\nexport default OrgOverlaysCard\n"],"names":["checkEntitlement","pluginDocsHelp","compareOverlayPrecedence","overlayStatus","AppLink","CardDisplay","ListPagination","ScrollTable","useSnackbar","Alert","Chip","Stack","Switch","TableBody","TableCell","TableHead","TableRow","Typography","collection","doc","getDoc","getDocs","limit","orderBy","query","setDoc","Fragment","useState","useFirestore","ceilingedWindow","useMarketingOrgMount","OrgSitePickerButton","orgSiteSectionHref","overlayDisplayName","overlayEngagementLabel","overlayPagesLabel","overlayWindowLabel","ORG_SITES_PER_PAGE","useOrgSiteReads","ORG_OVERLAYS_PER_SITE","readSiteOverlays","firestore","hostId","site","overlays","host","Promise","all","catch","rows","truncated","docs","map","entry","data","$id","id","exists","sort","defaults","bar","Boolean","announcementBar","enabled","text","popup","body","STATUS_CHIP","off","label","color","scheduled","live","COLUMNS","OrgOverlaysCard","props","org","mount","enqueueSnackbar","entitled","sites","hosts","page","setPage","pageSites","slice","reads","patch","handleToggle","overlay","merge","row","error","console","variant","header","subheader","help","anchor","contentGutterX","contentGutterY","contentBordered","severity","hostsReady","length","spacing","direction","sx","alignItems","justifyContent","section","size","aria-label","align","read","get","answer","value","href","name","subdomain","colSpan","flexWrap","status","Date","now","kind","stats","checked","onChange","slotProps","input","componentVariant","pageSize","rowCount","count","onPageChange","labelDisplayedRows","from","to","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAEEA,gBAAgB,EAChBC,cAAc,QACT,eAAc;AACrB,SAASC,wBAAwB,EAAEC,aAAa,QAA0B,oBAAU;AACpF,SAASC,OAAO,EAAEC,WAAW,QAAQ,uBAAsB;AAC3D,SAASC,cAAc,QAAQ,4DAA2D;AAC1F,SAASC,WAAW,QAAQ,yDAAwD;AACpF,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,KAAK,EACLC,IAAI,EACJC,KAAK,EACLC,MAAM,EACNC,SAAS,EACTC,SAAS,EACTC,SAAS,EACTC,QAAQ,EACRC,UAAU,QACL,gBAAe;AACtB,SACEC,UAAU,EACVC,GAAG,EACHC,MAAM,EACNC,OAAO,EACPC,KAAK,EACLC,OAAO,EACPC,KAAK,EACLC,MAAM,QAED,qBAAoB;AAC3B,SAASC,QAAQ,EAAEC,QAAQ,QAAQ,QAAO;AAC1C,SAASC,YAAY,QAAQ,iCAAgC;AAC7D,SAASC,eAAe,QAAQ,+DAA8D;AAC9F,SAASC,oBAAoB,QAAQ,2BAAuB;AAC5D,SACEC,mBAAmB,EACnBC,kBAAkB,QACb,8BAA0B;AACjC,SACEC,kBAAkB,EAClBC,sBAAsB,EACtBC,iBAAiB,EACjBC,kBAAkB,QACb,sBAAkB;AACzB,SAASC,kBAAkB,EAAEC,eAAe,QAAQ,0BAAsB;AAE1E;;;;;;;CAOC,GACD,OAAO,MAAMC,wBAAwB,GAAE;AAkBvC;;;;;CAKC,GACD,eAAeC,iBACbC,SAAoB,EACpBC,MAAc;QA2BOC,uBAAiCA,wBAC/BA,aAAuBA;IA1B9C,MAAM,CAACC,UAAUC,KAAK,GAAG,MAAMC,QAAQC,GAAG,CAAC;QACzC1B,QACEG,MACEN,WAAWuB,WAAW,SAASC,QAAQ,aACvCnB,QAAQ,SACRD,MAAMiB,wBAAwB;QAGlCnB,OAAOD,IAAIsB,WAAW,SAASC,SAASM,KAAK,CAAC,IAAM;KACrD;IACD,MAAM,EAAEC,IAAI,EAAEC,SAAS,EAAE,GAAGrB,gBAC1Be,SAASO,IAAI,CAACC,GAAG,CACf,CAACC,QAAW,aAAMA,MAAMC,IAAI;YAAoBC,KAAKF,MAAMG,EAAE;aAE/DjB;IAEF,MAAMI,OAAOE,CAAAA,wBAAAA,KAAMY,MAAM,MAAMZ,KAAKS,IAAI,KAA6B;IACrE,OAAO;QACL,sEAAsE;QACtE,2DAA2D;QAC3DL,MAAM;eAAIA;SAAK,CAACS,IAAI,CAACxD;QACrBgD;QACA,sEAAsE;QACtES,UAAUhB,OACN;YACEiB,KAAKC,QAAQlB,EAAAA,wBAAAA,KAAKmB,eAAe,qBAApBnB,sBAAsBoB,OAAO,OAAIpB,yBAAAA,KAAKmB,eAAe,qBAApBnB,uBAAsBqB,IAAI;YACxEC,OAAOJ,QAAQlB,EAAAA,cAAAA,KAAKsB,KAAK,qBAAVtB,YAAYoB,OAAO,OAAIpB,eAAAA,KAAKsB,KAAK,qBAAVtB,aAAYuB,IAAI;QACxD,IACA;IACN;AACF;AAEA,MAAMC,cAGF;IACFC,KAAK;QAAEC,OAAO;QAAOC,OAAO;IAAU;IACtCC,WAAW;QAAEF,OAAO;QAAaC,OAAO;IAAO;IAC/CE,MAAM;QAAEH,OAAO;QAAQC,OAAO;IAAU;AAC1C;AAEA,iEAAiE,GACjE,MAAMG,UAAU;AAOhB;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,gBAAgBC,KAA2B;;IACzD,MAAM,EAAEC,GAAG,EAAE,GAAGD;IAChB,MAAME,QAAQ/C;IACd,MAAMW,YAAYb;IAClB,MAAM,EAAEkD,eAAe,EAAE,GAAGtE;IAC5B,MAAMuE,WAAW/E,iBAAiB4E,KAAK;IAEvC,MAAMI,gBAAQH,yBAAAA,MAAOI,KAAK,mBAAI,EAAE;IAChC,MAAM,CAACC,MAAMC,QAAQ,GAAGxD,SAAS;IACjC,MAAMyD,YAAYJ,MAAMK,KAAK,CAC3BH,OAAO7C,oBACP,AAAC6C,CAAAA,OAAO,CAAA,IAAK7C;IAEf,MAAM,EAAEiD,KAAK,EAAEC,KAAK,EAAE,GAAGjD,gBACvByC,WAAWK,UAAUhC,GAAG,CAAC,CAACT,OAASA,KAAKa,EAAE,IAAI,EAAE,EAChD,CAACd,SAAWF,iBAAiBC,WAAWC,SACxC;QAACD;QAAWsC;KAAS;IAGvB;;;GAGC,GACD,MAAMS,eAAe,OAAO9C,QAAgB+C;QAC1C,MAAM1B,UAAU0B,QAAQ1B,OAAO,KAAK;QACpC,IAAI;YACF,MAAMtC,OACJN,IAAIsB,WAAW,SAASC,QAAQ,YAAY+C,QAAQlC,GAAG,GACvD;gBAAEQ;YAAQ,GACV;gBAAE2B,OAAO;YAAK;YAEhBH,MAAM7C,QAAQ,CAACC,OAAU,aACpBA;oBACHM,MAAMN,KAAKM,IAAI,CAACG,GAAG,CAAC,CAACuC,MACnBA,IAAIpC,GAAG,KAAKkC,QAAQlC,GAAG,GAAG,aAAKoC;4BAAK5B;6BAAY4B;;QAGtD,EAAE,OAAOC,OAAO;YACdC,QAAQD,KAAK,CAACA;YACdd,gBAAgB,qCAAqC;gBACnDgB,SAAS;YACX;QACF;IACF;IAEA,qBACE,KAACzF;QACC0F,QAAO;QACPC,WAAU;QACVC,MAAMhG,eAAe,qBAAqB;YACxCiG,QAAQ;QACV;QACAC,cAAc;QACdC,cAAc;QACdC,iBAAgB;kBAEf,CAACtB,yBACA,KAACtE;YAAM6F,UAAS;sBACb,gEACC;aAEF,CAACzB,SAAS,CAACA,MAAM0B,UAAU,GAAG,OAAO,CAACvB,MAAMwB,MAAM,iBACpD,KAACvF;YAAW6E,SAAQ;YAAQxB,OAAM;sBAC/B,iEACC;2BAGJ,MAAC3D;YAAM8F,SAAS;;8BACd,MAAC9F;oBACC+F,WAAU;oBACVD,SAAS;oBACTE,IAAI;wBAAEC,YAAY;wBAAUC,gBAAgB;oBAAgB;;sCAE5D,KAAC5F;4BAAW6E,SAAQ;4BAAQxB,OAAM;sCAC/B,iEACC,gEACA;;sCAEJ,KAACvC;4BACC8C,OAAOA;4BACPiC,SAAQ;4BACRzC,OAAM;;;;8BAGV,MAAC9D;oBAAYwG,MAAK;oBAAQC,cAAW;;sCACnC,KAACjG;sCACC,cAAA,MAACC;;kDACC,KAACF;kDAAW;;kDACZ,KAACA;kDAAW;;kDACZ,KAACA;kDAAW;;kDACZ,KAACA;kDAAW;;kDACZ,KAACA;kDAAW;;kDACZ,KAACA;kDAAW;;kDACZ,KAACA;wCAAUmG,OAAM;kDAAS;;;;;sCAG9B,KAACpG;sCACEuE,UAAUhC,GAAG,CAAC,CAACT;;gCACd,MAAMuE,OAAO5B,MAAM6B,GAAG,CAACxE,KAAKa,EAAE;gCAC9B,MAAM4D,iBAASF,wBAAAA,KAAMG,KAAK,mBAAI;gCAC9B,MAAMC,OAAOtF,mBAAmB6C,OAAOlC,KAAKa,EAAE,EAAE;gCAChD,MAAM+D,OAAO5E,KAAK4E,IAAI,IAAI5E,KAAK6E,SAAS,IAAI7E,KAAKa,EAAE;gCACnD,qBACE,MAAC9B;;sDAEC,KAACV;sDACC,cAAA,KAACF;gDAAU2G,SAAShD;0DAClB,cAAA,MAAC9D;oDACC+F,WAAU;oDACVD,SAAS;oDACTE,IAAI;wDAAEC,YAAY;wDAAYc,UAAU;oDAAO;;sEAE/C,KAACzG;4DAAW6E,SAAQ;sEACjBwB,qBAAO,KAAClH;gEAAQkH,MAAMA;0EAAOC;iEAAkBA;;wDAEjDH,CAAAA,0BAAAA,OAAQzD,QAAQ,kBACf,KAAC1C;4DAAW6E,SAAQ;4DAAUxB,OAAM;sEACjC,CAAC,YAAY,EAAE8C,OAAOzD,QAAQ,CAACC,GAAG,GAAG,OAAO,MAAM,GAAG,CAAC,GACrD,CAAC,cAAc,EAAEwD,OAAOzD,QAAQ,CAACM,KAAK,GAAG,OAAO,OAAO;6DAEzD;wDACHmD,CAAAA,0BAAAA,OAAQlE,SAAS,kBAChB,KAACjC;4DAAW6E,SAAQ;4DAAUxB,OAAM;sEACjC,CAAC,UAAU,EAAE/B,sBAAsB,SAAS,CAAC,GAC5C,6CACA;6DAEF;;;;;wCAIT2E,CAAAA,wBAAAA,KAAMS,MAAM,MAAK,wBAChB,KAAC3G;sDACC,cAAA,KAACF;gDAAU2G,SAAShD;0DAClB,cAAA,KAACxD;oDAAW6E,SAAQ;oDAAQxB,OAAM;8DAC/B;;;6CAIL8C,UAAU,CAACA,OAAOnE,IAAI,CAACuD,MAAM,iBAC/B,KAACxF;sDACC,cAAA,KAACF;gDAAU2G,SAAShD;0DAClB,cAAA,KAACxD;oDAAW6E,SAAQ;oDAAQxB,OAAM;8DAC/B;;;6CAKP,UAAC8C,0BAAAA,OAAQnE,IAAI,oBAAI,EAAE,EAAEG,GAAG,CAAC,CAACqC;4CACxB,MAAMkC,SAASxD,WAAW,CAAChE,cAAcsF,SAASmC,KAAKC,GAAG,IAAI;4CAC9D,qBACE,MAAC7G;;kEACC,KAACF;kEAAWmB,mBAAmBwD;;kEAC/B,KAAC3E;kEACE2E,QAAQqC,IAAI,KAAK,QAAQ,QAAQ;;kEAEpC,KAAChH;kEACC,cAAA,KAACJ;4DACCqG,MAAK;4DACLzC,OAAOqD,OAAOrD,KAAK;4DACnBD,OAAOsD,OAAOtD,KAAK;;;kEAGvB,KAACvD;kEAAWsB,mBAAmBqD;;kEAC/B,KAAC3E;kEAAWqB,kBAAkBsD;;kEAC9B,KAAC3E;kEACC,cAAA,KAACG;4DAAW6E,SAAQ;4DAAUxB,OAAM;sEACjCpC,uBAAuBuD,QAAQsC,KAAK;;;kEAGzC,MAACjH;wDAAUmG,OAAM;;0EACf,KAACrG;gEACCmG,MAAK;gEACLiB,SAASvC,QAAQ1B,OAAO,KAAK;gEAC7BkE,UAAU,IAAM,KAAKzC,aAAa7C,KAAKa,EAAE,EAAEiC;gEAC3CyC,WAAW;oEACTC,OAAO;wEACL,cAAc,CAAC,KAAK,EAAElG,mBAAmBwD,SAAS,IAAI,EAAE8B,MAAM;oEAChE;gEACF;;4DAGDD,qBACC,KAAClH;gEACCgI,kBAAiB;gEACjBd,MAAMA;gEACNP,MAAK;gEACLzC,OAAM;gEACN0C,cAAY,CAAC,KAAK,EAAE/E,mBAAmBwD,SAAS,IAAI,EAAE8B,MAAM;0EAE3D;iEAED;;;;+CAzCO9B,QAAQlC,GAAG;wCA6C9B;;mCA7FWZ,KAAKa,EAAE;4BAiG1B;;;;8BAGJ,KAAClD;oBACC4E,MAAMA;oBACNmD,UAAUhG;oBACViG,UAAUlD,UAAUoB,MAAM;oBAC1B+B,OAAOvD,MAAMwB,MAAM;oBACnBgC,cAAcrD;oBACdsD,oBAAoB,CAAC,EAAEC,IAAI,EAAEC,EAAE,EAAEJ,KAAK,EAAE,GACtC,CAAC,MAAM,EAAEG,KAAK,CAAC,EAAEC,GAAG,IAAI,EAAEJ,OAAO;;;;;AAO/C;AACA7D,gBAAgBkE,WAAW,GAAG;AAE9B,eAAelE,gBAAe"}
@@ -0,0 +1,26 @@
1
+ import { type MarketingOrgMount } from './marketing-org-mount';
2
+ /** A site's own Marketing hub at `section`, or `null` when it has no address. */
3
+ export declare function orgSiteSectionHref(mount: MarketingOrgMount, hostId: string, section: string): string | null;
4
+ export interface OrgSitePickerButtonProps {
5
+ mount: MarketingOrgMount;
6
+ /** The section of the site's Marketing hub to open — `overlays`, `experiments`. */
7
+ section: string;
8
+ label: string;
9
+ }
10
+ /**
11
+ * The org hub's way into anything that is EDITED on a site: a button that
12
+ * opens one of the organization's sites at a section of its own Marketing
13
+ * hub.
14
+ *
15
+ * An overlay is drawn on one site's pages and an A/B test splits one site's
16
+ * traffic, so their editors belong to the site — the org hub lists them and
17
+ * sends the reader to the site to write. With one site there is nothing to
18
+ * choose and the button is the link; with several it opens a menu of them.
19
+ * A site the shell could not give a subdomain is listed and disabled: it has
20
+ * no hub address to open.
21
+ */
22
+ export declare function OrgSitePickerButton(props: OrgSitePickerButtonProps): import("react").JSX.Element;
23
+ export declare namespace OrgSitePickerButton {
24
+ var displayName: string;
25
+ }
26
+ export default OrgSitePickerButton;
@@ -0,0 +1,104 @@
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 { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
+ import { AppLink } from "@aglyn/shared-ui-jsx";
20
+ import { Button, Menu, MenuItem } from "@mui/material";
21
+ import { forwardRef, useState } from "react";
22
+ import { orgSiteHubPath } from "./marketing-org-mount.js";
23
+ /** A site's own Marketing hub at `section`, or `null` when it has no address. */ export function orgSiteSectionHref(mount, hostId, section) {
24
+ const hub = orgSiteHubPath(mount, hostId, 'marketing');
25
+ return hub ? `${hub}/${section}` : null;
26
+ }
27
+ /** The anchor a site in the menu renders as — a real link, like every other. */ const MenuItemLink = /*#__PURE__*/ forwardRef((props, ref)=>/*#__PURE__*/ _jsx(AppLink, _extends({
28
+ ref: ref
29
+ }, props, {
30
+ componentVariant: 'naked'
31
+ })));
32
+ MenuItemLink.displayName = 'OrgSitePickerMenuItemLink';
33
+ /**
34
+ * The org hub's way into anything that is EDITED on a site: a button that
35
+ * opens one of the organization's sites at a section of its own Marketing
36
+ * hub.
37
+ *
38
+ * An overlay is drawn on one site's pages and an A/B test splits one site's
39
+ * traffic, so their editors belong to the site — the org hub lists them and
40
+ * sends the reader to the site to write. With one site there is nothing to
41
+ * choose and the button is the link; with several it opens a menu of them.
42
+ * A site the shell could not give a subdomain is listed and disabled: it has
43
+ * no hub address to open.
44
+ */ export function OrgSitePickerButton(props) {
45
+ const { mount, section, label } = props;
46
+ const [anchorEl, setAnchorEl] = useState(null);
47
+ const sites = mount.hosts;
48
+ if (sites.length === 1) {
49
+ const href = orgSiteSectionHref(mount, sites[0].id, section);
50
+ return href ? /*#__PURE__*/ _jsx(AppLink, {
51
+ componentVariant: "button",
52
+ href: href,
53
+ size: "small",
54
+ color: "primary",
55
+ variant: "contained",
56
+ children: label
57
+ }) : /*#__PURE__*/ _jsx(Button, {
58
+ size: "small",
59
+ color: "primary",
60
+ variant: "contained",
61
+ disabled: true,
62
+ children: label
63
+ });
64
+ }
65
+ return /*#__PURE__*/ _jsxs(_Fragment, {
66
+ children: [
67
+ /*#__PURE__*/ _jsx(Button, {
68
+ size: "small",
69
+ color: "primary",
70
+ variant: "contained",
71
+ disabled: !sites.length,
72
+ "aria-haspopup": "true",
73
+ onClick: (event)=>setAnchorEl(event.currentTarget),
74
+ children: label
75
+ }),
76
+ /*#__PURE__*/ _jsx(Menu, {
77
+ anchorEl: anchorEl,
78
+ open: Boolean(anchorEl),
79
+ onClose: ()=>setAnchorEl(null),
80
+ slotProps: {
81
+ list: {
82
+ 'aria-label': `${label} on which site`
83
+ }
84
+ },
85
+ children: sites.map((site)=>{
86
+ const href = orgSiteSectionHref(mount, site.id, section);
87
+ return /*#__PURE__*/ _jsx(MenuItem, _extends({}, href ? {
88
+ component: MenuItemLink,
89
+ href
90
+ } : {
91
+ disabled: true
92
+ }, {
93
+ onClick: ()=>setAnchorEl(null),
94
+ children: site.name || site.subdomain || site.id
95
+ }), site.id);
96
+ })
97
+ })
98
+ ]
99
+ });
100
+ }
101
+ OrgSitePickerButton.displayName = 'OrgSitePickerButton';
102
+ export default OrgSitePickerButton;
103
+
104
+ //# sourceMappingURL=org-site-picker-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/components/org-site-picker-button.tsx"],"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 { AppLink, type AppLinkNakedLinkProps } from '@aglyn/shared-ui-jsx'\nimport { Button, Menu, MenuItem } from '@mui/material'\nimport { forwardRef, useState } from 'react'\nimport { orgSiteHubPath, type MarketingOrgMount } from './marketing-org-mount'\n\n/** A site's own Marketing hub at `section`, or `null` when it has no address. */\nexport function orgSiteSectionHref(\n mount: MarketingOrgMount,\n hostId: string,\n section: string,\n): string | null {\n const hub = orgSiteHubPath(mount, hostId, 'marketing')\n return hub ? `${hub}/${section}` : null\n}\n\n/** The anchor a site in the menu renders as — a real link, like every other. */\nconst MenuItemLink = forwardRef<any, AppLinkNakedLinkProps>((props, ref) => (\n <AppLink ref={ref} {...props} componentVariant={'naked'} />\n))\nMenuItemLink.displayName = 'OrgSitePickerMenuItemLink'\n\nexport interface OrgSitePickerButtonProps {\n mount: MarketingOrgMount\n /** The section of the site's Marketing hub to open — `overlays`, `experiments`. */\n section: string\n label: string\n}\n\n/**\n * The org hub's way into anything that is EDITED on a site: a button that\n * opens one of the organization's sites at a section of its own Marketing\n * hub.\n *\n * An overlay is drawn on one site's pages and an A/B test splits one site's\n * traffic, so their editors belong to the site — the org hub lists them and\n * sends the reader to the site to write. With one site there is nothing to\n * choose and the button is the link; with several it opens a menu of them.\n * A site the shell could not give a subdomain is listed and disabled: it has\n * no hub address to open.\n */\nexport function OrgSitePickerButton(props: OrgSitePickerButtonProps) {\n const { mount, section, label } = props\n const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)\n const sites = mount.hosts\n\n if (sites.length === 1) {\n const href = orgSiteSectionHref(mount, sites[0].id, section)\n return href ? (\n <AppLink\n componentVariant=\"button\"\n href={href}\n size=\"small\"\n color=\"primary\"\n variant=\"contained\"\n >\n {label}\n </AppLink>\n ) : (\n <Button size=\"small\" color=\"primary\" variant=\"contained\" disabled>\n {label}\n </Button>\n )\n }\n\n return (\n <>\n <Button\n size=\"small\"\n color=\"primary\"\n variant=\"contained\"\n disabled={!sites.length}\n aria-haspopup=\"true\"\n onClick={(event) => setAnchorEl(event.currentTarget)}\n >\n {label}\n </Button>\n <Menu\n anchorEl={anchorEl}\n open={Boolean(anchorEl)}\n onClose={() => setAnchorEl(null)}\n slotProps={{ list: { 'aria-label': `${label} on which site` } }}\n >\n {sites.map((site) => {\n const href = orgSiteSectionHref(mount, site.id, section)\n return (\n <MenuItem\n key={site.id}\n {...((href\n ? { component: MenuItemLink, href }\n : { disabled: true }) as any)}\n onClick={() => setAnchorEl(null)}\n >\n {site.name || site.subdomain || site.id}\n </MenuItem>\n )\n })}\n </Menu>\n </>\n )\n}\nOrgSitePickerButton.displayName = 'OrgSitePickerButton'\n\nexport default OrgSitePickerButton\n"],"names":["AppLink","Button","Menu","MenuItem","forwardRef","useState","orgSiteHubPath","orgSiteSectionHref","mount","hostId","section","hub","MenuItemLink","props","ref","componentVariant","displayName","OrgSitePickerButton","label","anchorEl","setAnchorEl","sites","hosts","length","href","id","size","color","variant","disabled","aria-haspopup","onClick","event","currentTarget","open","Boolean","onClose","slotProps","list","map","site","component","name","subdomain"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,OAAO,QAAoC,uBAAsB;AAC1E,SAASC,MAAM,EAAEC,IAAI,EAAEC,QAAQ,QAAQ,gBAAe;AACtD,SAASC,UAAU,EAAEC,QAAQ,QAAQ,QAAO;AAC5C,SAASC,cAAc,QAAgC,2BAAuB;AAE9E,+EAA+E,GAC/E,OAAO,SAASC,mBACdC,KAAwB,EACxBC,MAAc,EACdC,OAAe;IAEf,MAAMC,MAAML,eAAeE,OAAOC,QAAQ;IAC1C,OAAOE,MAAM,GAAGA,IAAI,CAAC,EAAED,SAAS,GAAG;AACrC;AAEA,8EAA8E,GAC9E,MAAME,6BAAeR,WAAuC,CAACS,OAAOC,oBAClE,KAACd;QAAQc,KAAKA;OAASD;QAAOE,kBAAkB;;AAElDH,aAAaI,WAAW,GAAG;AAS3B;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,oBAAoBJ,KAA+B;IACjE,MAAM,EAAEL,KAAK,EAAEE,OAAO,EAAEQ,KAAK,EAAE,GAAGL;IAClC,MAAM,CAACM,UAAUC,YAAY,GAAGf,SAA6B;IAC7D,MAAMgB,QAAQb,MAAMc,KAAK;IAEzB,IAAID,MAAME,MAAM,KAAK,GAAG;QACtB,MAAMC,OAAOjB,mBAAmBC,OAAOa,KAAK,CAAC,EAAE,CAACI,EAAE,EAAEf;QACpD,OAAOc,qBACL,KAACxB;YACCe,kBAAiB;YACjBS,MAAMA;YACNE,MAAK;YACLC,OAAM;YACNC,SAAQ;sBAEPV;2BAGH,KAACjB;YAAOyB,MAAK;YAAQC,OAAM;YAAUC,SAAQ;YAAYC,QAAQ;sBAC9DX;;IAGP;IAEA,qBACE;;0BACE,KAACjB;gBACCyB,MAAK;gBACLC,OAAM;gBACNC,SAAQ;gBACRC,UAAU,CAACR,MAAME,MAAM;gBACvBO,iBAAc;gBACdC,SAAS,CAACC,QAAUZ,YAAYY,MAAMC,aAAa;0BAElDf;;0BAEH,KAAChB;gBACCiB,UAAUA;gBACVe,MAAMC,QAAQhB;gBACdiB,SAAS,IAAMhB,YAAY;gBAC3BiB,WAAW;oBAAEC,MAAM;wBAAE,cAAc,GAAGpB,MAAM,cAAc,CAAC;oBAAC;gBAAE;0BAE7DG,MAAMkB,GAAG,CAAC,CAACC;oBACV,MAAMhB,OAAOjB,mBAAmBC,OAAOgC,KAAKf,EAAE,EAAEf;oBAChD,qBACE,KAACP,uBAEOqB,OACF;wBAAEiB,WAAW7B;wBAAcY;oBAAK,IAChC;wBAAEK,UAAU;oBAAK;wBACrBE,SAAS,IAAMX,YAAY;kCAE1BoB,KAAKE,IAAI,IAAIF,KAAKG,SAAS,IAAIH,KAAKf,EAAE;wBANlCe,KAAKf,EAAE;gBASlB;;;;AAIR;AACAR,oBAAoBD,WAAW,GAAG;AAElC,eAAeC,oBAAmB"}
@@ -0,0 +1,38 @@
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 '../model';
18
+ /**
19
+ * How an overlay row reads, in the site's own list and in the
20
+ * organization's list of every site's overlays — one spelling for both, so
21
+ * the same overlay does not read two ways one click apart.
22
+ */
23
+ /**
24
+ * The overlay's name, or the opening of its copy when nobody named it — the
25
+ * editor stores an empty name as `null` — or its id when it has neither.
26
+ */
27
+ export declare function overlayDisplayName(overlay: HostOverlay & {
28
+ $id?: string;
29
+ }): string;
30
+ /** The showing window, open ends as `…`, or `Always` with neither end set. */
31
+ export declare function overlayWindowLabel(overlay: Pick<HostOverlay, 'startAtMs' | 'endAtMs'>): string;
32
+ /** The page patterns it shows on, or `All pages` with none. */
33
+ export declare function overlayPagesLabel(overlay: Pick<HostOverlay, 'pathPatterns'>): string;
34
+ /**
35
+ * Lifetime engagement (AGL-271) — the counters the beacon increments — or a
36
+ * dash for an overlay nobody has seen yet.
37
+ */
38
+ export declare function overlayEngagementLabel(stats: HostOverlay['stats'] | undefined): string;
@@ -0,0 +1,55 @@
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
+ * How an overlay row reads, in the site's own list and in the
18
+ * organization's list of every site's overlays — one spelling for both, so
19
+ * the same overlay does not read two ways one click apart.
20
+ */ /**
21
+ * The overlay's name, or the opening of its copy when nobody named it — the
22
+ * editor stores an empty name as `null` — or its id when it has neither.
23
+ */ export function overlayDisplayName(overlay) {
24
+ var _overlay_bar, _overlay_popup;
25
+ if (overlay.name) return overlay.name;
26
+ const copy = overlay.kind === 'bar' ? (_overlay_bar = overlay.bar) == null ? void 0 : _overlay_bar.text : (_overlay_popup = overlay.popup) == null ? void 0 : _overlay_popup.headline;
27
+ return (copy != null ? copy : '').slice(0, 32) || overlay.$id || '';
28
+ }
29
+ /** The showing window, open ends as `…`, or `Always` with neither end set. */ export function overlayWindowLabel(overlay) {
30
+ if (!overlay.startAtMs && !overlay.endAtMs) return 'Always';
31
+ const day = (ms)=>ms ? new Date(ms).toLocaleDateString() : '…';
32
+ return `${day(overlay.startAtMs)} → ${day(overlay.endAtMs)}`;
33
+ }
34
+ /** The page patterns it shows on, or `All pages` with none. */ export function overlayPagesLabel(overlay) {
35
+ var _overlay_pathPatterns;
36
+ return ((_overlay_pathPatterns = overlay.pathPatterns) == null ? void 0 : _overlay_pathPatterns.length) ? overlay.pathPatterns.join(', ') : 'All pages';
37
+ }
38
+ /**
39
+ * Lifetime engagement (AGL-271) — the counters the beacon increments — or a
40
+ * dash for an overlay nobody has seen yet.
41
+ */ export function overlayEngagementLabel(stats) {
42
+ var _ref, _ref1, _ref2;
43
+ const impressions = (_ref = stats == null ? void 0 : stats.impressions) != null ? _ref : 0;
44
+ const clicks = (_ref1 = stats == null ? void 0 : stats.clicks) != null ? _ref1 : 0;
45
+ const dismissals = (_ref2 = stats == null ? void 0 : stats.dismissals) != null ? _ref2 : 0;
46
+ if (!impressions && !clicks && !dismissals) return '—';
47
+ const parts = [
48
+ `${impressions.toLocaleString()} views`
49
+ ];
50
+ if (clicks) parts.push(`${clicks.toLocaleString()} clicks`);
51
+ if (dismissals) parts.push(`${dismissals.toLocaleString()} dismissed`);
52
+ return parts.join(' · ');
53
+ }
54
+
55
+ //# sourceMappingURL=overlay-labels.js.map