@tokenoftrust/storefront-runner 2.0.4 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/storefront/migrations-apps/0011_dt_tracking_tenant_databases.sql +23 -0
- package/apps/storefront/node_modules/.bin/vitest +2 -2
- package/apps/storefront/node_modules/.vite/deps/_metadata.json +19 -19
- package/apps/storefront/node_modules/.vite/deps_ssr/_metadata.json +3 -3
- package/apps/storefront/src/components/chrome/MegaMenu.astro +11 -1
- package/apps/storefront/src/components/chrome/NavDropdown.astro +49 -7
- package/apps/storefront/src/components/chrome/SiteFooter.astro +29 -4
- package/apps/storefront/src/components/chrome/SiteHeader.astro +39 -6
- package/apps/storefront/src/components/chrome/UtilityNav.astro +13 -1
- package/apps/storefront/src/components/islands/MobileNav.tsx +42 -2
- package/apps/storefront/src/layouts/Layout.astro +64 -6
- package/apps/storefront/src/lib/chrome/ctaIdentity.ts +21 -0
- package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome.example.json +22 -22
- package/apps/storefront/src/lib/chrome/trackingPlacements.ts +36 -0
- package/apps/storefront/src/lib/d1/schema-apps.ts +57 -0
- package/apps/storefront/src/lib/rawChrome.ts +73 -14
- package/apps/storefront/src/lib/rawMarketingHtml.ts +30 -0
- package/apps/storefront/src/lib/shared/developerInvites.ts +6 -0
- package/apps/storefront/src/lib/storyblok/provider.ts +21 -5
- package/apps/storefront/src/lib/tracking/collectorAllowlist.ts +40 -0
- package/apps/storefront/src/lib/tracking/collectorIngest.ts +188 -0
- package/apps/storefront/src/lib/tracking/d1Control.ts +180 -0
- package/apps/storefront/src/lib/tracking/interactionCollector.ts +202 -0
- package/apps/storefront/src/lib/tracking/interactionHealth.ts +252 -0
- package/apps/storefront/src/lib/tracking/interactionSchema.ts +107 -0
- package/apps/storefront/src/lib/tracking/interactionStore.ts +0 -0
- package/apps/storefront/src/lib/tracking/pruneRollup.ts +260 -0
- package/apps/storefront/src/lib/tracking/publishSafetyNet.ts +162 -0
- package/apps/storefront/src/lib/tracking/tenantD1Registry.ts +214 -0
- package/apps/storefront/src/lib/tracking/tenantReadAccess.ts +138 -0
- package/apps/storefront/src/lib/tracking-writeback/rewriteTrackingIds.ts +175 -0
- package/apps/storefront/src/lib/tracking-writeback/writebackCandidate.ts +110 -0
- package/apps/storefront/src/pages/api/admin/interaction-insights.ts +101 -0
- package/apps/storefront/src/pages/api/tracking/interaction.ts +123 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +33 -14
- package/apps/storefront/src/workers/pruneRollupWorker.ts +120 -0
- package/package.json +1 -1
- package/packages/public-runtime/node_modules/.bin/vitest +2 -2
- package/packages/public-runtime/src/chrome.ts +18 -7
- package/packages/public-runtime/src/index.ts +10 -0
- package/packages/public-runtime/src/integration/index.ts +1 -0
- package/packages/public-runtime/src/integration/interaction-tracking-gate.ts +142 -0
- package/packages/public-runtime/src/interaction-tracking.ts +364 -0
- package/packages/public-runtime/src/tracking-id-derivation.ts +1008 -0
- package/scripts/tenant/validate.mjs +306 -1
- package/scripts/tot-dev.mjs +59 -3
- package/tenants/home/public/js/storefront.js +6 -87
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- Directory of per-tenant raw-interaction D1 databases (dt-storage-per-tenant-d1).
|
|
2
|
+
-- Mapping ONLY — no interaction data lands here; the rows live in each tenant's
|
|
3
|
+
-- own database (see src/lib/tracking/interactionSchema.ts). Additive.
|
|
4
|
+
CREATE TABLE `tracking_tenant_databases` (
|
|
5
|
+
`tenant_id` text PRIMARY KEY NOT NULL,
|
|
6
|
+
`database_name` text NOT NULL,
|
|
7
|
+
`database_uuid` text NOT NULL,
|
|
8
|
+
`schema_version` integer NOT NULL,
|
|
9
|
+
`created_at` text NOT NULL,
|
|
10
|
+
`updated_at` text NOT NULL,
|
|
11
|
+
`last_pruned_at` text,
|
|
12
|
+
`pruned_rows` integer DEFAULT 0 NOT NULL,
|
|
13
|
+
`size_bytes` integer,
|
|
14
|
+
`size_checked_at` text
|
|
15
|
+
);
|
|
16
|
+
--> statement-breakpoint
|
|
17
|
+
-- Two tenants sharing one database would defeat structural isolation.
|
|
18
|
+
CREATE UNIQUE INDEX `uq_tracking_tenant_databases_uuid`
|
|
19
|
+
ON `tracking_tenant_databases` (`database_uuid`);
|
|
20
|
+
--> statement-breakpoint
|
|
21
|
+
-- The scheduled prune's work queue: oldest-pruned first.
|
|
22
|
+
CREATE INDEX `idx_tracking_tenant_databases_pruned`
|
|
23
|
+
ON `tracking_tenant_databases` (`last_pruned_at`);
|
|
@@ -25,9 +25,9 @@ case `uname -a` in
|
|
|
25
25
|
esac
|
|
26
26
|
|
|
27
27
|
if [ -z "$NODE_PATH" ]; then
|
|
28
|
-
export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/vitest@2.1.8_@types+node@26.4.1_happy-dom@20.13.
|
|
28
|
+
export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/vitest@2.1.8_@types+node@26.4.1_happy-dom@20.13.2_lightningcss@1.33.0/node_modules/vitest/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/vitest@2.1.8_@types+node@26.4.1_happy-dom@20.13.2_lightningcss@1.33.0/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules"
|
|
29
29
|
else
|
|
30
|
-
export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/vitest@2.1.8_@types+node@26.4.1_happy-dom@20.13.
|
|
30
|
+
export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/vitest@2.1.8_@types+node@26.4.1_happy-dom@20.13.2_lightningcss@1.33.0/node_modules/vitest/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/vitest@2.1.8_@types+node@26.4.1_happy-dom@20.13.2_lightningcss@1.33.0/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
31
31
|
fi
|
|
32
32
|
if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
|
|
33
33
|
exec "$basedir/node.exe" "$basedir_win/../vitest/vitest.mjs" "$@"
|
|
@@ -1,103 +1,103 @@
|
|
|
1
1
|
{
|
|
2
|
-
"hash": "
|
|
2
|
+
"hash": "b0be5d68",
|
|
3
3
|
"configHash": "6038e6d2",
|
|
4
|
-
"lockfileHash": "
|
|
5
|
-
"browserHash": "
|
|
4
|
+
"lockfileHash": "db60c5b3",
|
|
5
|
+
"browserHash": "ec565bb8",
|
|
6
6
|
"optimized": {
|
|
7
7
|
"@astrojs/preact/client.js": {
|
|
8
8
|
"src": "../../../../../node_modules/.pnpm/@astrojs+preact@6.0.5_@babel+core@7.29.7_@types+node@26.4.1_esbuild@0.28.2_jiti@2.7.0_p_9974d7dcb4fcbb2fdffe5819f7140fba/node_modules/@astrojs/preact/dist/client.js",
|
|
9
9
|
"file": "@astrojs_preact_client__js.js",
|
|
10
|
-
"fileHash": "
|
|
10
|
+
"fileHash": "455f86c5",
|
|
11
11
|
"needsInterop": false
|
|
12
12
|
},
|
|
13
13
|
"@astrojs/preact > @preact/signals": {
|
|
14
14
|
"src": "../../../../../node_modules/.pnpm/@preact+signals@2.11.1_preact@10.29.8/node_modules/@preact/signals/dist/signals.module.js",
|
|
15
15
|
"file": "@astrojs_preact_n_@preact_signals.js",
|
|
16
|
-
"fileHash": "
|
|
16
|
+
"fileHash": "8c741faa",
|
|
17
17
|
"needsInterop": false
|
|
18
18
|
},
|
|
19
19
|
"@astrojs/preact/server.js": {
|
|
20
20
|
"src": "../../../../../node_modules/.pnpm/@astrojs+preact@6.0.5_@babel+core@7.29.7_@types+node@26.4.1_esbuild@0.28.2_jiti@2.7.0_p_9974d7dcb4fcbb2fdffe5819f7140fba/node_modules/@astrojs/preact/dist/server.js",
|
|
21
21
|
"file": "@astrojs_preact_server__js.js",
|
|
22
|
-
"fileHash": "
|
|
22
|
+
"fileHash": "7505b6c7",
|
|
23
23
|
"needsInterop": false
|
|
24
24
|
},
|
|
25
25
|
"astro > aria-query": {
|
|
26
26
|
"src": "../../../../../node_modules/.pnpm/aria-query@5.3.2/node_modules/aria-query/lib/index.js",
|
|
27
27
|
"file": "astro_n_aria-query.js",
|
|
28
|
-
"fileHash": "
|
|
28
|
+
"fileHash": "eda53a8d",
|
|
29
29
|
"needsInterop": true
|
|
30
30
|
},
|
|
31
31
|
"astro > axobject-query": {
|
|
32
32
|
"src": "../../../../../node_modules/.pnpm/axobject-query@4.1.0/node_modules/axobject-query/lib/index.js",
|
|
33
33
|
"file": "astro_n_axobject-query.js",
|
|
34
|
-
"fileHash": "
|
|
34
|
+
"fileHash": "3a839b14",
|
|
35
35
|
"needsInterop": true
|
|
36
36
|
},
|
|
37
37
|
"astro > html-escaper": {
|
|
38
38
|
"src": "../../../../../node_modules/.pnpm/html-escaper@3.0.3/node_modules/html-escaper/esm/index.js",
|
|
39
39
|
"file": "astro_n_html-escaper.js",
|
|
40
|
-
"fileHash": "
|
|
40
|
+
"fileHash": "842bf3b2",
|
|
41
41
|
"needsInterop": false
|
|
42
42
|
},
|
|
43
43
|
"astro/runtime/client/dev-toolbar/entrypoint.js": {
|
|
44
44
|
"src": "../../../../../node_modules/.pnpm/astro@7.2.10_@emnapi+core@1.11.1_@emnapi+runtime@1.11.3_@types+node@26.4.1_jiti@2.7.0_tsx@4.23.13_yaml@2.9.0/node_modules/astro/dist/runtime/client/dev-toolbar/entrypoint.js",
|
|
45
45
|
"file": "astro_runtime_client_dev-toolbar_entrypoint__js.js",
|
|
46
|
-
"fileHash": "
|
|
46
|
+
"fileHash": "58367797",
|
|
47
47
|
"needsInterop": false
|
|
48
48
|
},
|
|
49
49
|
"preact": {
|
|
50
50
|
"src": "../../../../../node_modules/.pnpm/preact@10.29.8_preact-render-to-string@6.7.0/node_modules/preact/dist/preact.module.js",
|
|
51
51
|
"file": "preact.js",
|
|
52
|
-
"fileHash": "
|
|
52
|
+
"fileHash": "f07c9fbd",
|
|
53
53
|
"needsInterop": false
|
|
54
54
|
},
|
|
55
55
|
"preact/compat": {
|
|
56
56
|
"src": "../../../../../node_modules/.pnpm/preact@10.29.8_preact-render-to-string@6.7.0/node_modules/preact/compat/dist/compat.module.js",
|
|
57
57
|
"file": "preact_compat.js",
|
|
58
|
-
"fileHash": "
|
|
58
|
+
"fileHash": "848429c1",
|
|
59
59
|
"needsInterop": false
|
|
60
60
|
},
|
|
61
61
|
"preact/compat/jsx-runtime": {
|
|
62
62
|
"src": "../../../../../node_modules/.pnpm/preact@10.29.8_preact-render-to-string@6.7.0/node_modules/preact/compat/jsx-runtime.mjs",
|
|
63
63
|
"file": "preact_compat_jsx-runtime.js",
|
|
64
|
-
"fileHash": "
|
|
64
|
+
"fileHash": "8451bcf5",
|
|
65
65
|
"needsInterop": false
|
|
66
66
|
},
|
|
67
67
|
"preact/debug": {
|
|
68
68
|
"src": "../../../../../node_modules/.pnpm/preact@10.29.8_preact-render-to-string@6.7.0/node_modules/preact/debug/dist/debug.module.js",
|
|
69
69
|
"file": "preact_debug.js",
|
|
70
|
-
"fileHash": "
|
|
70
|
+
"fileHash": "b7df4cef",
|
|
71
71
|
"needsInterop": false
|
|
72
72
|
},
|
|
73
73
|
"preact/devtools": {
|
|
74
74
|
"src": "../../../../../node_modules/.pnpm/preact@10.29.8_preact-render-to-string@6.7.0/node_modules/preact/devtools/dist/devtools.module.js",
|
|
75
75
|
"file": "preact_devtools.js",
|
|
76
|
-
"fileHash": "
|
|
76
|
+
"fileHash": "6ceae058",
|
|
77
77
|
"needsInterop": false
|
|
78
78
|
},
|
|
79
79
|
"preact/hooks": {
|
|
80
80
|
"src": "../../../../../node_modules/.pnpm/preact@10.29.8_preact-render-to-string@6.7.0/node_modules/preact/hooks/dist/hooks.module.js",
|
|
81
81
|
"file": "preact_hooks.js",
|
|
82
|
-
"fileHash": "
|
|
82
|
+
"fileHash": "937b89df",
|
|
83
83
|
"needsInterop": false
|
|
84
84
|
},
|
|
85
85
|
"preact/jsx-dev-runtime": {
|
|
86
86
|
"src": "../../../../../node_modules/.pnpm/preact@10.29.8_preact-render-to-string@6.7.0/node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js",
|
|
87
87
|
"file": "preact_jsx-dev-runtime.js",
|
|
88
|
-
"fileHash": "
|
|
88
|
+
"fileHash": "debc9d4c",
|
|
89
89
|
"needsInterop": false
|
|
90
90
|
},
|
|
91
91
|
"preact/jsx-runtime": {
|
|
92
92
|
"src": "../../../../../node_modules/.pnpm/preact@10.29.8_preact-render-to-string@6.7.0/node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js",
|
|
93
93
|
"file": "preact_jsx-runtime.js",
|
|
94
|
-
"fileHash": "
|
|
94
|
+
"fileHash": "0bafc776",
|
|
95
95
|
"needsInterop": false
|
|
96
96
|
},
|
|
97
97
|
"preact/test-utils": {
|
|
98
98
|
"src": "../../../../../node_modules/.pnpm/preact@10.29.8_preact-render-to-string@6.7.0/node_modules/preact/test-utils/dist/testUtils.module.js",
|
|
99
99
|
"file": "preact_test-utils.js",
|
|
100
|
-
"fileHash": "
|
|
100
|
+
"fileHash": "3f269260",
|
|
101
101
|
"needsInterop": false
|
|
102
102
|
}
|
|
103
103
|
},
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { ChromeNavItem } from "@/lib/chrome/model";
|
|
11
11
|
import NavDropdown from "@/components/chrome/NavDropdown.astro";
|
|
12
|
+
import { CHROME_PLACEMENT } from "@/lib/chrome/trackingPlacements";
|
|
12
13
|
|
|
13
14
|
interface Props {
|
|
14
15
|
items: ChromeNavItem[];
|
|
@@ -21,6 +22,15 @@ const { items, ariaLabel = "Primary", class: className } = Astro.props;
|
|
|
21
22
|
|
|
22
23
|
<nav aria-label={ariaLabel} class:list={["hidden md:block", className]}>
|
|
23
24
|
<ul class="flex items-center gap-6">
|
|
24
|
-
{
|
|
25
|
+
{
|
|
26
|
+
items.map((item, i) => (
|
|
27
|
+
<NavDropdown
|
|
28
|
+
item={item}
|
|
29
|
+
placement={CHROME_PLACEMENT.primaryNav}
|
|
30
|
+
menuPlacement={CHROME_PLACEMENT.megaMenu}
|
|
31
|
+
index={i}
|
|
32
|
+
/>
|
|
33
|
+
))
|
|
34
|
+
}
|
|
25
35
|
</ul>
|
|
26
36
|
</nav>
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import type { ChromeNavItem, ChromeLink } from "@/lib/chrome/model";
|
|
19
19
|
import { withBase } from "@/lib/basePath";
|
|
20
20
|
import { isCurrentNavHref } from "@/lib/chrome/navCurrent";
|
|
21
|
+
import { trackingAttributes } from "@tot/public-runtime";
|
|
21
22
|
|
|
22
23
|
interface Props {
|
|
23
24
|
item: ChromeNavItem;
|
|
@@ -26,9 +27,19 @@ interface Props {
|
|
|
26
27
|
* "accordion" = mobile stacked item using a native <details> disclosure.
|
|
27
28
|
*/
|
|
28
29
|
mode?: "bar" | "accordion";
|
|
30
|
+
/**
|
|
31
|
+
* `data-tot-placement` for this item's OWN link — explicit, passed by the
|
|
32
|
+
* caller (MegaMenu for mode="bar", SiteHeader's no-JS mobile fallback for
|
|
33
|
+
* mode="accordion"). Never inferred from `mode`.
|
|
34
|
+
*/
|
|
35
|
+
placement: string;
|
|
36
|
+
/** `data-tot-placement` for this item's dropdown/mega children. */
|
|
37
|
+
menuPlacement: string;
|
|
38
|
+
/** This item's zero-based position within the top-level nav list (`data-tot-i`, scope "nav-primary"). */
|
|
39
|
+
index: number;
|
|
29
40
|
}
|
|
30
41
|
|
|
31
|
-
const { item, mode = "bar" } = Astro.props;
|
|
42
|
+
const { item, mode = "bar", placement, menuPlacement, index } = Astro.props;
|
|
32
43
|
const { basePath } = Astro.locals;
|
|
33
44
|
|
|
34
45
|
const itemHref = withBase(basePath, item.href);
|
|
@@ -44,12 +55,37 @@ const hasMenu =
|
|
|
44
55
|
const isMega = !!(item.columns && item.columns.length > 0);
|
|
45
56
|
// Normalize columns/children into titled groups for the accordion body so the
|
|
46
57
|
// same markup renders a mega panel (multiple titled columns) or a single-column
|
|
47
|
-
// dropdown (one untitled group).
|
|
48
|
-
|
|
58
|
+
// dropdown (one untitled group). Each link is paired with its flat position
|
|
59
|
+
// within THIS item's repeat container (scope `nav-mega:<item.id>`).
|
|
60
|
+
let flatChildIndex = 0;
|
|
61
|
+
const rawGroups = isMega
|
|
49
62
|
? item.columns!.map((c) => ({ title: c.title, links: c.links }))
|
|
50
63
|
: item.children
|
|
51
64
|
? [{ title: undefined as string | undefined, links: item.children }]
|
|
52
65
|
: [];
|
|
66
|
+
const groups = rawGroups.map((g) => ({
|
|
67
|
+
title: g.title,
|
|
68
|
+
links: g.links.map((link) => ({ link, index: flatChildIndex++ })),
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
const itemStamp = trackingAttributes({
|
|
72
|
+
el: item.id,
|
|
73
|
+
action: item.action,
|
|
74
|
+
tracking: item.tracking,
|
|
75
|
+
placement,
|
|
76
|
+
scope: "nav-primary",
|
|
77
|
+
index,
|
|
78
|
+
});
|
|
79
|
+
const megaScope = `nav-mega:${item.id}`;
|
|
80
|
+
const childStamp = (link: ChromeLink, childIndex: number) =>
|
|
81
|
+
trackingAttributes({
|
|
82
|
+
el: link.id,
|
|
83
|
+
action: link.action,
|
|
84
|
+
tracking: link.tracking,
|
|
85
|
+
placement: menuPlacement,
|
|
86
|
+
scope: megaScope,
|
|
87
|
+
index: childIndex,
|
|
88
|
+
});
|
|
53
89
|
---
|
|
54
90
|
|
|
55
91
|
{
|
|
@@ -79,13 +115,14 @@ const groups = isMega
|
|
|
79
115
|
</p>
|
|
80
116
|
)}
|
|
81
117
|
<ul class="grid gap-0.5">
|
|
82
|
-
{col.links.map((child) => (
|
|
118
|
+
{col.links.map(({ link: child, index: childIndex }) => (
|
|
83
119
|
<li>
|
|
84
120
|
<a
|
|
85
121
|
href={hrefOf(child)}
|
|
86
122
|
aria-current={isChildCurrent(child) ? "page" : undefined}
|
|
87
123
|
class="flex items-center gap-1.5 rounded-sm px-2 py-1.5 text-sm text-muted transition-colors hover:bg-bg hover:text-primary"
|
|
88
124
|
{...(child.external ? { rel: "noopener noreferrer" } : { "data-astro-prefetch": true })}
|
|
125
|
+
{...childStamp(child, childIndex)}
|
|
89
126
|
>
|
|
90
127
|
{child.label}
|
|
91
128
|
{child.badge && (
|
|
@@ -112,6 +149,7 @@ const groups = isMega
|
|
|
112
149
|
"flex items-center gap-1.5 border-b border-border py-2.5 text-sm font-medium text-text transition-colors last:border-b-0 hover:text-accent",
|
|
113
150
|
item.flag && "text-accent",
|
|
114
151
|
]}
|
|
152
|
+
{...itemStamp}
|
|
115
153
|
>
|
|
116
154
|
{item.label}
|
|
117
155
|
{item.badge && (
|
|
@@ -133,6 +171,7 @@ const groups = isMega
|
|
|
133
171
|
item.flag && "font-semibold text-accent",
|
|
134
172
|
]}
|
|
135
173
|
aria-haspopup="true"
|
|
174
|
+
{...itemStamp}
|
|
136
175
|
>
|
|
137
176
|
{item.label}
|
|
138
177
|
{item.badge && (
|
|
@@ -146,7 +185,7 @@ const groups = isMega
|
|
|
146
185
|
<div class="invisible absolute left-0 top-full z-50 translate-y-1 opacity-0 transition-all duration-150 group-hover:visible group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:visible group-focus-within:translate-y-0 group-focus-within:opacity-100">
|
|
147
186
|
{isMega ? (
|
|
148
187
|
<div class="mt-1 grid gap-6 rounded-md border border-border bg-surface p-5 shadow-md sm:grid-cols-2 lg:grid-cols-3">
|
|
149
|
-
{
|
|
188
|
+
{groups.map((col) => (
|
|
150
189
|
<div class="min-w-44">
|
|
151
190
|
{col.title && (
|
|
152
191
|
<p class="mb-2 font-mono text-[0.62rem] uppercase tracking-[0.12em] text-muted">
|
|
@@ -154,13 +193,14 @@ const groups = isMega
|
|
|
154
193
|
</p>
|
|
155
194
|
)}
|
|
156
195
|
<ul class="grid gap-0.5">
|
|
157
|
-
{col.links.map((child) => (
|
|
196
|
+
{col.links.map(({ link: child, index: childIndex }) => (
|
|
158
197
|
<li>
|
|
159
198
|
<a
|
|
160
199
|
href={hrefOf(child)}
|
|
161
200
|
aria-current={isChildCurrent(child) ? "page" : undefined}
|
|
162
201
|
class="flex items-center gap-1.5 rounded-sm px-2 py-1 text-sm text-muted transition-colors hover:bg-bg hover:text-primary"
|
|
163
202
|
{...(child.external ? { rel: "noopener noreferrer" } : { "data-astro-prefetch": true })}
|
|
203
|
+
{...childStamp(child, childIndex)}
|
|
164
204
|
>
|
|
165
205
|
{child.label}
|
|
166
206
|
{child.badge && (
|
|
@@ -177,13 +217,14 @@ const groups = isMega
|
|
|
177
217
|
</div>
|
|
178
218
|
) : (
|
|
179
219
|
<ul class="mt-1 grid min-w-56 gap-0.5 rounded-md border border-border bg-surface p-3 shadow-md">
|
|
180
|
-
{
|
|
220
|
+
{groups[0]?.links.map(({ link: child, index: childIndex }) => (
|
|
181
221
|
<li>
|
|
182
222
|
<a
|
|
183
223
|
href={hrefOf(child)}
|
|
184
224
|
aria-current={isChildCurrent(child) ? "page" : undefined}
|
|
185
225
|
class="block rounded-sm px-3 py-1.5 text-sm text-muted transition-colors hover:bg-bg hover:text-primary"
|
|
186
226
|
{...(child.external ? { rel: "noopener noreferrer" } : { "data-astro-prefetch": true })}
|
|
227
|
+
{...childStamp(child, childIndex)}
|
|
187
228
|
>
|
|
188
229
|
{child.label}
|
|
189
230
|
</a>
|
|
@@ -203,6 +244,7 @@ const groups = isMega
|
|
|
203
244
|
"inline-flex items-center gap-1 py-2 text-sm transition-colors hover:text-accent",
|
|
204
245
|
item.flag && "font-semibold text-accent",
|
|
205
246
|
]}
|
|
247
|
+
{...itemStamp}
|
|
206
248
|
>
|
|
207
249
|
{item.label}
|
|
208
250
|
{item.badge && (
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { ChromeLink, FooterColumn, FooterNewsletter, SocialLink, ChromeLogo, FooterVariant } from "@/lib/chrome/model";
|
|
11
11
|
import { withBase } from "@/lib/basePath";
|
|
12
|
+
import { CHROME_PLACEMENT } from "@/lib/chrome/trackingPlacements";
|
|
13
|
+
import { trackingAttributes } from "@tot/public-runtime";
|
|
12
14
|
|
|
13
15
|
interface Props {
|
|
14
16
|
columns: FooterColumn[];
|
|
@@ -46,6 +48,24 @@ const {
|
|
|
46
48
|
const { basePath } = Astro.locals;
|
|
47
49
|
const hrefOf = (l: ChromeLink) => (l.external ? l.href : withBase(basePath, l.href));
|
|
48
50
|
const showLogo = !!brand?.logo && /^(https?:|\/)/.test(brand.logo.src);
|
|
51
|
+
const columnLinkStamp = (l: ChromeLink, colIndex: number, index: number) =>
|
|
52
|
+
trackingAttributes({
|
|
53
|
+
el: l.id,
|
|
54
|
+
action: l.action,
|
|
55
|
+
tracking: l.tracking,
|
|
56
|
+
placement: CHROME_PLACEMENT.footer,
|
|
57
|
+
scope: `footer-col:${colIndex}`,
|
|
58
|
+
index,
|
|
59
|
+
});
|
|
60
|
+
const bottomLinkStamp = (l: ChromeLink, index: number) =>
|
|
61
|
+
trackingAttributes({
|
|
62
|
+
el: l.id,
|
|
63
|
+
action: l.action,
|
|
64
|
+
tracking: l.tracking,
|
|
65
|
+
placement: CHROME_PLACEMENT.footerBottom,
|
|
66
|
+
scope: "footer-bottom",
|
|
67
|
+
index,
|
|
68
|
+
});
|
|
49
69
|
---
|
|
50
70
|
|
|
51
71
|
<footer class:list={["border-t border-border bg-surface text-text", className]} data-variant={variant}>
|
|
@@ -116,16 +136,17 @@ const showLogo = !!brand?.logo && /^(https?:|\/)/.test(brand.logo.src);
|
|
|
116
136
|
|
|
117
137
|
<!-- Link columns -->
|
|
118
138
|
{
|
|
119
|
-
columns.map((col) => (
|
|
139
|
+
columns.map((col, colIndex) => (
|
|
120
140
|
<div>
|
|
121
141
|
<h3 class="font-display text-sm font-semibold">{col.title}</h3>
|
|
122
142
|
<ul class="mt-3 grid gap-2">
|
|
123
|
-
{col.links.map((l) => (
|
|
143
|
+
{col.links.map((l, i) => (
|
|
124
144
|
<li>
|
|
125
145
|
<a
|
|
126
146
|
href={hrefOf(l)}
|
|
127
147
|
class="text-sm text-muted transition-colors hover:text-primary"
|
|
128
148
|
{...(l.external ? { rel: "noopener noreferrer" } : {})}
|
|
149
|
+
{...columnLinkStamp(l, colIndex, i)}
|
|
129
150
|
>
|
|
130
151
|
{l.label}
|
|
131
152
|
</a>
|
|
@@ -198,9 +219,13 @@ const showLogo = !!brand?.logo && /^(https?:|\/)/.test(brand.logo.src);
|
|
|
198
219
|
bottomLinks.length > 0 && (
|
|
199
220
|
<nav aria-label="Legal">
|
|
200
221
|
<ul class="flex flex-wrap gap-x-5 gap-y-1">
|
|
201
|
-
{bottomLinks.map((l) => (
|
|
222
|
+
{bottomLinks.map((l, i) => (
|
|
202
223
|
<li>
|
|
203
|
-
<a
|
|
224
|
+
<a
|
|
225
|
+
href={hrefOf(l)}
|
|
226
|
+
class="transition-colors hover:text-primary"
|
|
227
|
+
{...bottomLinkStamp(l, i)}
|
|
228
|
+
>
|
|
204
229
|
{l.label}
|
|
205
230
|
</a>
|
|
206
231
|
</li>
|
|
@@ -19,6 +19,9 @@ import NavDropdown from "@/components/chrome/NavDropdown.astro";
|
|
|
19
19
|
import SearchTypeahead from "@/components/islands/SearchTypeahead.tsx";
|
|
20
20
|
import MobileNav from "@/components/islands/MobileNav.tsx";
|
|
21
21
|
import { withBase } from "@/lib/basePath";
|
|
22
|
+
import { ctaIdentityAttr } from "@/lib/chrome/ctaIdentity";
|
|
23
|
+
import { CHROME_PLACEMENT } from "@/lib/chrome/trackingPlacements";
|
|
24
|
+
import { trackingAttributes } from "@tot/public-runtime";
|
|
22
25
|
|
|
23
26
|
interface Props {
|
|
24
27
|
logo?: ChromeLogo;
|
|
@@ -97,11 +100,17 @@ const ctaClass = (v?: string) =>
|
|
|
97
100
|
// source produces `columns` today; NavDropdown's accordion mode is the richer
|
|
98
101
|
// zero-JS alternative if per-column grouping is ever needed on mobile.
|
|
99
102
|
const mobileNav = nav.map((item) => ({
|
|
103
|
+
id: item.id,
|
|
100
104
|
label: item.label,
|
|
101
105
|
href: withBase(basePath, item.href),
|
|
106
|
+
action: item.action,
|
|
107
|
+
tracking: item.tracking,
|
|
102
108
|
children: (item.children ?? item.columns?.flatMap((c) => c.links))?.map((child) => ({
|
|
109
|
+
id: child.id,
|
|
103
110
|
label: child.label,
|
|
104
111
|
href: withBase(basePath, child.href),
|
|
112
|
+
action: child.action,
|
|
113
|
+
tracking: child.tracking,
|
|
105
114
|
})),
|
|
106
115
|
}));
|
|
107
116
|
|
|
@@ -186,11 +195,27 @@ const currentPath = Astro.url.pathname;
|
|
|
186
195
|
{
|
|
187
196
|
!minimal && headerCtas.length > 0 && (
|
|
188
197
|
<div class="hidden items-center gap-2 sm:flex">
|
|
189
|
-
{headerCtas.map((cta) =>
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
198
|
+
{headerCtas.map((cta, i) => {
|
|
199
|
+
const idAttr = ctaIdentityAttr(cta);
|
|
200
|
+
const stamp = trackingAttributes({
|
|
201
|
+
el: cta.id,
|
|
202
|
+
action: cta.action,
|
|
203
|
+
tracking: cta.tracking,
|
|
204
|
+
placement: CHROME_PLACEMENT.header,
|
|
205
|
+
scope: "header-ctas",
|
|
206
|
+
index: i,
|
|
207
|
+
});
|
|
208
|
+
return (
|
|
209
|
+
<a
|
|
210
|
+
href={withBase(basePath, cta.href)}
|
|
211
|
+
class={ctaClass(cta.variant)}
|
|
212
|
+
{...{ [idAttr.name]: idAttr.value }}
|
|
213
|
+
{...stamp}
|
|
214
|
+
>
|
|
215
|
+
{cta.label}
|
|
216
|
+
</a>
|
|
217
|
+
);
|
|
218
|
+
})}
|
|
194
219
|
</div>
|
|
195
220
|
)
|
|
196
221
|
}
|
|
@@ -269,7 +294,15 @@ const currentPath = Astro.url.pathname;
|
|
|
269
294
|
<MegaMenu items={nav} />
|
|
270
295
|
<nav aria-label="Mobile" data-mobile-nav-fallback class="md:hidden">
|
|
271
296
|
<ul class="grid">
|
|
272
|
-
{nav.map((item) =>
|
|
297
|
+
{nav.map((item, i) => (
|
|
298
|
+
<NavDropdown
|
|
299
|
+
item={item}
|
|
300
|
+
mode="accordion"
|
|
301
|
+
placement={CHROME_PLACEMENT.mobileMenu}
|
|
302
|
+
menuPlacement={CHROME_PLACEMENT.mobileMenu}
|
|
303
|
+
index={i}
|
|
304
|
+
/>
|
|
305
|
+
))}
|
|
273
306
|
</ul>
|
|
274
307
|
</nav>
|
|
275
308
|
</div>
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { ChromeLink, SocialLink } from "@/lib/chrome/model";
|
|
9
9
|
import { withBase } from "@/lib/basePath";
|
|
10
|
+
import { CHROME_PLACEMENT } from "@/lib/chrome/trackingPlacements";
|
|
11
|
+
import { trackingAttributes } from "@tot/public-runtime";
|
|
10
12
|
|
|
11
13
|
interface Props {
|
|
12
14
|
links?: ChromeLink[];
|
|
@@ -19,6 +21,15 @@ interface Props {
|
|
|
19
21
|
const { links = [], locale, currency, social = [], class: className } = Astro.props;
|
|
20
22
|
const { basePath } = Astro.locals;
|
|
21
23
|
const hrefOf = (l: ChromeLink) => (l.external ? l.href : withBase(basePath, l.href));
|
|
24
|
+
const linkStamp = (l: ChromeLink, index: number) =>
|
|
25
|
+
trackingAttributes({
|
|
26
|
+
el: l.id,
|
|
27
|
+
action: l.action,
|
|
28
|
+
tracking: l.tracking,
|
|
29
|
+
placement: CHROME_PLACEMENT.utilityNav,
|
|
30
|
+
scope: "utility-nav",
|
|
31
|
+
index,
|
|
32
|
+
});
|
|
22
33
|
---
|
|
23
34
|
|
|
24
35
|
<div
|
|
@@ -30,12 +41,13 @@ const hrefOf = (l: ChromeLink) => (l.external ? l.href : withBase(basePath, l.hr
|
|
|
30
41
|
<div class="container-prose flex h-9 items-center justify-between gap-4 text-[0.72rem]">
|
|
31
42
|
<ul class="flex items-center gap-5">
|
|
32
43
|
{
|
|
33
|
-
links.map((l) => (
|
|
44
|
+
links.map((l, i) => (
|
|
34
45
|
<li>
|
|
35
46
|
<a
|
|
36
47
|
href={hrefOf(l)}
|
|
37
48
|
class="transition-colors hover:text-primary"
|
|
38
49
|
{...(l.external ? { rel: "noopener noreferrer" } : {})}
|
|
50
|
+
{...linkStamp(l, i)}
|
|
39
51
|
>
|
|
40
52
|
{l.label}
|
|
41
53
|
</a>
|
|
@@ -12,14 +12,30 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
|
14
14
|
import { isCurrentNavHref } from "@/lib/chrome/navCurrent";
|
|
15
|
+
import { CHROME_PLACEMENT } from "@/lib/chrome/trackingPlacements";
|
|
16
|
+
import { trackingAttributes } from "@tot/public-runtime";
|
|
15
17
|
|
|
18
|
+
/**
|
|
19
|
+
* `action`/`tracking` are declared loosely here (not the strict
|
|
20
|
+
* `TrackingDeclaration` union `SiteHeader.astro` maps from) — a fresh object
|
|
21
|
+
* literal spreading a discriminated union's fields widens to two independent
|
|
22
|
+
* optional properties, so TS can no longer see "exactly one of the two" at
|
|
23
|
+
* the type level. `trackingAttributes()` is still the runtime oracle: it
|
|
24
|
+
* throws on an invalid combination regardless of what the type allows.
|
|
25
|
+
*/
|
|
16
26
|
interface NavChild {
|
|
27
|
+
id: string;
|
|
17
28
|
label: string;
|
|
18
29
|
href: string;
|
|
30
|
+
action?: string;
|
|
31
|
+
tracking?: "none";
|
|
19
32
|
}
|
|
20
33
|
interface NavItem {
|
|
34
|
+
id: string;
|
|
21
35
|
label: string;
|
|
22
36
|
href: string;
|
|
37
|
+
action?: string;
|
|
38
|
+
tracking?: "none";
|
|
23
39
|
children?: NavChild[];
|
|
24
40
|
}
|
|
25
41
|
|
|
@@ -212,7 +228,7 @@ export default function MobileNav({ nav, announcement, currentPath }: Props) {
|
|
|
212
228
|
|
|
213
229
|
<nav aria-label="Mobile" class="flex-1 px-2 py-4">
|
|
214
230
|
<ul class="space-y-1">
|
|
215
|
-
{nav.map((item) => (
|
|
231
|
+
{nav.map((item, i) => (
|
|
216
232
|
<li key={item.href}>
|
|
217
233
|
{item.children && item.children.length > 0 ? (
|
|
218
234
|
<div class="px-3 py-2">
|
|
@@ -222,11 +238,19 @@ export default function MobileNav({ nav, announcement, currentPath }: Props) {
|
|
|
222
238
|
currentPath && isCurrentNavHref(currentPath, item.href) ? "page" : undefined
|
|
223
239
|
}
|
|
224
240
|
class="block font-display text-base font-medium text-text hover:text-primary"
|
|
241
|
+
{...trackingAttributes({
|
|
242
|
+
el: item.id,
|
|
243
|
+
action: item.action,
|
|
244
|
+
tracking: item.tracking,
|
|
245
|
+
placement: CHROME_PLACEMENT.mobileMenu,
|
|
246
|
+
scope: "nav-mobile",
|
|
247
|
+
index: i,
|
|
248
|
+
})}
|
|
225
249
|
>
|
|
226
250
|
{item.label}
|
|
227
251
|
</a>
|
|
228
252
|
<ul class="mt-1 space-y-0.5 border-l border-border pl-3">
|
|
229
|
-
{item.children.map((child) => (
|
|
253
|
+
{item.children.map((child, ci) => (
|
|
230
254
|
<li key={child.href}>
|
|
231
255
|
<a
|
|
232
256
|
href={child.href}
|
|
@@ -234,6 +258,14 @@ export default function MobileNav({ nav, announcement, currentPath }: Props) {
|
|
|
234
258
|
currentPath && isCurrentNavHref(currentPath, child.href) ? "page" : undefined
|
|
235
259
|
}
|
|
236
260
|
class="block rounded px-2 py-1.5 font-body text-sm text-muted hover:bg-border/40 hover:text-text"
|
|
261
|
+
{...trackingAttributes({
|
|
262
|
+
el: child.id,
|
|
263
|
+
action: child.action,
|
|
264
|
+
tracking: child.tracking,
|
|
265
|
+
placement: CHROME_PLACEMENT.mobileMenu,
|
|
266
|
+
scope: `nav-mobile-mega:${item.id}`,
|
|
267
|
+
index: ci,
|
|
268
|
+
})}
|
|
237
269
|
>
|
|
238
270
|
{child.label}
|
|
239
271
|
</a>
|
|
@@ -248,6 +280,14 @@ export default function MobileNav({ nav, announcement, currentPath }: Props) {
|
|
|
248
280
|
currentPath && isCurrentNavHref(currentPath, item.href) ? "page" : undefined
|
|
249
281
|
}
|
|
250
282
|
class="block rounded px-3 py-2.5 font-display text-base font-medium text-text hover:bg-border/40 hover:text-primary"
|
|
283
|
+
{...trackingAttributes({
|
|
284
|
+
el: item.id,
|
|
285
|
+
action: item.action,
|
|
286
|
+
tracking: item.tracking,
|
|
287
|
+
placement: CHROME_PLACEMENT.mobileMenu,
|
|
288
|
+
scope: "nav-mobile",
|
|
289
|
+
index: i,
|
|
290
|
+
})}
|
|
251
291
|
>
|
|
252
292
|
{item.label}
|
|
253
293
|
</a>
|