bsmnt 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/dist/application/add-integration/index.d.ts.map +1 -1
  2. package/dist/application/add-integration/index.js +23 -0
  3. package/dist/application/add-integration/index.js.map +1 -1
  4. package/dist/domain/consent.d.ts +17 -0
  5. package/dist/domain/consent.d.ts.map +1 -0
  6. package/dist/domain/consent.js +13 -0
  7. package/dist/domain/consent.js.map +1 -0
  8. package/dist/domain/integration.d.ts +19 -2
  9. package/dist/domain/integration.d.ts.map +1 -1
  10. package/dist/domain/integration.js +10 -1
  11. package/dist/domain/integration.js.map +1 -1
  12. package/dist/domain/jobs.d.ts +17 -0
  13. package/dist/domain/jobs.d.ts.map +1 -0
  14. package/dist/domain/jobs.js +13 -0
  15. package/dist/domain/jobs.js.map +1 -0
  16. package/dist/domain/skills.d.ts +23 -2
  17. package/dist/domain/skills.d.ts.map +1 -1
  18. package/dist/domain/skills.js +23 -2
  19. package/dist/domain/skills.js.map +1 -1
  20. package/dist/domain/templates.d.ts +7 -1
  21. package/dist/domain/templates.d.ts.map +1 -1
  22. package/dist/domain/templates.js +2 -0
  23. package/dist/domain/templates.js.map +1 -1
  24. package/dist/index.js +34 -9
  25. package/dist/index.js.map +1 -1
  26. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  27. package/dist/infrastructure/create/setup-agent.js +11 -4
  28. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  29. package/dist/infrastructure/integrations/inject-integration.d.ts.map +1 -1
  30. package/dist/infrastructure/integrations/inject-integration.js +36 -2
  31. package/dist/infrastructure/integrations/inject-integration.js.map +1 -1
  32. package/dist/modules/features/consent/ketch/config.d.ts +18 -0
  33. package/dist/modules/features/consent/ketch/config.d.ts.map +1 -0
  34. package/dist/modules/features/consent/ketch/config.js +24 -0
  35. package/dist/modules/features/consent/ketch/config.js.map +1 -0
  36. package/dist/modules/features/consent/ketch/mergers/biome.d.ts +16 -0
  37. package/dist/modules/features/consent/ketch/mergers/biome.d.ts.map +1 -0
  38. package/dist/modules/features/consent/ketch/mergers/biome.js +31 -0
  39. package/dist/modules/features/consent/ketch/mergers/biome.js.map +1 -0
  40. package/dist/modules/features/consent/ketch/mergers/check-integration.d.ts +13 -0
  41. package/dist/modules/features/consent/ketch/mergers/check-integration.d.ts.map +1 -0
  42. package/dist/modules/features/consent/ketch/mergers/check-integration.js +64 -0
  43. package/dist/modules/features/consent/ketch/mergers/check-integration.js.map +1 -0
  44. package/dist/modules/features/consent/ketch/mergers/env-example.d.ts +12 -0
  45. package/dist/modules/features/consent/ketch/mergers/env-example.d.ts.map +1 -0
  46. package/dist/modules/features/consent/ketch/mergers/env-example.js +28 -0
  47. package/dist/modules/features/consent/ketch/mergers/env-example.js.map +1 -0
  48. package/dist/modules/features/consent/ketch/mergers/footer.d.ts +17 -0
  49. package/dist/modules/features/consent/ketch/mergers/footer.d.ts.map +1 -0
  50. package/dist/modules/features/consent/ketch/mergers/footer.js +40 -0
  51. package/dist/modules/features/consent/ketch/mergers/footer.js.map +1 -0
  52. package/dist/modules/features/consent/ketch/mergers/knip.d.ts +18 -0
  53. package/dist/modules/features/consent/ketch/mergers/knip.d.ts.map +1 -0
  54. package/dist/modules/features/consent/ketch/mergers/knip.js +37 -0
  55. package/dist/modules/features/consent/ketch/mergers/knip.js.map +1 -0
  56. package/dist/modules/features/consent/ketch/mergers/layout.d.ts +25 -0
  57. package/dist/modules/features/consent/ketch/mergers/layout.d.ts.map +1 -0
  58. package/dist/modules/features/consent/ketch/mergers/layout.js +55 -0
  59. package/dist/modules/features/consent/ketch/mergers/layout.js.map +1 -0
  60. package/dist/modules/features/jobs/ashby/config.d.ts +19 -0
  61. package/dist/modules/features/jobs/ashby/config.d.ts.map +1 -0
  62. package/dist/modules/features/jobs/ashby/config.js +29 -0
  63. package/dist/modules/features/jobs/ashby/config.js.map +1 -0
  64. package/dist/modules/features/jobs/ashby/mergers/check-integration.d.ts +14 -0
  65. package/dist/modules/features/jobs/ashby/mergers/check-integration.d.ts.map +1 -0
  66. package/dist/modules/features/jobs/ashby/mergers/check-integration.js +78 -0
  67. package/dist/modules/features/jobs/ashby/mergers/check-integration.js.map +1 -0
  68. package/dist/modules/features/jobs/ashby/mergers/env-example.d.ts +12 -0
  69. package/dist/modules/features/jobs/ashby/mergers/env-example.d.ts.map +1 -0
  70. package/dist/modules/features/jobs/ashby/mergers/env-example.js +27 -0
  71. package/dist/modules/features/jobs/ashby/mergers/env-example.js.map +1 -0
  72. package/dist/modules/features/jobs/ashby/mergers/knip.d.ts +18 -0
  73. package/dist/modules/features/jobs/ashby/mergers/knip.d.ts.map +1 -0
  74. package/dist/modules/features/jobs/ashby/mergers/knip.js +41 -0
  75. package/dist/modules/features/jobs/ashby/mergers/knip.js.map +1 -0
  76. package/dist/paths.d.ts +12 -0
  77. package/dist/paths.d.ts.map +1 -1
  78. package/dist/paths.js +12 -0
  79. package/dist/paths.js.map +1 -1
  80. package/package.json +3 -1
  81. package/src/agent-skills/ashby-pagebuilder-block/SKILL.md +277 -0
  82. package/src/agent-skills/hubspot-pagebuilder-block/SKILL.md +58 -25
  83. package/src/agent-skills/ketch-consent-check/SKILL.md +197 -0
  84. package/src/agent-skills/ketch-consent-setup/SKILL.md +326 -0
  85. package/src/agent-skills/ketch-consent-setup/references/ui-customization.md +329 -0
  86. package/src/agent-skills/page-builder-block/SKILL.md +150 -0
  87. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
  88. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
  89. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
  90. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +6 -3
  91. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +8 -0
  92. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +12 -0
  93. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
  94. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
  95. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
  96. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
  97. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
  98. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
  99. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +8 -0
  100. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
  101. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
  102. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
  103. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
  104. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
  105. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +7 -0
  106. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
  107. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
  108. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
  109. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/layout.ts +4 -2
  110. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +8 -0
  111. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +12 -0
  112. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
  113. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
  114. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
  115. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
  116. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
  117. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
  118. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +8 -0
  119. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
  120. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
  121. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
  122. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
  123. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
  124. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +7 -0
  125. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +2 -0
  126. package/src/modules/features/consent/ketch/files/lib/integrations/check-integration.ts +25 -0
  127. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/README.md +70 -0
  128. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/categories.ts +20 -0
  129. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/consent-store.ts +107 -0
  130. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/env.ts +20 -0
  131. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/ketch-experience.css +136 -0
  132. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/preferences-button.tsx +51 -0
  133. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/script.tsx +8 -0
  134. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/types.ts +15 -0
  135. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/use-consent.ts +26 -0
  136. package/src/modules/features/jobs/ashby/files/_sanity-adapter/job-board-block.ts +53 -0
  137. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/README.md +134 -0
  138. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/env.ts +3 -0
  139. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/index.ts +40 -0
  140. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/normalize.ts +124 -0
  141. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/types.ts +20 -0
  142. package/src/modules/features/jobs/ashby/files/lib/integrations/check-integration.ts +25 -0
  143. package/src/templates/next-pagebuilder/AGENTS.md +129 -0
  144. package/src/templates/next-pagebuilder/CLAUDE.md +3 -0
  145. package/src/templates/next-pagebuilder/README.md +70 -0
  146. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +2 -0
  147. package/src/templates/next-pagebuilder/components/layout/spacing-scale/index.tsx +18 -0
  148. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +1 -1
  149. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +2 -2
  150. package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +3 -7
  151. package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +2 -2
  152. package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +3 -7
  153. package/src/templates/next-pagebuilder/components/page-builder/components/rich-text-block.tsx +3 -5
  154. package/src/templates/next-pagebuilder/components/page-builder/registry.tsx +70 -0
  155. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +33 -49
  156. package/src/templates/next-pagebuilder/components/page-builder/section.tsx +35 -0
  157. package/src/templates/next-pagebuilder/components/page-document/index.tsx +1 -1
  158. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +92 -4
  159. package/src/templates/next-pagebuilder-cache/AGENTS.md +136 -0
  160. package/src/templates/next-pagebuilder-cache/CLAUDE.md +3 -0
  161. package/src/templates/next-pagebuilder-cache/README.md +75 -0
  162. package/src/templates/next-pagebuilder-cache/app/(content)/layout.tsx +2 -0
  163. package/src/templates/next-pagebuilder-cache/components/layout/spacing-scale/index.tsx +46 -0
  164. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +1 -1
  165. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/index.tsx +2 -2
  166. package/src/templates/next-pagebuilder-cache/components/page-builder/components/description.tsx +3 -7
  167. package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +2 -2
  168. package/src/templates/next-pagebuilder-cache/components/page-builder/components/hero.tsx +3 -7
  169. package/src/templates/next-pagebuilder-cache/components/page-builder/components/rich-text-block.tsx +3 -5
  170. package/src/templates/next-pagebuilder-cache/components/page-builder/registry.tsx +70 -0
  171. package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +33 -49
  172. package/src/templates/next-pagebuilder-cache/components/page-builder/section.tsx +35 -0
  173. package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +1 -1
  174. package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +92 -4
@@ -0,0 +1,326 @@
1
+ ---
2
+ name: ketch-consent-setup
3
+ description: >-
4
+ Install the Ketch consent gate (CMP) in a basement Next.js project, and control
5
+ its UI — env vars, the Smart Tag, the consent store, where preferences are
6
+ reachable from, and styling. Use when the user asks to "add Ketch", "install
7
+ Ketch", "set up the cookie banner", "set up the consent banner", "add a CMP",
8
+ "add the consent gate", "we need cookie consent". Also use for UI questions
9
+ about an existing Ketch install: "hide the floating cookie button", "move the
10
+ consent widget out of the corner / into the footer", "restyle the consent
11
+ banner", "pass custom styles to Ketch", "make the banner match our design
12
+ tokens", or "why isn't the consent banner showing". Gating an individual
13
+ tracker is the `ketch-consent-check` skill.
14
+ ---
15
+
16
+ # Install the Ketch consent gate
17
+
18
+ Ketch is the CMP. It owns the consent record, the jurisdiction resolution, the
19
+ legal copy, the banner and preferences modal, and the persistence. The project
20
+ owns exactly three things, and this skill wires all three:
21
+
22
+ 1. Loading Ketch's Smart Tag.
23
+ 2. **One** helper that answers "do I have consent for category X?".
24
+ 3. An entry point to reopen preferences after the banner is gone.
25
+
26
+ **Already installed?** If `lib/integrations/ketch/` exists, stop — you want the
27
+ `ketch-consent-check` skill instead, which gates an individual tracker.
28
+
29
+ Read each file first and adapt to its actual shape — do not blindly
30
+ pattern-match. If an anchor below is missing, stop and report it rather than
31
+ guessing.
32
+
33
+ ## 0. Preconditions
34
+
35
+ - A basement Next.js project (App Router).
36
+ - A Ketch account for the client, with a **property** already created. You need
37
+ its org code and property code. If the property doesn't exist yet, that's a
38
+ dashboard task at https://app.ketch.com and it blocks this skill — report it.
39
+ - If you intend to hide Ketch's floating button, a persistent surface every
40
+ template renders — footer, nav, settings menu (in a basement project usually
41
+ `components/layout/footer` reached via `components/layout/wrapper`).
42
+
43
+ ## 1. Env vars
44
+
45
+ Both codes are public — they appear in the boot URL in the page source — so
46
+ they go in as plain values:
47
+
48
+ ```bash
49
+ bun run env:set NEXT_PUBLIC_KETCH_ORG_CODE <org_code> --plain
50
+ bun run env:set NEXT_PUBLIC_KETCH_PROPERTY_CODE <property_code> --plain
51
+ ```
52
+
53
+ Get them from 1Password → **Development** vault → the project's integrations
54
+ item. They are the two path segments of the boot URL:
55
+
56
+ ```
57
+ https://global.ketchcdn.com/web/v3/config/<org>/<property>/boot.js
58
+ ```
59
+
60
+ If all you have is the raw Ketch `<script>` snippet, read both out of that URL.
61
+
62
+ Because they are plain, **nothing needs to be pushed to Vercel** — the build
63
+ reads the committed `.env` and Next inlines them. Do not run
64
+ `env:setup-remote` for these.
65
+
66
+ Document them in `.env.example` too.
67
+
68
+ ## 2. Files — `lib/integrations/ketch/`
69
+
70
+ Follow the shape of the project's other integrations (e.g.
71
+ `lib/integrations/hubspot/`): self-contained folder, relative imports inside it,
72
+ `@/` imports across folders, and a `README.md`.
73
+
74
+ | File | Role |
75
+ | --- | --- |
76
+ | `env.ts` | Validates both codes against `/^[a-z0-9_-]+$/i`, exports `ketchBootUrl` (`null` when unset) |
77
+ | `types.ts` | Types for the `window.ketch` / `window.semaphore` surface |
78
+ | `categories.ts` | Category ↔ Ketch purpose ↔ tracker mapping |
79
+ | `consent-store.ts` | The only module that talks to Ketch |
80
+ | `use-consent.ts` | The helper everything else imports |
81
+ | `script.tsx` | Renders the Smart Tag |
82
+ | `preferences-button.tsx` | Preferences entry point (`cva`, takes `variant` / `size`) |
83
+ | `ketch-experience.css` | Maps Ketch's theme to the project's tokens |
84
+
85
+ ### 2.1 Categories
86
+
87
+ Three stable local names so nothing else hardcodes a Ketch purpose code:
88
+
89
+ ```ts
90
+ export const CONSENT_CATEGORIES = {
91
+ essential: "essential_services",
92
+ analytics: "analytics",
93
+ marketing: "targeted_advertising",
94
+ } as const
95
+ ```
96
+
97
+ Confirm the right-hand side against the property's real purpose codes:
98
+
99
+ ```bash
100
+ curl -s https://global.ketchcdn.com/web/v3/config/<org>/<property>/production/default/en/config.json \
101
+ | jq -r '.purposes[] | "\(.code) \(.name)"'
102
+ ```
103
+
104
+ ### 2.2 The store
105
+
106
+ Module-level state read through `useSyncExternalStore` — no Provider, no
107
+ Context. It attaches `ketch('on', 'consent')` and `ketch('on', 'jurisdiction')`
108
+ once, on first subscribe, and re-notifies subscribers on every preference
109
+ change. `useConsent()` wraps it and exposes `hasConsent(category)`, `granted`,
110
+ `isResolved`, `jurisdiction` and `openPreferences()`.
111
+
112
+ Three facts about Ketch's runtime that the store has to respect:
113
+
114
+ - Consent arrives as `Record<purposeCode, boolean>`, and a purpose key is
115
+ **absent** until a record exists. **Absent must read as denied** — this is
116
+ what guarantees nothing fires before consent.
117
+ - `window.ketch(cmd, …)` pushes onto `window.semaphore`, and Ketch's `boot.js`
118
+ **unshifts** its own `["init", config]` entry. So queuing commands before the
119
+ SDK loads is safe; after init, `semaphore.push` is rebound to dispatch
120
+ directly and `semaphore.loaded` flips to `true`.
121
+ - Persistence is Ketch's, not ours: `_ketch_consent_v1_` in `localStorage` plus
122
+ the `_swb` cookie, surviving navigation and reloads. **Do not add a second
123
+ source of truth** — they drift.
124
+
125
+ ### 2.3 knip
126
+
127
+ Register the folder as an entry or CI fails on "unused exports":
128
+
129
+ ```jsonc
130
+ // knip.json
131
+ "entry": ["lib/integrations/ketch/**/*.{ts,tsx}", /* …existing… */]
132
+ ```
133
+
134
+ ## 3. Load the tag
135
+
136
+ ```tsx
137
+ export const KetchConsentScript = () => {
138
+ if (!ketchBootUrl) return null
139
+ return <script async src={ketchBootUrl} />
140
+ }
141
+ ```
142
+
143
+ **Mount it in the layout that renders the site chrome, not the root layout.**
144
+ In a basement project that is the route group holding the header and footer —
145
+ `app/(content)/layout.tsx` — alongside the entry point from step 4b:
146
+
147
+ ```tsx
148
+ const Layout = ({ children }: { children: React.ReactNode }) => (
149
+ <>
150
+ <KetchConsentScript />
151
+ <Wrapper>{children}</Wrapper>
152
+ </>
153
+ )
154
+ ```
155
+
156
+ The root layout also wraps admin routes like `/studio`, and a CMP has no
157
+ business loading there: nothing on those routes tracks anyone, the banner
158
+ covers the tooling, and the floating button lands on top of the Studio UI.
159
+ Mounting one level down keeps Ketch on the pages that actually need it.
160
+
161
+ Two consequences worth knowing, neither of them a problem:
162
+
163
+ - `script.tsx` imports `ketch-experience.css`, so the styles ride along with
164
+ the component and only ship on the routes that render it. That is what you
165
+ want — it also means the floating-button override is scoped the same way.
166
+ - Ketch does not load on the 404, which falls back to the root layout. Fine as
167
+ long as nothing tracks there either. If you later mount a tracker in the root
168
+ layout, move this back up and accept it on `/studio`.
169
+
170
+ Three traps, all verified on Next 16.3 + React 19:
171
+
172
+ - **Do not use `next/script` with `strategy="beforeInteractive"`.** It throws
173
+ *"Encountered a script tag while rendering React component"* and emits no
174
+ server-side `<script>`. A plain `<script async src>` is hoisted into `<head>`
175
+ by React 19 during SSR, which is what a CMP needs.
176
+ - **Do not render it as a sibling of `<body>`.** A `<script>` as a direct child
177
+ of `<html>` is invalid HTML and produces a cascade of hydration errors. If
178
+ you do mount it in the root layout, it goes *inside* `<body>`.
179
+ - **Do not mount it in a layout that some templates skip.** Consent has to be
180
+ askable on every page a visitor can land on. One layout above the footer, not
181
+ per-page.
182
+
183
+ When `ketchBootUrl` is `null` the tag is not rendered and every consent check
184
+ returns `false` — the site degrades to "no tracking", never to "tracking
185
+ without consent".
186
+
187
+ ## 4. Decide the UI — do not assume
188
+
189
+ Ketch ships a banner, a preferences modal and a floating button, all in **light
190
+ DOM**, so the project can keep, restyle, reposition or hide any of it. There is
191
+ no single correct answer here — **ask, or follow the design.** Do not copy
192
+ another project's choice.
193
+
194
+ Two decisions:
195
+
196
+ **a. The floating button.** Ketch mounts it into `<div id="lanyard_fab_button">`
197
+ as `body.firstChild` whenever the property config has an `accessibilityButton`.
198
+
199
+ | Option | When |
200
+ | --- | --- |
201
+ | Keep it as configured | No design opinion yet; fastest path |
202
+ | Reposition / restyle it | It collides with a chat widget or sticky CTA |
203
+ | Hide it | The design forbids persistent floating UI (common — it covers the layout on every page and is worst on mobile) |
204
+
205
+ **b. Where preferences are reachable from.** Consent must stay reopenable after
206
+ the banner is gone. The floating button, a footer link, a nav item, a
207
+ privacy-page link, or several — all valid. If you hide the floating button you
208
+ **must** provide another entry point, and it has to be on something every
209
+ template renders.
210
+
211
+ Whatever the project picks, route the call through the integration's helper
212
+ (`openPreferences()`), not `window.ketch` directly.
213
+
214
+ Mechanisms for all of the above — the exact element IDs, why hiding the button
215
+ needs `!important`, the available `ketch(…)` commands — are in
216
+ [`references/ui-customization.md`](references/ui-customization.md).
217
+
218
+ ## 5. Styling
219
+
220
+ Ketch's UI is styleable three ways, and they compose. Choose by how much design
221
+ control is needed and whether you have dashboard access:
222
+
223
+ | Approach | Trade-off |
224
+ | --- | --- |
225
+ | **Dashboard theme** | Survives code changes, no CSS to maintain — but hexes are duplicated from the design system and drift |
226
+ | **`--k-*` variable overrides** | Token-driven, so the design system stays the source of truth and Ketch follows automatically — but the names are Ketch-internal |
227
+ | **Direct CSS on Ketch's element IDs** | Total control, needed for layout and responsive work — but couples to Ketch's DOM |
228
+
229
+ Default recommendation for a basement project: **variables for colour and type,
230
+ direct CSS only for structure.** That keeps the banner following the design
231
+ tokens with no maintenance, e.g.:
232
+
233
+ ```css
234
+ html:root {
235
+ --k-banner-container-background-color: var(--color-primary);
236
+ --k-banner-header-title-color: var(--color-secondary);
237
+ --k-banner-description-link-color: var(--color-contrast);
238
+ }
239
+ ```
240
+
241
+ Scope overrides to `html:root`, **not** `:root` — Ketch injects its own `:root`
242
+ block into `<head>` at runtime, after the project stylesheet.
243
+
244
+ If there is no design yet, wire it to whatever tokens exist rather than
245
+ hardcoding hexes; the UI then updates on its own when real tokens land. Say so
246
+ explicitly instead of inventing a visual direction.
247
+
248
+ Variable naming, how to enumerate what a property exposes, the `!important`
249
+ rules, mobile and safe-area handling, and the case for replacing the UI entirely:
250
+ [`references/ui-customization.md`](references/ui-customization.md).
251
+
252
+ ## 6. Verify
253
+
254
+ ```bash
255
+ bun run typecheck && bun run lint && bun run knip && bun run build
256
+ ```
257
+
258
+ Then in the browser:
259
+
260
+ 1. Load a page that renders your chosen entry point. **In a Sanity
261
+ page-builder project `/` is usually a 404** — no page document exists for
262
+ it — and the 404 falls back to the root layout, so it has no chrome and no
263
+ Ketch. Testing there shows no banner and no button and looks broken. Use a
264
+ real slug.
265
+ 2. The tag is in `<head>`:
266
+ `curl -s <url> | grep -o '<script async[^>]*ketchcdn[^>]*>'`
267
+ If it only appears escaped inside the RSC payload (`\"src\":\"https://…`)
268
+ and not as a real tag, you are looking at a 404, not a page.
269
+ 3. `window.semaphore.loaded === true` in the console.
270
+ 4. The banner shows on first visit, and the chosen entry point reopens the
271
+ preferences modal.
272
+ 5. The floating button is in the state you intended — kept, moved or hidden —
273
+ both while the banner is open and after it's dismissed.
274
+ 6. Check at <640px too; a widget that looks fine on desktop can cover the
275
+ layout on mobile.
276
+ 7. Choose something, reload, confirm it persisted.
277
+
278
+ ## 7. Region behaviour — confirm and document it
279
+
280
+ Ketch resolves a jurisdiction from the visitor's IP and applies that
281
+ jurisdiction's legal basis per purpose. This is dashboard configuration, not
282
+ code, so **read it rather than assuming**:
283
+
284
+ ```bash
285
+ for j in gdpr US_California US_Texas Canada Quebec default; do
286
+ echo "== $j"
287
+ curl -s https://global.ketchcdn.com/web/v3/config/<org>/<property>/production/$j/en/config.json \
288
+ | jq -r '[.regulations[0]] + [.purposes[] | "\(.code)=\(.legalBasisCode)"] | join(" ")'
289
+ done
290
+ ```
291
+
292
+ Typical result: EEA/UK/CH, California and Quebec are **opt-in**
293
+ (`consent_optin` — off until accepted); Canada and most US state-privacy-law
294
+ states are **opt-out** (`consent_optout` — on until refused); everywhere else
295
+ falls to `default` (`disclosure`, notice only). The full jurisdiction → region
296
+ map is in the property's `boot.js` under `jurisdiction.scopes`.
297
+
298
+ Put the resulting table in the integration's `README.md` — "what an EU visitor
299
+ sees vs a US visitor" is a question that gets asked repeatedly.
300
+
301
+ Note that because the helper defaults to denied until Ketch confirms, the
302
+ project's own trackers stay off in opt-out regions too until the record
303
+ arrives. That is stricter than required and intentional.
304
+
305
+ **Opt-out and notice-only jurisdictions won't show Accept/Reject, and the
306
+ banner won't come back on reload** once Ketch auto-grants that jurisdiction's
307
+ default record — that's the legal basis, not a bug. To actually see and test
308
+ the opt-in banner, force a jurisdiction that has one — append
309
+ `?ketch_jurisdiction=gdpr` (or `US_Texas`, …) to the URL, no code change
310
+ needed. Full mechanics, including a code-level override for previews, are in
311
+ `references/ui-customization.md`.
312
+
313
+ ## 8. Document it
314
+
315
+ Document it wherever the project already documents integrations — a section in
316
+ the root `README.md`, or a `lib/integrations/ketch/README.md` if the project uses
317
+ per-integration READMEs. Ask rather than adding a file nobody wanted.
318
+
319
+ Cover: what Ketch is, who owns the account, where the keys live, **how another
320
+ integration checks consent before firing**, the region-by-region behaviour, and
321
+ open dashboard items.
322
+
323
+ ## Next
324
+
325
+ Gating an actual tracker (GTM, PostHog, Claydar, …) is the
326
+ `ketch-consent-check` skill. Do not wire trackers here.
@@ -0,0 +1,329 @@
1
+ # Controlling Ketch's UI
2
+
3
+ Ketch ships its own banner, modal, preferences center and floating button. This
4
+ file documents the surface it exposes so a project can keep, restyle, reposition
5
+ or replace any of it. **None of these choices is "the" right one** — pick per
6
+ project and per design.
7
+
8
+ ## What Ketch actually renders
9
+
10
+ All of it is **light DOM — there is no shadow root**, so ordinary CSS reaches it.
11
+
12
+ | Element | What it is |
13
+ | --- | --- |
14
+ | `<div id="lanyard_root">` | Host for every experience: banner, modal, preferences, consent gate |
15
+ | `<div id="lanyard_fab_button">` | The floating button. Inserted as **`body.firstChild`** |
16
+ | `#ketch-banner` | The banner container |
17
+ | `#ketch-banner-content` | Banner copy area |
18
+ | `#ketch-banner-buttons-container-standard` / `-compact` | Banner button rows (which one depends on the configured size) |
19
+ | `#ketch-banner-button-*` | Individual banner buttons |
20
+ | `#ketch-modal` | The "customize settings" modal |
21
+ | `#ketch-preferences` | The preferences center |
22
+ | `#ketch-preferences-purposes-tab`, `-subscriptions-tab`, `-requests-tab`, … | Preference center tabs |
23
+
24
+ At runtime Ketch also appends a `<style>` to `<head>` containing
25
+ `:root { --k-…: …; }` with its whole resolved theme. **That stylesheet lands
26
+ after the project's CSS** — the single most important fact for overriding it.
27
+
28
+ The floating button is only rendered when both are true:
29
+
30
+ - the property config has an `accessibilityButton` object, and
31
+ - experiences aren't disabled.
32
+
33
+ So removing `accessibilityButton` in the Ketch dashboard is the only way to stop
34
+ it being created at all. Everything below is client-side.
35
+
36
+ ## Choice 1 — the floating button
37
+
38
+ ### Keep it (Ketch's default)
39
+
40
+ Do nothing. Position and colour come from `accessibilityButton` in the
41
+ dashboard (`position: "bottom-left" | "bottom-right" | …`, `color`). Change it
42
+ there, no code.
43
+
44
+ ### Reposition or restyle it
45
+
46
+ ```css
47
+ #lanyard_fab_button {
48
+ inset: auto 1.5rem 1.5rem auto;
49
+ z-index: 40;
50
+ }
51
+ ```
52
+
53
+ Useful when the dashboard's corner collides with a chat widget or a sticky CTA.
54
+
55
+ ### Hide it
56
+
57
+ ```css
58
+ #lanyard_fab_button {
59
+ display: none !important;
60
+ }
61
+ ```
62
+
63
+ `!important` is **required**: Ketch toggles this element's inline
64
+ `style.display` when experiences open and close, and an inline style beats any
65
+ plain rule. Without it the button reappears after the banner is dismissed.
66
+
67
+ If the project's linter forbids `!important`, scope an override rather than
68
+ dropping it — the rule doesn't work without it:
69
+
70
+ ```jsonc
71
+ // biome.json → overrides
72
+ {
73
+ "includes": ["lib/integrations/ketch/ketch-experience.css"],
74
+ "linter": { "rules": { "complexity": { "noImportantStyles": "off" } } }
75
+ }
76
+ ```
77
+
78
+ Hiding it in CSS is a patch: Ketch still mounts the element on every page.
79
+ Removing `accessibilityButton` from the dashboard is the durable fix. Do both
80
+ when you have dashboard access; flag it as a follow-up when you don't.
81
+
82
+ ## Choice 2 — where preferences are reachable from
83
+
84
+ Consent has to stay reopenable after the banner is gone. Any of these works:
85
+
86
+ | Option | How |
87
+ | --- | --- |
88
+ | Ketch's floating button | Default, nothing to build |
89
+ | A footer link | Render a button that calls `openPreferences()` |
90
+ | A nav / settings menu item | Same call, different mount point |
91
+ | A privacy-policy page link | Same call |
92
+ | Several at once | Fine — the call is idempotent |
93
+
94
+ The integration's `openPreferences()` helper opens the preferences center as a
95
+ **modal** over the current page, not a full-page navigation:
96
+
97
+ ```ts
98
+ window.ketch("showConsent", { displayHint: "experiences.consent.modal" })
99
+ ```
100
+
101
+ Prefer routing it through the integration's helper (`openPreferences()` from
102
+ `useConsent()`) so nothing else touches `window.ketch`.
103
+
104
+ It is safe to call before the SDK has loaded — it queues on `window.semaphore`
105
+ and dispatches after init — so a control doesn't need to wait for readiness or
106
+ be disabled on first paint.
107
+
108
+ Related commands:
109
+
110
+ | Command | Effect |
111
+ | --- | --- |
112
+ | `ketch("showConsent", { displayHint: "experiences.consent.modal" })` | Open preferences as a modal — what `openPreferences()` calls |
113
+ | `ketch("showPreferences")` | Open the full-page preference experience instead of a modal |
114
+ | `ketch("showConsent")` | Re-show the consent banner |
115
+ | `ketch("showPreferences", { tab: "purposes" })` | Open the full-page experience on a specific tab |
116
+
117
+ Whichever mount point you choose, put it somewhere **every template renders** —
118
+ usually the footer or a persistent nav — and verify it on a route that isn't the
119
+ homepage.
120
+
121
+ ## Choice 3 — styling
122
+
123
+ Three approaches. They compose; pick by how much design control the project
124
+ needs and whether you have dashboard access.
125
+
126
+ ### A. Theme it in the Ketch dashboard
127
+
128
+ Set colours, radii and fonts in the property's theme editor.
129
+
130
+ - **For:** survives any code change, no CSS to maintain, works for teams
131
+ without frontend access.
132
+ - **Against:** hex values duplicated from the design system, so it drifts when
133
+ tokens change; needs dashboard access; can't reference CSS variables.
134
+
135
+ Best when the site's palette is stable and nobody wants Ketch CSS in the repo.
136
+
137
+ ### B. Override the `--k-*` variables (token-driven)
138
+
139
+ Ketch's components consume their theme through CSS custom properties, so
140
+ redefining them restyles the UI without touching structure:
141
+
142
+ ```css
143
+ html:root {
144
+ --k-banner-container-background-color: var(--color-primary);
145
+ --k-banner-header-title-color: var(--color-secondary);
146
+ --k-banner-description-link-color: var(--color-contrast);
147
+ --k-banner-buttons-primary-background-color: var(--color-secondary);
148
+ --k-banner-buttons-primary-text-color: var(--color-primary);
149
+ }
150
+ ```
151
+
152
+ - **For:** the design system stays the single source of truth — when tokens
153
+ change, Ketch follows automatically, no edits.
154
+ - **Against:** variable names are Ketch-internal and could change across SDK
155
+ versions; only covers what the theme models.
156
+
157
+ **Use `html:root`, not `:root`.** Ketch injects its own `:root` block into
158
+ `<head>` at runtime, so it comes after the project stylesheet and wins on equal
159
+ specificity. `html:root` is one step higher and wins regardless of order.
160
+ `:root { … !important }` also works; `html:root` is cleaner.
161
+
162
+ #### Deriving the variable names
163
+
164
+ The rule is mechanical: **take the theme value's dot path and replace dots with
165
+ dashes, prefixed `--k-`.**
166
+
167
+ ```
168
+ banner.container.background.color → --k-banner-container-background-color
169
+ modal.purposeList.switchButtons.on.background.color
170
+ → --k-modal-purposeList-switchButtons-on-background-color
171
+ ```
172
+
173
+ Casing is preserved, so `purposeList` stays camelCase. Enumerate what's
174
+ available for the property:
175
+
176
+ ```bash
177
+ curl -s https://global.ketchcdn.com/web/v3/config/<org>/<property>/production/default/en/config.json \
178
+ | jq '.theme.banner, .theme.modal, .theme.preference'
179
+ ```
180
+
181
+ Watch out for non-colour values. Radii are sometimes stored unitless (`"8"`),
182
+ which is invalid in `border-radius: var(…)` — set those explicitly with a unit
183
+ (`8px`) or handle them with direct CSS instead.
184
+
185
+ ### C. Direct CSS on the element IDs
186
+
187
+ For anything the theme doesn't model — layout, stacking, spacing, responsive
188
+ behaviour, fonts:
189
+
190
+ ```css
191
+ #lanyard_root,
192
+ #lanyard_root * {
193
+ font-family: var(--font-sans), system-ui, sans-serif !important;
194
+ }
195
+
196
+ @media (max-width: 640px) {
197
+ #ketch-banner {
198
+ inset-inline: 0 !important;
199
+ width: 100% !important;
200
+ border-radius: 0 !important;
201
+ padding-bottom: max(1rem, var(--safe-area-inset-bottom, 0px)) !important;
202
+ }
203
+
204
+ #ketch-banner-buttons-container-standard {
205
+ flex-direction: column !important;
206
+ width: 100% !important;
207
+ }
208
+ }
209
+ ```
210
+
211
+ `!important` is usually needed here because Ketch's internals are Tailwind
212
+ classes with an `!` important prefix reading the variables (e.g.
213
+ `!ketch-bg-[--k-banner-container-background-color]`).
214
+
215
+ Ketch exposes safe-area values on its root for notched devices:
216
+ `--safe-area-inset-top` / `-right` / `-bottom` / `-left`.
217
+
218
+ - **For:** total control, reaches everything.
219
+ - **Against:** couples to Ketch's DOM, so it's the first thing to break on an
220
+ SDK upgrade. Prefer B for colours and keep C for structure.
221
+
222
+ ### D. Replace the UI entirely
223
+
224
+ Possible — suppress Ketch's experiences and drive consent with `setConsent` from
225
+ a custom component. **Think hard before doing this.** You take on per-jurisdiction
226
+ copy, GPC and ATT signal banners, vendor lists, TCF strings and the audit trail,
227
+ which is the bulk of what the CMP is paid for. Only justified when design
228
+ requirements genuinely cannot be met by A–C, and worth confirming with whoever
229
+ owns compliance.
230
+
231
+ ## Verifying UI changes
232
+
233
+ CSS against a third-party widget can't be verified from a build — check in a
234
+ browser:
235
+
236
+ 1. Clear site data so the banner shows again (or use `ketch("showConsent")`).
237
+ 2. Banner: colours, fonts, button styles, and the link to the privacy policy.
238
+ 3. Open "customize settings": purpose toggles, accept/reject buttons, scrolling.
239
+ 4. Dismiss the banner and confirm the floating button is in the state you
240
+ intended — this is where a missing `!important` shows up.
241
+ 5. Mobile viewport, and a notched device if safe-area padding matters.
242
+ 6. Keyboard: tab through the banner and modal, confirm focus is visible and
243
+ trapped in the modal.
244
+
245
+ ## Testing a different jurisdiction
246
+
247
+ The banner and preferences center render completely differently per
248
+ jurisdiction, so "the toggles are locked" or "Accept All does nothing" is
249
+ usually **not a bug** — it's the visitor resolving to a jurisdiction where every
250
+ purpose is `disclosure` (notice-only). Ketch then shows each purpose as
251
+ **"Always Active"** with the toggle disabled, and the accept/reject buttons have
252
+ nothing to change. Check the legal basis before debugging the UI:
253
+
254
+ ```bash
255
+ curl -s https://global.ketchcdn.com/web/v3/config/<org>/<property>/production/default/en/config.json \
256
+ | jq -r '.purposes[] | "\(.code) \(.legalBasisCode)"'
257
+ ```
258
+
259
+ Force a jurisdiction instead of relying on geo-IP by setting an attribute on the
260
+ root element:
261
+
262
+ ```html
263
+ <html jurisdiction="gdpr">
264
+ ```
265
+
266
+ For a quick manual check with no code change, Ketch also accepts a URL query
267
+ parameter that sets the same thing:
268
+
269
+ ```
270
+ https://example.com/?ketch_jurisdiction=gdpr
271
+ ```
272
+
273
+ Verified against a real property: LatAm traffic normally resolves to
274
+ `default` (notice-only), where consent is granted automatically and the
275
+ banner won't re-show on reload — see the gotcha below.
276
+ `?ketch_jurisdiction=gdpr` (or `US_Texas`, `US_California`, …) forces the
277
+ banner to render for that jurisdiction instead, with real Accept/Reject
278
+ buttons — no VPN needed. Use this to actually see the opt-in UI from a
279
+ region that defaults to notice-only.
280
+
281
+ In Next.js, drive it from a gitignored env var so it never ships:
282
+
283
+ ```tsx
284
+ <html
285
+ lang="en"
286
+ {...(forcedJurisdiction
287
+ ? ({ jurisdiction: forcedJurisdiction } as Record<string, string>)
288
+ : {})}
289
+ >
290
+ ```
291
+
292
+ Ketch's resolution order, highest priority first:
293
+
294
+ 1. An override in Ketch's own storage
295
+ 2. `config.jurisdiction.code`
296
+ 3. **`document.documentElement.getAttribute("jurisdiction")`**
297
+ 4. A `dataLayer` variable, if the property configures one
298
+ 5. The region → jurisdiction map (`jurisdiction.scopes` in `boot.js`)
299
+ 6. `defaultJurisdictionCode`
300
+
301
+ Because the attribute is checked before the region map, it works from anywhere
302
+ with no VPN. Try `gdpr` or `US_California` for opt-in UI with live toggles,
303
+ `US_Texas` for opt-out, `default` for notice-only.
304
+
305
+ Countries absent from `jurisdiction.scopes` fall through to `default`. If the
306
+ site has real traffic from a country that isn't mapped, that's a dashboard
307
+ decision for whoever owns compliance — surface it rather than working around it.
308
+
309
+ ## Gotchas
310
+
311
+ - **Opt-out and notice-only jurisdictions don't offer Reject, and the banner
312
+ won't re-show.** Once Ketch auto-grants a jurisdiction's default record,
313
+ that resolves `isResolved` the same way an explicit accept would — no
314
+ reload brings the banner back, and there's no Reject All to click if you
315
+ were expecting one. This is the jurisdiction's legal basis, not a bug in
316
+ the integration or a broken build. Force a different jurisdiction (above)
317
+ before assuming something's wrong.
318
+ - **`options.appDivs`.** The property config can list element IDs that Ketch
319
+ gates on consent (commonly a chat widget container). If a third-party widget
320
+ mysteriously doesn't appear, check whether it's listed there.
321
+ - **Ketch normalises margins/padding** inside `#lanyard_root` with its own
322
+ injected stylesheet. Reset-style rules of your own may be redundant or fight
323
+ it.
324
+ - **Ketch's stylesheet is added on first experience render**, not at boot. A
325
+ rule that works on the banner may not have applied yet when you inspect
326
+ before it opens.
327
+ - **Custom CSS in the dashboard.** The property supports a `customCss` field
328
+ injected at runtime. If overrides behave unpredictably, check whether
329
+ something is already set there before adding more CSS in the repo.