@tickboxhq/banner-default 0.0.12

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tiny Systems Limited
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # @tickboxhq/banner-default
2
+
3
+ A styled consent banner and notice card for Tickbox. Reach for this when you don't want to design and build your own.
4
+
5
+ If you do want your own, `@tickboxhq/react` and `@tickboxhq/vue` already export headless `<ConsentBanner>` and `<ConsentNotice>` render-prop components. This package just adds visuals on top.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @tickboxhq/banner-default
11
+ ```
12
+
13
+ You'll also need the framework adapter (which you probably already have):
14
+
15
+ ```bash
16
+ # React
17
+ pnpm add @tickboxhq/core @tickboxhq/react
18
+
19
+ # Vue / Nuxt
20
+ pnpm add @tickboxhq/core @tickboxhq/vue
21
+ ```
22
+
23
+ ## React
24
+
25
+ ```tsx
26
+ import { ConsentProvider } from '@tickboxhq/react'
27
+ import { ConsentBannerDefault } from '@tickboxhq/banner-default/react'
28
+ import config from './consent.config'
29
+
30
+ export default function App() {
31
+ return (
32
+ <ConsentProvider config={config}>
33
+ {/* your app */}
34
+ <ConsentBannerDefault policyUrl={config.policy?.url} />
35
+ </ConsentProvider>
36
+ )
37
+ }
38
+ ```
39
+
40
+ For sites with only `notice`-mode categories (UK DUAA-exempt analytics like Plausible or GoatCounter):
41
+
42
+ ```tsx
43
+ import { ConsentNoticeDefault } from '@tickboxhq/banner-default/react'
44
+
45
+ <ConsentNoticeDefault policyUrl="/privacy" />
46
+ ```
47
+
48
+ ## Vue / Nuxt
49
+
50
+ ```vue
51
+ <script setup lang="ts">
52
+ import { ConsentBannerDefault } from '@tickboxhq/banner-default/vue'
53
+ import config from './consent.config'
54
+ </script>
55
+
56
+ <template>
57
+ <ConsentBannerDefault :policy-url="config.policy?.url" />
58
+ </template>
59
+ ```
60
+
61
+ For notice-only sites:
62
+
63
+ ```vue
64
+ <script setup lang="ts">
65
+ import { ConsentNoticeDefault } from '@tickboxhq/banner-default/vue'
66
+ </script>
67
+
68
+ <template>
69
+ <ConsentNoticeDefault policy-url="/privacy" />
70
+ </template>
71
+ ```
72
+
73
+ ## Props
74
+
75
+ Both components accept the same shape:
76
+
77
+ | Prop | Type | Default | Notes |
78
+ |---|---|---|---|
79
+ | `policyUrl` | `string` | — | Privacy policy URL. Hidden if omitted. |
80
+ | `theme` | `'light' \| 'dark'` | follows `prefers-color-scheme` | Force a theme. |
81
+ | `copy` | `Partial<BannerCopy>` / `Partial<NoticeCopy>` | English defaults | Override individual labels. |
82
+
83
+ `<ConsentNoticeDefault>` also takes `optOutCategoryId` (default `'analytics'`) — the category to deny when the user clicks "Opt out".
84
+
85
+ ## Styling
86
+
87
+ The components inject a single `<style>` tag, once per page. Themes are CSS custom properties, so you can re-skin without forking:
88
+
89
+ ```css
90
+ .tb-root {
91
+ --tb-radius: 12px;
92
+ --tb-primary-bg: #6366f1;
93
+ --tb-primary-fg: #fff;
94
+ }
95
+ ```
96
+
97
+ The full set: `--tb-bg`, `--tb-fg`, `--tb-fg-muted`, `--tb-border`, `--tb-shadow`, `--tb-primary-bg`, `--tb-primary-fg`, `--tb-secondary-bg`, `--tb-secondary-fg`, `--tb-link`, `--tb-radius`, `--tb-z`.
98
+
99
+ Light/dark follows `prefers-color-scheme`. Pass `theme="light"` or `theme="dark"` to override.
100
+
101
+ ## Banner vs notice
102
+
103
+ Use the banner when you have any `consent`-mode categories — most EU sites. It's a bottom bar with Accept all / Reject all / Customise, and Customise opens a modal with per-category toggles.
104
+
105
+ Use the notice when you only have `notice`-mode categories — typical for UK sites running just DUAA-exempt analytics. It's a small bottom-right card with Got it / Opt out.
106
+
107
+ If your site has both kinds of categories, use the banner. The customise modal lists notice-mode categories too, so the notice card stays out of the way.
108
+
109
+ ## Licence
110
+
111
+ MIT
@@ -0,0 +1,356 @@
1
+ // src/shared/copy.ts
2
+ var DEFAULT_BANNER_COPY = {
3
+ title: "Cookies and tracking",
4
+ description: "We use cookies to make this site work and, with your consent, to measure usage. You can choose what to allow.",
5
+ acceptLabel: "Accept all",
6
+ rejectLabel: "Reject all",
7
+ customiseLabel: "Customise",
8
+ saveLabel: "Save preferences",
9
+ closeLabel: "Close",
10
+ policyLinkLabel: "Privacy policy",
11
+ requiredBadge: "Required"
12
+ };
13
+ var DEFAULT_NOTICE_COPY = {
14
+ title: "A note about analytics",
15
+ description: "We use privacy-friendly analytics to understand how this site is used. No personal data is collected and no advertising profiles are built.",
16
+ acknowledgeLabel: "Got it",
17
+ optOutLabel: "Opt out",
18
+ policyLinkLabel: "Privacy policy"
19
+ };
20
+
21
+ // src/shared/styles.ts
22
+ var TICKBOX_STYLES = `
23
+ :where(.tb-root) {
24
+ --tb-bg: #ffffff;
25
+ --tb-fg: #1f2328;
26
+ --tb-fg-muted: #59636e;
27
+ --tb-border: #d1d9e0;
28
+ --tb-shadow: 0 8px 24px rgba(140, 149, 159, 0.2);
29
+ --tb-primary-bg: #1f2328;
30
+ --tb-primary-fg: #ffffff;
31
+ --tb-primary-bg-hover: #000000;
32
+ --tb-secondary-bg: #ffffff;
33
+ --tb-secondary-fg: #1f2328;
34
+ --tb-secondary-bg-hover: #f6f8fa;
35
+ --tb-link: #0969da;
36
+ --tb-radius: 6px;
37
+ --tb-z: 2147483000;
38
+ font-family:
39
+ -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica,
40
+ Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
41
+ color: var(--tb-fg);
42
+ font-size: 14px;
43
+ line-height: 1.5;
44
+ -webkit-font-smoothing: antialiased;
45
+ }
46
+ @media (prefers-color-scheme: dark) {
47
+ :where(.tb-root:not([data-tb-theme="light"])) {
48
+ --tb-bg: #0d1117;
49
+ --tb-fg: #f0f6fc;
50
+ --tb-fg-muted: #9198a1;
51
+ --tb-border: #30363d;
52
+ --tb-shadow: 0 8px 24px rgba(1, 4, 9, 0.85);
53
+ --tb-primary-bg: #f0f6fc;
54
+ --tb-primary-fg: #0d1117;
55
+ --tb-primary-bg-hover: #ffffff;
56
+ --tb-secondary-bg: #15191f;
57
+ --tb-secondary-fg: #f0f6fc;
58
+ --tb-secondary-bg-hover: #1f2328;
59
+ --tb-link: #4493f8;
60
+ }
61
+ }
62
+ :where(.tb-root[data-tb-theme="dark"]) {
63
+ --tb-bg: #0d1117;
64
+ --tb-fg: #f0f6fc;
65
+ --tb-fg-muted: #9198a1;
66
+ --tb-border: #30363d;
67
+ --tb-shadow: 0 8px 24px rgba(1, 4, 9, 0.85);
68
+ --tb-primary-bg: #f0f6fc;
69
+ --tb-primary-fg: #0d1117;
70
+ --tb-primary-bg-hover: #ffffff;
71
+ --tb-secondary-bg: #15191f;
72
+ --tb-secondary-fg: #f0f6fc;
73
+ --tb-secondary-bg-hover: #1f2328;
74
+ --tb-link: #4493f8;
75
+ }
76
+
77
+ .tb-banner {
78
+ position: fixed;
79
+ left: 16px;
80
+ right: 16px;
81
+ bottom: 16px;
82
+ z-index: var(--tb-z);
83
+ background: var(--tb-bg);
84
+ color: var(--tb-fg);
85
+ border: 1px solid var(--tb-border);
86
+ border-radius: var(--tb-radius);
87
+ box-shadow: var(--tb-shadow);
88
+ padding: 16px 20px;
89
+ display: flex;
90
+ flex-wrap: wrap;
91
+ align-items: center;
92
+ justify-content: space-between;
93
+ gap: 16px;
94
+ animation: tb-fade-in 160ms ease-out;
95
+ }
96
+ .tb-banner-text {
97
+ flex: 1 1 320px;
98
+ min-width: 0;
99
+ }
100
+ .tb-banner-title {
101
+ font-weight: 600;
102
+ margin: 0 0 2px;
103
+ font-size: 14px;
104
+ }
105
+ .tb-banner-desc {
106
+ margin: 0;
107
+ color: var(--tb-fg-muted);
108
+ }
109
+ .tb-banner-actions {
110
+ display: flex;
111
+ gap: 8px;
112
+ flex-wrap: wrap;
113
+ }
114
+
115
+ .tb-notice {
116
+ position: fixed;
117
+ right: 16px;
118
+ bottom: 16px;
119
+ z-index: var(--tb-z);
120
+ background: var(--tb-bg);
121
+ color: var(--tb-fg);
122
+ border: 1px solid var(--tb-border);
123
+ border-radius: var(--tb-radius);
124
+ box-shadow: var(--tb-shadow);
125
+ padding: 14px 16px;
126
+ max-width: 360px;
127
+ animation: tb-fade-in 160ms ease-out;
128
+ }
129
+ .tb-notice-title {
130
+ font-weight: 600;
131
+ margin: 0 0 4px;
132
+ font-size: 14px;
133
+ }
134
+ .tb-notice-desc {
135
+ margin: 0 0 10px;
136
+ color: var(--tb-fg-muted);
137
+ font-size: 13px;
138
+ }
139
+ .tb-notice-actions {
140
+ display: flex;
141
+ gap: 8px;
142
+ align-items: center;
143
+ justify-content: flex-end;
144
+ flex-wrap: wrap;
145
+ }
146
+
147
+ .tb-link {
148
+ color: var(--tb-link);
149
+ text-decoration: none;
150
+ font-size: 13px;
151
+ margin-right: auto;
152
+ }
153
+ .tb-link:hover { text-decoration: underline; }
154
+
155
+ .tb-btn {
156
+ appearance: none;
157
+ border: 1px solid transparent;
158
+ border-radius: var(--tb-radius);
159
+ padding: 6px 14px;
160
+ font-size: 13px;
161
+ font-weight: 500;
162
+ font-family: inherit;
163
+ cursor: pointer;
164
+ line-height: 1.5;
165
+ transition: background-color 80ms ease;
166
+ white-space: nowrap;
167
+ }
168
+ .tb-btn:focus-visible {
169
+ outline: 2px solid var(--tb-link);
170
+ outline-offset: 2px;
171
+ }
172
+ .tb-btn-primary {
173
+ background: var(--tb-primary-bg);
174
+ color: var(--tb-primary-fg);
175
+ }
176
+ .tb-btn-primary:hover { background: var(--tb-primary-bg-hover); }
177
+ .tb-btn-secondary {
178
+ background: var(--tb-secondary-bg);
179
+ color: var(--tb-secondary-fg);
180
+ border-color: var(--tb-border);
181
+ }
182
+ .tb-btn-secondary:hover { background: var(--tb-secondary-bg-hover); }
183
+ .tb-btn-ghost {
184
+ background: transparent;
185
+ color: var(--tb-fg-muted);
186
+ padding: 6px 10px;
187
+ }
188
+ .tb-btn-ghost:hover { color: var(--tb-fg); }
189
+
190
+ .tb-modal-backdrop {
191
+ position: fixed;
192
+ inset: 0;
193
+ background: rgba(15, 18, 24, 0.5);
194
+ z-index: var(--tb-z);
195
+ display: flex;
196
+ align-items: center;
197
+ justify-content: center;
198
+ padding: 20px;
199
+ animation: tb-fade-in 160ms ease-out;
200
+ }
201
+ .tb-modal {
202
+ background: var(--tb-bg);
203
+ color: var(--tb-fg);
204
+ border: 1px solid var(--tb-border);
205
+ border-radius: var(--tb-radius);
206
+ box-shadow: var(--tb-shadow);
207
+ width: 100%;
208
+ max-width: 520px;
209
+ max-height: 85vh;
210
+ display: flex;
211
+ flex-direction: column;
212
+ }
213
+ .tb-modal-head {
214
+ padding: 14px 16px;
215
+ border-bottom: 1px solid var(--tb-border);
216
+ display: flex;
217
+ align-items: center;
218
+ justify-content: space-between;
219
+ gap: 16px;
220
+ }
221
+ .tb-modal-title {
222
+ margin: 0;
223
+ font-size: 15px;
224
+ font-weight: 600;
225
+ }
226
+ .tb-modal-body {
227
+ padding: 12px 16px;
228
+ overflow-y: auto;
229
+ display: flex;
230
+ flex-direction: column;
231
+ gap: 12px;
232
+ }
233
+ .tb-modal-foot {
234
+ padding: 12px 16px;
235
+ border-top: 1px solid var(--tb-border);
236
+ display: flex;
237
+ gap: 8px;
238
+ justify-content: flex-end;
239
+ flex-wrap: wrap;
240
+ }
241
+
242
+ .tb-cat {
243
+ border: 1px solid var(--tb-border);
244
+ border-radius: var(--tb-radius);
245
+ padding: 12px;
246
+ display: flex;
247
+ gap: 12px;
248
+ align-items: flex-start;
249
+ }
250
+ .tb-cat-text { flex: 1; min-width: 0; }
251
+ .tb-cat-name {
252
+ font-weight: 600;
253
+ margin: 0 0 2px;
254
+ font-size: 13px;
255
+ display: flex;
256
+ align-items: center;
257
+ gap: 6px;
258
+ }
259
+ .tb-cat-desc {
260
+ margin: 0;
261
+ color: var(--tb-fg-muted);
262
+ font-size: 13px;
263
+ }
264
+ .tb-badge {
265
+ display: inline-block;
266
+ font-size: 11px;
267
+ font-weight: 500;
268
+ color: var(--tb-fg-muted);
269
+ background: var(--tb-secondary-bg-hover);
270
+ border: 1px solid var(--tb-border);
271
+ border-radius: 999px;
272
+ padding: 1px 8px;
273
+ }
274
+
275
+ .tb-switch {
276
+ position: relative;
277
+ display: inline-block;
278
+ width: 32px;
279
+ height: 18px;
280
+ flex-shrink: 0;
281
+ margin-top: 2px;
282
+ }
283
+ .tb-switch input {
284
+ opacity: 0;
285
+ width: 0;
286
+ height: 0;
287
+ position: absolute;
288
+ }
289
+ .tb-switch-track {
290
+ position: absolute;
291
+ inset: 0;
292
+ background: var(--tb-border);
293
+ border-radius: 999px;
294
+ transition: background-color 100ms ease;
295
+ cursor: pointer;
296
+ }
297
+ .tb-switch-thumb {
298
+ position: absolute;
299
+ top: 2px;
300
+ left: 2px;
301
+ width: 14px;
302
+ height: 14px;
303
+ background: var(--tb-bg);
304
+ border-radius: 50%;
305
+ transition: transform 100ms ease;
306
+ }
307
+ .tb-switch input:checked + .tb-switch-track {
308
+ background: var(--tb-primary-bg);
309
+ }
310
+ .tb-switch input:checked + .tb-switch-track .tb-switch-thumb {
311
+ transform: translateX(14px);
312
+ }
313
+ .tb-switch input:disabled + .tb-switch-track {
314
+ opacity: 0.5;
315
+ cursor: not-allowed;
316
+ }
317
+ .tb-switch input:focus-visible + .tb-switch-track {
318
+ outline: 2px solid var(--tb-link);
319
+ outline-offset: 2px;
320
+ }
321
+
322
+ @keyframes tb-fade-in {
323
+ from { opacity: 0; transform: translateY(4px); }
324
+ to { opacity: 1; transform: translateY(0); }
325
+ }
326
+
327
+ @media (max-width: 640px) {
328
+ .tb-banner {
329
+ flex-direction: column;
330
+ align-items: stretch;
331
+ }
332
+ .tb-banner-actions {
333
+ flex-direction: column;
334
+ }
335
+ .tb-banner-actions .tb-btn { width: 100%; }
336
+ }
337
+ `;
338
+ var STYLE_ID = "tickbox-default-styles";
339
+ var injected = false;
340
+ function injectStyles() {
341
+ if (injected) return;
342
+ if (typeof document === "undefined") return;
343
+ if (document.getElementById(STYLE_ID)) {
344
+ injected = true;
345
+ return;
346
+ }
347
+ const el = document.createElement("style");
348
+ el.id = STYLE_ID;
349
+ el.textContent = TICKBOX_STYLES;
350
+ document.head.appendChild(el);
351
+ injected = true;
352
+ }
353
+
354
+ export { DEFAULT_BANNER_COPY, DEFAULT_NOTICE_COPY, injectStyles };
355
+ //# sourceMappingURL=chunk-WJSFVAK4.js.map
356
+ //# sourceMappingURL=chunk-WJSFVAK4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/copy.ts","../src/shared/styles.ts"],"names":[],"mappings":"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","file":"chunk-WJSFVAK4.js","sourcesContent":["export type BannerCopy = {\n title: string\n description: string\n acceptLabel: string\n rejectLabel: string\n customiseLabel: string\n saveLabel: string\n closeLabel: string\n policyLinkLabel: string\n requiredBadge: string\n}\n\nexport type NoticeCopy = {\n title: string\n description: string\n acknowledgeLabel: string\n optOutLabel: string\n policyLinkLabel: string\n}\n\nexport const DEFAULT_BANNER_COPY: BannerCopy = {\n title: 'Cookies and tracking',\n description:\n 'We use cookies to make this site work and, with your consent, to measure usage. You can choose what to allow.',\n acceptLabel: 'Accept all',\n rejectLabel: 'Reject all',\n customiseLabel: 'Customise',\n saveLabel: 'Save preferences',\n closeLabel: 'Close',\n policyLinkLabel: 'Privacy policy',\n requiredBadge: 'Required',\n}\n\nexport const DEFAULT_NOTICE_COPY: NoticeCopy = {\n title: 'A note about analytics',\n description:\n 'We use privacy-friendly analytics to understand how this site is used. No personal data is collected and no advertising profiles are built.',\n acknowledgeLabel: 'Got it',\n optOutLabel: 'Opt out',\n policyLinkLabel: 'Privacy policy',\n}\n","/**\n * Inline CSS for the default banner / notice / modal components.\n *\n * Uses CSS custom properties so users can re-theme without forking. Light\n * and dark themes are wired through `prefers-color-scheme` and the\n * `[data-tb-theme]` attribute.\n *\n * Visual style: GitHub-ish — system font, 6px corners, subtle border + soft\n * shadow, equal-prominence accept/reject buttons.\n */\nexport const TICKBOX_STYLES = `\n:where(.tb-root) {\n --tb-bg: #ffffff;\n --tb-fg: #1f2328;\n --tb-fg-muted: #59636e;\n --tb-border: #d1d9e0;\n --tb-shadow: 0 8px 24px rgba(140, 149, 159, 0.2);\n --tb-primary-bg: #1f2328;\n --tb-primary-fg: #ffffff;\n --tb-primary-bg-hover: #000000;\n --tb-secondary-bg: #ffffff;\n --tb-secondary-fg: #1f2328;\n --tb-secondary-bg-hover: #f6f8fa;\n --tb-link: #0969da;\n --tb-radius: 6px;\n --tb-z: 2147483000;\n font-family:\n -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Noto Sans\", Helvetica,\n Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\";\n color: var(--tb-fg);\n font-size: 14px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n}\n@media (prefers-color-scheme: dark) {\n :where(.tb-root:not([data-tb-theme=\"light\"])) {\n --tb-bg: #0d1117;\n --tb-fg: #f0f6fc;\n --tb-fg-muted: #9198a1;\n --tb-border: #30363d;\n --tb-shadow: 0 8px 24px rgba(1, 4, 9, 0.85);\n --tb-primary-bg: #f0f6fc;\n --tb-primary-fg: #0d1117;\n --tb-primary-bg-hover: #ffffff;\n --tb-secondary-bg: #15191f;\n --tb-secondary-fg: #f0f6fc;\n --tb-secondary-bg-hover: #1f2328;\n --tb-link: #4493f8;\n }\n}\n:where(.tb-root[data-tb-theme=\"dark\"]) {\n --tb-bg: #0d1117;\n --tb-fg: #f0f6fc;\n --tb-fg-muted: #9198a1;\n --tb-border: #30363d;\n --tb-shadow: 0 8px 24px rgba(1, 4, 9, 0.85);\n --tb-primary-bg: #f0f6fc;\n --tb-primary-fg: #0d1117;\n --tb-primary-bg-hover: #ffffff;\n --tb-secondary-bg: #15191f;\n --tb-secondary-fg: #f0f6fc;\n --tb-secondary-bg-hover: #1f2328;\n --tb-link: #4493f8;\n}\n\n.tb-banner {\n position: fixed;\n left: 16px;\n right: 16px;\n bottom: 16px;\n z-index: var(--tb-z);\n background: var(--tb-bg);\n color: var(--tb-fg);\n border: 1px solid var(--tb-border);\n border-radius: var(--tb-radius);\n box-shadow: var(--tb-shadow);\n padding: 16px 20px;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n animation: tb-fade-in 160ms ease-out;\n}\n.tb-banner-text {\n flex: 1 1 320px;\n min-width: 0;\n}\n.tb-banner-title {\n font-weight: 600;\n margin: 0 0 2px;\n font-size: 14px;\n}\n.tb-banner-desc {\n margin: 0;\n color: var(--tb-fg-muted);\n}\n.tb-banner-actions {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.tb-notice {\n position: fixed;\n right: 16px;\n bottom: 16px;\n z-index: var(--tb-z);\n background: var(--tb-bg);\n color: var(--tb-fg);\n border: 1px solid var(--tb-border);\n border-radius: var(--tb-radius);\n box-shadow: var(--tb-shadow);\n padding: 14px 16px;\n max-width: 360px;\n animation: tb-fade-in 160ms ease-out;\n}\n.tb-notice-title {\n font-weight: 600;\n margin: 0 0 4px;\n font-size: 14px;\n}\n.tb-notice-desc {\n margin: 0 0 10px;\n color: var(--tb-fg-muted);\n font-size: 13px;\n}\n.tb-notice-actions {\n display: flex;\n gap: 8px;\n align-items: center;\n justify-content: flex-end;\n flex-wrap: wrap;\n}\n\n.tb-link {\n color: var(--tb-link);\n text-decoration: none;\n font-size: 13px;\n margin-right: auto;\n}\n.tb-link:hover { text-decoration: underline; }\n\n.tb-btn {\n appearance: none;\n border: 1px solid transparent;\n border-radius: var(--tb-radius);\n padding: 6px 14px;\n font-size: 13px;\n font-weight: 500;\n font-family: inherit;\n cursor: pointer;\n line-height: 1.5;\n transition: background-color 80ms ease;\n white-space: nowrap;\n}\n.tb-btn:focus-visible {\n outline: 2px solid var(--tb-link);\n outline-offset: 2px;\n}\n.tb-btn-primary {\n background: var(--tb-primary-bg);\n color: var(--tb-primary-fg);\n}\n.tb-btn-primary:hover { background: var(--tb-primary-bg-hover); }\n.tb-btn-secondary {\n background: var(--tb-secondary-bg);\n color: var(--tb-secondary-fg);\n border-color: var(--tb-border);\n}\n.tb-btn-secondary:hover { background: var(--tb-secondary-bg-hover); }\n.tb-btn-ghost {\n background: transparent;\n color: var(--tb-fg-muted);\n padding: 6px 10px;\n}\n.tb-btn-ghost:hover { color: var(--tb-fg); }\n\n.tb-modal-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(15, 18, 24, 0.5);\n z-index: var(--tb-z);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n animation: tb-fade-in 160ms ease-out;\n}\n.tb-modal {\n background: var(--tb-bg);\n color: var(--tb-fg);\n border: 1px solid var(--tb-border);\n border-radius: var(--tb-radius);\n box-shadow: var(--tb-shadow);\n width: 100%;\n max-width: 520px;\n max-height: 85vh;\n display: flex;\n flex-direction: column;\n}\n.tb-modal-head {\n padding: 14px 16px;\n border-bottom: 1px solid var(--tb-border);\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n}\n.tb-modal-title {\n margin: 0;\n font-size: 15px;\n font-weight: 600;\n}\n.tb-modal-body {\n padding: 12px 16px;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n.tb-modal-foot {\n padding: 12px 16px;\n border-top: 1px solid var(--tb-border);\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n flex-wrap: wrap;\n}\n\n.tb-cat {\n border: 1px solid var(--tb-border);\n border-radius: var(--tb-radius);\n padding: 12px;\n display: flex;\n gap: 12px;\n align-items: flex-start;\n}\n.tb-cat-text { flex: 1; min-width: 0; }\n.tb-cat-name {\n font-weight: 600;\n margin: 0 0 2px;\n font-size: 13px;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.tb-cat-desc {\n margin: 0;\n color: var(--tb-fg-muted);\n font-size: 13px;\n}\n.tb-badge {\n display: inline-block;\n font-size: 11px;\n font-weight: 500;\n color: var(--tb-fg-muted);\n background: var(--tb-secondary-bg-hover);\n border: 1px solid var(--tb-border);\n border-radius: 999px;\n padding: 1px 8px;\n}\n\n.tb-switch {\n position: relative;\n display: inline-block;\n width: 32px;\n height: 18px;\n flex-shrink: 0;\n margin-top: 2px;\n}\n.tb-switch input {\n opacity: 0;\n width: 0;\n height: 0;\n position: absolute;\n}\n.tb-switch-track {\n position: absolute;\n inset: 0;\n background: var(--tb-border);\n border-radius: 999px;\n transition: background-color 100ms ease;\n cursor: pointer;\n}\n.tb-switch-thumb {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 14px;\n height: 14px;\n background: var(--tb-bg);\n border-radius: 50%;\n transition: transform 100ms ease;\n}\n.tb-switch input:checked + .tb-switch-track {\n background: var(--tb-primary-bg);\n}\n.tb-switch input:checked + .tb-switch-track .tb-switch-thumb {\n transform: translateX(14px);\n}\n.tb-switch input:disabled + .tb-switch-track {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.tb-switch input:focus-visible + .tb-switch-track {\n outline: 2px solid var(--tb-link);\n outline-offset: 2px;\n}\n\n@keyframes tb-fade-in {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n@media (max-width: 640px) {\n .tb-banner {\n flex-direction: column;\n align-items: stretch;\n }\n .tb-banner-actions {\n flex-direction: column;\n }\n .tb-banner-actions .tb-btn { width: 100%; }\n}\n`\n\nconst STYLE_ID = 'tickbox-default-styles'\n\nlet injected = false\n\n/**\n * Insert the stylesheet into `<head>` exactly once per page. Safe to call\n * from every component mount — subsequent calls are no-ops. No-op on the\n * server (no `document`).\n */\nexport function injectStyles(): void {\n if (injected) return\n if (typeof document === 'undefined') return\n if (document.getElementById(STYLE_ID)) {\n injected = true\n return\n }\n const el = document.createElement('style')\n el.id = STYLE_ID\n el.textContent = TICKBOX_STYLES\n document.head.appendChild(el)\n injected = true\n}\n"]}
@@ -0,0 +1,20 @@
1
+ type BannerCopy = {
2
+ title: string;
3
+ description: string;
4
+ acceptLabel: string;
5
+ rejectLabel: string;
6
+ customiseLabel: string;
7
+ saveLabel: string;
8
+ closeLabel: string;
9
+ policyLinkLabel: string;
10
+ requiredBadge: string;
11
+ };
12
+ type NoticeCopy = {
13
+ title: string;
14
+ description: string;
15
+ acknowledgeLabel: string;
16
+ optOutLabel: string;
17
+ policyLinkLabel: string;
18
+ };
19
+
20
+ export type { BannerCopy as B, NoticeCopy as N };
@@ -0,0 +1,64 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { B as BannerCopy, N as NoticeCopy } from '../copy-DA2csB2L.js';
3
+
4
+ type ConsentBannerDefaultProps = {
5
+ /**
6
+ * Override individual labels and copy strings. Anything you don't pass
7
+ * falls back to the English defaults.
8
+ */
9
+ copy?: Partial<BannerCopy>;
10
+ /**
11
+ * URL of the privacy policy linked from the banner. If omitted, the link
12
+ * is hidden. The Tickbox config's `policy.url` is the natural source —
13
+ * pass it here.
14
+ */
15
+ policyUrl?: string | undefined;
16
+ /**
17
+ * Force light or dark theme. By default the banner follows
18
+ * `prefers-color-scheme`.
19
+ */
20
+ theme?: 'light' | 'dark';
21
+ };
22
+ /**
23
+ * Drop-in styled consent banner. Mounts itself only when the headless
24
+ * `<ConsentBanner>` says it should be open. Click "Customise" to expand
25
+ * a per-category modal.
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * import config from './consent.config'
30
+ * <ConsentBannerDefault policyUrl={config.policy?.url} />
31
+ * ```
32
+ */
33
+ declare function ConsentBannerDefault(props: ConsentBannerDefaultProps): react_jsx_runtime.JSX.Element;
34
+
35
+ type ConsentNoticeDefaultProps = {
36
+ /**
37
+ * Override individual labels and copy strings. Anything you don't pass
38
+ * falls back to the English defaults.
39
+ */
40
+ copy?: Partial<NoticeCopy>;
41
+ /** Privacy-policy URL. If omitted, the link is hidden. */
42
+ policyUrl?: string | undefined;
43
+ /**
44
+ * Category ID to deny when the user clicks "Opt out". Defaults to
45
+ * `'analytics'` since that's the most common notice-mode category.
46
+ */
47
+ optOutCategoryId?: string;
48
+ /** Force light or dark theme. */
49
+ theme?: 'light' | 'dark';
50
+ };
51
+ /**
52
+ * Drop-in styled notice card for sites that have only `notice`-mode
53
+ * categories (typically UK DUAA-exempt analytics like Plausible or
54
+ * GoatCounter). Bottom-right toast.
55
+ *
56
+ * @example
57
+ * ```tsx
58
+ * import config from './consent.config'
59
+ * <ConsentNoticeDefault policyUrl={config.policy?.url} />
60
+ * ```
61
+ */
62
+ declare function ConsentNoticeDefault(props: ConsentNoticeDefaultProps): react_jsx_runtime.JSX.Element;
63
+
64
+ export { ConsentBannerDefault, type ConsentBannerDefaultProps, ConsentNoticeDefault, type ConsentNoticeDefaultProps };
@@ -0,0 +1,213 @@
1
+ import { DEFAULT_BANNER_COPY, injectStyles, DEFAULT_NOTICE_COPY } from '../chunk-WJSFVAK4.js';
2
+ import { ConsentBanner, ConsentNotice } from '@tickboxhq/react';
3
+ import { useState, useEffect, useId, useRef } from 'react';
4
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
+
6
+ function ConsentBannerDefault(props) {
7
+ return /* @__PURE__ */ jsx(ConsentBanner, { children: (api) => /* @__PURE__ */ jsx(BannerInner, { api, props }) });
8
+ }
9
+ function BannerInner({
10
+ api,
11
+ props
12
+ }) {
13
+ const copy = { ...DEFAULT_BANNER_COPY, ...props.copy };
14
+ const [showModal, setShowModal] = useState(false);
15
+ useEffect(() => {
16
+ injectStyles();
17
+ }, []);
18
+ const themeAttrs = props.theme ? { "data-tb-theme": props.theme } : {};
19
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
20
+ /* @__PURE__ */ jsxs("div", { className: "tb-root tb-banner", role: "region", "aria-label": copy.title, ...themeAttrs, children: [
21
+ /* @__PURE__ */ jsxs("div", { className: "tb-banner-text", children: [
22
+ /* @__PURE__ */ jsx("p", { className: "tb-banner-title", children: copy.title }),
23
+ /* @__PURE__ */ jsx("p", { className: "tb-banner-desc", children: copy.description })
24
+ ] }),
25
+ /* @__PURE__ */ jsxs("div", { className: "tb-banner-actions", children: [
26
+ props.policyUrl && /* @__PURE__ */ jsx("a", { className: "tb-link", href: props.policyUrl, children: copy.policyLinkLabel }),
27
+ /* @__PURE__ */ jsx("button", { type: "button", className: "tb-btn tb-btn-secondary", onClick: () => api.denyAll(), children: copy.rejectLabel }),
28
+ /* @__PURE__ */ jsx(
29
+ "button",
30
+ {
31
+ type: "button",
32
+ className: "tb-btn tb-btn-secondary",
33
+ onClick: () => setShowModal(true),
34
+ children: copy.customiseLabel
35
+ }
36
+ ),
37
+ /* @__PURE__ */ jsx("button", { type: "button", className: "tb-btn tb-btn-primary", onClick: () => api.grantAll(), children: copy.acceptLabel })
38
+ ] })
39
+ ] }),
40
+ showModal && /* @__PURE__ */ jsx(
41
+ CustomiseModal,
42
+ {
43
+ api,
44
+ copy,
45
+ theme: props.theme,
46
+ onClose: () => setShowModal(false)
47
+ }
48
+ )
49
+ ] });
50
+ }
51
+ function CustomiseModal({
52
+ api,
53
+ copy,
54
+ theme,
55
+ onClose
56
+ }) {
57
+ const titleId = useId();
58
+ const containerRef = useRef(null);
59
+ useEffect(() => {
60
+ function onKey(e) {
61
+ if (e.key === "Escape") onClose();
62
+ if (e.key === "Tab") trapFocus(e, containerRef.current);
63
+ }
64
+ document.addEventListener("keydown", onKey);
65
+ const previouslyFocused = document.activeElement;
66
+ const first = containerRef.current?.querySelector(
67
+ 'button, [href], input, [tabindex]:not([tabindex="-1"])'
68
+ );
69
+ first?.focus();
70
+ return () => {
71
+ document.removeEventListener("keydown", onKey);
72
+ previouslyFocused?.focus?.();
73
+ };
74
+ }, [onClose]);
75
+ const themeAttrs = theme ? { "data-tb-theme": theme } : {};
76
+ return /* @__PURE__ */ jsx(
77
+ "div",
78
+ {
79
+ className: "tb-root tb-modal-backdrop",
80
+ onClick: onClose,
81
+ onKeyDown: (e) => {
82
+ if (e.key === "Escape") onClose();
83
+ },
84
+ role: "presentation",
85
+ ...themeAttrs,
86
+ children: /* @__PURE__ */ jsxs(
87
+ "div",
88
+ {
89
+ ref: containerRef,
90
+ className: "tb-modal",
91
+ role: "dialog",
92
+ "aria-modal": "true",
93
+ "aria-labelledby": titleId,
94
+ onClick: (e) => e.stopPropagation(),
95
+ onKeyDown: (e) => e.stopPropagation(),
96
+ children: [
97
+ /* @__PURE__ */ jsxs("div", { className: "tb-modal-head", children: [
98
+ /* @__PURE__ */ jsx("h2", { id: titleId, className: "tb-modal-title", children: copy.customiseLabel }),
99
+ /* @__PURE__ */ jsx(
100
+ "button",
101
+ {
102
+ type: "button",
103
+ className: "tb-btn tb-btn-ghost",
104
+ "aria-label": copy.closeLabel,
105
+ onClick: onClose,
106
+ children: "\u2715"
107
+ }
108
+ )
109
+ ] }),
110
+ /* @__PURE__ */ jsx("div", { className: "tb-modal-body", children: api.resolved.map((cat) => {
111
+ const checked = api.decisions[cat.id] === true;
112
+ const id = `tb-cat-${cat.id}`;
113
+ return /* @__PURE__ */ jsxs("div", { className: "tb-cat", children: [
114
+ /* @__PURE__ */ jsxs("div", { className: "tb-cat-text", children: [
115
+ /* @__PURE__ */ jsxs("p", { className: "tb-cat-name", children: [
116
+ /* @__PURE__ */ jsx("label", { htmlFor: id, children: cat.id }),
117
+ cat.required && /* @__PURE__ */ jsx("span", { className: "tb-badge", children: copy.requiredBadge })
118
+ ] }),
119
+ cat.description && /* @__PURE__ */ jsx("p", { className: "tb-cat-desc", children: cat.description })
120
+ ] }),
121
+ /* @__PURE__ */ jsxs("label", { className: "tb-switch", children: [
122
+ /* @__PURE__ */ jsx(
123
+ "input",
124
+ {
125
+ id,
126
+ type: "checkbox",
127
+ checked,
128
+ disabled: cat.required,
129
+ onChange: (e) => {
130
+ if (e.target.checked) api.grant(cat.id);
131
+ else api.deny(cat.id);
132
+ }
133
+ }
134
+ ),
135
+ /* @__PURE__ */ jsx("span", { className: "tb-switch-track", children: /* @__PURE__ */ jsx("span", { className: "tb-switch-thumb" }) })
136
+ ] })
137
+ ] }, cat.id);
138
+ }) }),
139
+ /* @__PURE__ */ jsxs("div", { className: "tb-modal-foot", children: [
140
+ /* @__PURE__ */ jsx("button", { type: "button", className: "tb-btn tb-btn-secondary", onClick: () => api.denyAll(), children: copy.rejectLabel }),
141
+ /* @__PURE__ */ jsx("button", { type: "button", className: "tb-btn tb-btn-primary", onClick: () => api.save(), children: copy.saveLabel })
142
+ ] })
143
+ ]
144
+ }
145
+ )
146
+ }
147
+ );
148
+ }
149
+ function trapFocus(e, container) {
150
+ if (!container) return;
151
+ const focusables = container.querySelectorAll(
152
+ 'button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])'
153
+ );
154
+ if (focusables.length === 0) return;
155
+ const first = focusables[0];
156
+ const last = focusables[focusables.length - 1];
157
+ if (!first || !last) return;
158
+ const active = document.activeElement;
159
+ if (e.shiftKey && active === first) {
160
+ e.preventDefault();
161
+ last.focus();
162
+ } else if (!e.shiftKey && active === last) {
163
+ e.preventDefault();
164
+ first.focus();
165
+ }
166
+ }
167
+ function ConsentNoticeDefault(props) {
168
+ return /* @__PURE__ */ jsx(ConsentNotice, { children: (api) => /* @__PURE__ */ jsx(NoticeInner, { api, props }) });
169
+ }
170
+ function NoticeInner({ api, props }) {
171
+ const copy = { ...DEFAULT_NOTICE_COPY, ...props.copy };
172
+ const optOutId = props.optOutCategoryId ?? "analytics";
173
+ useEffect(() => {
174
+ injectStyles();
175
+ }, []);
176
+ const themeAttrs = props.theme ? { "data-tb-theme": props.theme } : {};
177
+ return /* @__PURE__ */ jsxs(
178
+ "div",
179
+ {
180
+ className: "tb-root tb-notice",
181
+ role: "status",
182
+ "aria-live": "polite",
183
+ "aria-label": copy.title,
184
+ ...themeAttrs,
185
+ children: [
186
+ /* @__PURE__ */ jsx("p", { className: "tb-notice-title", children: copy.title }),
187
+ /* @__PURE__ */ jsx("p", { className: "tb-notice-desc", children: copy.description }),
188
+ /* @__PURE__ */ jsxs("div", { className: "tb-notice-actions", children: [
189
+ props.policyUrl && /* @__PURE__ */ jsx("a", { className: "tb-link", href: props.policyUrl, children: copy.policyLinkLabel }),
190
+ /* @__PURE__ */ jsx(
191
+ "button",
192
+ {
193
+ type: "button",
194
+ className: "tb-btn tb-btn-secondary",
195
+ onClick: () => {
196
+ if (api.resolved.some((r) => r.id === optOutId)) {
197
+ api.deny(optOutId);
198
+ }
199
+ api.save();
200
+ },
201
+ children: copy.optOutLabel
202
+ }
203
+ ),
204
+ /* @__PURE__ */ jsx("button", { type: "button", className: "tb-btn tb-btn-primary", onClick: () => api.save(), children: copy.acknowledgeLabel })
205
+ ] })
206
+ ]
207
+ }
208
+ );
209
+ }
210
+
211
+ export { ConsentBannerDefault, ConsentNoticeDefault };
212
+ //# sourceMappingURL=index.js.map
213
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/banner.tsx","../../src/react/notice.tsx"],"names":["jsx","useEffect","jsxs"],"mappings":"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","file":"index.js","sourcesContent":["import { type ConsentApi, ConsentBanner } from '@tickboxhq/react'\nimport { useEffect, useId, useRef, useState } from 'react'\nimport { type BannerCopy, DEFAULT_BANNER_COPY } from '../shared/copy.js'\nimport { injectStyles } from '../shared/styles.js'\n\nexport type ConsentBannerDefaultProps = {\n /**\n * Override individual labels and copy strings. Anything you don't pass\n * falls back to the English defaults.\n */\n copy?: Partial<BannerCopy>\n /**\n * URL of the privacy policy linked from the banner. If omitted, the link\n * is hidden. The Tickbox config's `policy.url` is the natural source —\n * pass it here.\n */\n policyUrl?: string | undefined\n /**\n * Force light or dark theme. By default the banner follows\n * `prefers-color-scheme`.\n */\n theme?: 'light' | 'dark'\n}\n\n/**\n * Drop-in styled consent banner. Mounts itself only when the headless\n * `<ConsentBanner>` says it should be open. Click \"Customise\" to expand\n * a per-category modal.\n *\n * @example\n * ```tsx\n * import config from './consent.config'\n * <ConsentBannerDefault policyUrl={config.policy?.url} />\n * ```\n */\nexport function ConsentBannerDefault(props: ConsentBannerDefaultProps) {\n return <ConsentBanner>{(api) => <BannerInner api={api} props={props} />}</ConsentBanner>\n}\n\nfunction BannerInner({\n api,\n props,\n}: {\n api: ConsentApi\n props: ConsentBannerDefaultProps\n}) {\n const copy: BannerCopy = { ...DEFAULT_BANNER_COPY, ...props.copy }\n const [showModal, setShowModal] = useState(false)\n\n useEffect(() => {\n injectStyles()\n }, [])\n\n const themeAttrs = props.theme ? { 'data-tb-theme': props.theme } : {}\n\n return (\n <>\n <div className=\"tb-root tb-banner\" role=\"region\" aria-label={copy.title} {...themeAttrs}>\n <div className=\"tb-banner-text\">\n <p className=\"tb-banner-title\">{copy.title}</p>\n <p className=\"tb-banner-desc\">{copy.description}</p>\n </div>\n <div className=\"tb-banner-actions\">\n {props.policyUrl && (\n <a className=\"tb-link\" href={props.policyUrl}>\n {copy.policyLinkLabel}\n </a>\n )}\n <button type=\"button\" className=\"tb-btn tb-btn-secondary\" onClick={() => api.denyAll()}>\n {copy.rejectLabel}\n </button>\n <button\n type=\"button\"\n className=\"tb-btn tb-btn-secondary\"\n onClick={() => setShowModal(true)}\n >\n {copy.customiseLabel}\n </button>\n <button type=\"button\" className=\"tb-btn tb-btn-primary\" onClick={() => api.grantAll()}>\n {copy.acceptLabel}\n </button>\n </div>\n </div>\n {showModal && (\n <CustomiseModal\n api={api}\n copy={copy}\n theme={props.theme}\n onClose={() => setShowModal(false)}\n />\n )}\n </>\n )\n}\n\nfunction CustomiseModal({\n api,\n copy,\n theme,\n onClose,\n}: {\n api: ConsentApi\n copy: BannerCopy\n theme?: 'light' | 'dark'\n onClose: () => void\n}) {\n const titleId = useId()\n const containerRef = useRef<HTMLDivElement | null>(null)\n\n useEffect(() => {\n function onKey(e: KeyboardEvent) {\n if (e.key === 'Escape') onClose()\n if (e.key === 'Tab') trapFocus(e, containerRef.current)\n }\n document.addEventListener('keydown', onKey)\n const previouslyFocused = document.activeElement as HTMLElement | null\n const first = containerRef.current?.querySelector<HTMLElement>(\n 'button, [href], input, [tabindex]:not([tabindex=\"-1\"])',\n )\n first?.focus()\n return () => {\n document.removeEventListener('keydown', onKey)\n previouslyFocused?.focus?.()\n }\n }, [onClose])\n\n const themeAttrs = theme ? { 'data-tb-theme': theme } : {}\n\n return (\n <div\n className=\"tb-root tb-modal-backdrop\"\n onClick={onClose}\n onKeyDown={(e) => {\n if (e.key === 'Escape') onClose()\n }}\n role=\"presentation\"\n {...themeAttrs}\n >\n <div\n ref={containerRef}\n className=\"tb-modal\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={titleId}\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => e.stopPropagation()}\n >\n <div className=\"tb-modal-head\">\n <h2 id={titleId} className=\"tb-modal-title\">\n {copy.customiseLabel}\n </h2>\n <button\n type=\"button\"\n className=\"tb-btn tb-btn-ghost\"\n aria-label={copy.closeLabel}\n onClick={onClose}\n >\n ✕\n </button>\n </div>\n <div className=\"tb-modal-body\">\n {api.resolved.map((cat) => {\n const checked = api.decisions[cat.id] === true\n const id = `tb-cat-${cat.id}`\n return (\n <div key={cat.id} className=\"tb-cat\">\n <div className=\"tb-cat-text\">\n <p className=\"tb-cat-name\">\n <label htmlFor={id}>{cat.id}</label>\n {cat.required && <span className=\"tb-badge\">{copy.requiredBadge}</span>}\n </p>\n {cat.description && <p className=\"tb-cat-desc\">{cat.description}</p>}\n </div>\n <label className=\"tb-switch\">\n <input\n id={id}\n type=\"checkbox\"\n checked={checked}\n disabled={cat.required}\n onChange={(e) => {\n if (e.target.checked) api.grant(cat.id)\n else api.deny(cat.id)\n }}\n />\n <span className=\"tb-switch-track\">\n <span className=\"tb-switch-thumb\" />\n </span>\n </label>\n </div>\n )\n })}\n </div>\n <div className=\"tb-modal-foot\">\n <button type=\"button\" className=\"tb-btn tb-btn-secondary\" onClick={() => api.denyAll()}>\n {copy.rejectLabel}\n </button>\n <button type=\"button\" className=\"tb-btn tb-btn-primary\" onClick={() => api.save()}>\n {copy.saveLabel}\n </button>\n </div>\n </div>\n </div>\n )\n}\n\nfunction trapFocus(e: KeyboardEvent, container: HTMLDivElement | null) {\n if (!container) return\n const focusables = container.querySelectorAll<HTMLElement>(\n 'button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex=\"-1\"])',\n )\n if (focusables.length === 0) return\n const first = focusables[0]\n const last = focusables[focusables.length - 1]\n if (!first || !last) return\n const active = document.activeElement\n if (e.shiftKey && active === first) {\n e.preventDefault()\n last.focus()\n } else if (!e.shiftKey && active === last) {\n e.preventDefault()\n first.focus()\n }\n}\n","import { type ConsentApi, ConsentNotice } from '@tickboxhq/react'\nimport { useEffect } from 'react'\nimport { DEFAULT_NOTICE_COPY, type NoticeCopy } from '../shared/copy.js'\nimport { injectStyles } from '../shared/styles.js'\n\nexport type ConsentNoticeDefaultProps = {\n /**\n * Override individual labels and copy strings. Anything you don't pass\n * falls back to the English defaults.\n */\n copy?: Partial<NoticeCopy>\n /** Privacy-policy URL. If omitted, the link is hidden. */\n policyUrl?: string | undefined\n /**\n * Category ID to deny when the user clicks \"Opt out\". Defaults to\n * `'analytics'` since that's the most common notice-mode category.\n */\n optOutCategoryId?: string\n /** Force light or dark theme. */\n theme?: 'light' | 'dark'\n}\n\n/**\n * Drop-in styled notice card for sites that have only `notice`-mode\n * categories (typically UK DUAA-exempt analytics like Plausible or\n * GoatCounter). Bottom-right toast.\n *\n * @example\n * ```tsx\n * import config from './consent.config'\n * <ConsentNoticeDefault policyUrl={config.policy?.url} />\n * ```\n */\nexport function ConsentNoticeDefault(props: ConsentNoticeDefaultProps) {\n return <ConsentNotice>{(api) => <NoticeInner api={api} props={props} />}</ConsentNotice>\n}\n\nfunction NoticeInner({ api, props }: { api: ConsentApi; props: ConsentNoticeDefaultProps }) {\n const copy: NoticeCopy = { ...DEFAULT_NOTICE_COPY, ...props.copy }\n const optOutId = props.optOutCategoryId ?? 'analytics'\n\n useEffect(() => {\n injectStyles()\n }, [])\n\n const themeAttrs = props.theme ? { 'data-tb-theme': props.theme } : {}\n\n return (\n <div\n className=\"tb-root tb-notice\"\n role=\"status\"\n aria-live=\"polite\"\n aria-label={copy.title}\n {...themeAttrs}\n >\n <p className=\"tb-notice-title\">{copy.title}</p>\n <p className=\"tb-notice-desc\">{copy.description}</p>\n <div className=\"tb-notice-actions\">\n {props.policyUrl && (\n <a className=\"tb-link\" href={props.policyUrl}>\n {copy.policyLinkLabel}\n </a>\n )}\n <button\n type=\"button\"\n className=\"tb-btn tb-btn-secondary\"\n onClick={() => {\n if (api.resolved.some((r) => r.id === optOutId)) {\n api.deny(optOutId)\n }\n api.save()\n }}\n >\n {copy.optOutLabel}\n </button>\n <button type=\"button\" className=\"tb-btn tb-btn-primary\" onClick={() => api.save()}>\n {copy.acknowledgeLabel}\n </button>\n </div>\n </div>\n )\n}\n"]}
@@ -0,0 +1,114 @@
1
+ import * as vue from 'vue';
2
+ import { PropType } from 'vue';
3
+ import { B as BannerCopy, N as NoticeCopy } from '../copy-DA2csB2L.js';
4
+
5
+ type ConsentBannerDefaultProps = {
6
+ copy?: Partial<BannerCopy>;
7
+ policyUrl?: string;
8
+ theme?: 'light' | 'dark';
9
+ };
10
+ /**
11
+ * Drop-in styled consent banner for Vue. Mounts only when the headless
12
+ * `<ConsentBanner>` says it should be open. "Customise" opens a modal
13
+ * with per-category toggles.
14
+ *
15
+ * @example
16
+ * ```vue
17
+ * <script setup lang="ts">
18
+ * import { ConsentBannerDefault } from '@tickboxhq/banner-default/vue'
19
+ * import config from './consent.config'
20
+ * </script>
21
+ * <template>
22
+ * <ConsentBannerDefault :policy-url="config.policy?.url" />
23
+ * </template>
24
+ * ```
25
+ */
26
+ declare const ConsentBannerDefault: vue.DefineComponent<vue.ExtractPropTypes<{
27
+ copy: {
28
+ type: PropType<Partial<BannerCopy>>;
29
+ default: () => {};
30
+ };
31
+ policyUrl: {
32
+ type: PropType<string | undefined>;
33
+ default: undefined;
34
+ };
35
+ theme: {
36
+ type: PropType<"light" | "dark" | undefined>;
37
+ default: undefined;
38
+ };
39
+ }>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
40
+ [key: string]: any;
41
+ }>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{
42
+ copy: {
43
+ type: PropType<Partial<BannerCopy>>;
44
+ default: () => {};
45
+ };
46
+ policyUrl: {
47
+ type: PropType<string | undefined>;
48
+ default: undefined;
49
+ };
50
+ theme: {
51
+ type: PropType<"light" | "dark" | undefined>;
52
+ default: undefined;
53
+ };
54
+ }>> & Readonly<{}>, {
55
+ copy: Partial<BannerCopy>;
56
+ policyUrl: string | undefined;
57
+ theme: "light" | "dark" | undefined;
58
+ }, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
59
+
60
+ type ConsentNoticeDefaultProps = {
61
+ copy?: Partial<NoticeCopy>;
62
+ policyUrl?: string;
63
+ optOutCategoryId?: string;
64
+ theme?: 'light' | 'dark';
65
+ };
66
+ /**
67
+ * Drop-in styled notice card for sites with only `notice`-mode categories
68
+ * (typically UK DUAA-exempt analytics). Bottom-right toast with
69
+ * "Got it" / "Opt out" actions.
70
+ */
71
+ declare const ConsentNoticeDefault: vue.DefineComponent<vue.ExtractPropTypes<{
72
+ copy: {
73
+ type: PropType<Partial<NoticeCopy>>;
74
+ default: () => {};
75
+ };
76
+ policyUrl: {
77
+ type: PropType<string | undefined>;
78
+ default: undefined;
79
+ };
80
+ optOutCategoryId: {
81
+ type: StringConstructor;
82
+ default: string;
83
+ };
84
+ theme: {
85
+ type: PropType<"light" | "dark" | undefined>;
86
+ default: undefined;
87
+ };
88
+ }>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
89
+ [key: string]: any;
90
+ }>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{
91
+ copy: {
92
+ type: PropType<Partial<NoticeCopy>>;
93
+ default: () => {};
94
+ };
95
+ policyUrl: {
96
+ type: PropType<string | undefined>;
97
+ default: undefined;
98
+ };
99
+ optOutCategoryId: {
100
+ type: StringConstructor;
101
+ default: string;
102
+ };
103
+ theme: {
104
+ type: PropType<"light" | "dark" | undefined>;
105
+ default: undefined;
106
+ };
107
+ }>> & Readonly<{}>, {
108
+ copy: Partial<NoticeCopy>;
109
+ policyUrl: string | undefined;
110
+ theme: "light" | "dark" | undefined;
111
+ optOutCategoryId: string;
112
+ }, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
113
+
114
+ export { ConsentBannerDefault, type ConsentBannerDefaultProps, ConsentNoticeDefault, type ConsentNoticeDefaultProps };
@@ -0,0 +1,235 @@
1
+ import { injectStyles, DEFAULT_BANNER_COPY, DEFAULT_NOTICE_COPY } from '../chunk-WJSFVAK4.js';
2
+ import { ConsentBanner, ConsentNotice } from '@tickboxhq/vue';
3
+ import { defineComponent, onMounted, ref, h } from 'vue';
4
+
5
+ var ConsentBannerDefault = defineComponent({
6
+ name: "ConsentBannerDefault",
7
+ props: {
8
+ copy: { type: Object, default: () => ({}) },
9
+ policyUrl: { type: String, default: void 0 },
10
+ theme: { type: String, default: void 0 }
11
+ },
12
+ setup(props) {
13
+ onMounted(() => injectStyles());
14
+ const showModal = ref(false);
15
+ return () => h(ConsentBanner, null, {
16
+ default: (api) => renderBanner(api, props, showModal)
17
+ });
18
+ }
19
+ });
20
+ function renderBanner(api, props, showModal) {
21
+ const copy = { ...DEFAULT_BANNER_COPY, ...props.copy ?? {} };
22
+ const themeAttrs = props.theme ? { "data-tb-theme": props.theme } : {};
23
+ return h("div", null, [
24
+ h(
25
+ "div",
26
+ {
27
+ class: "tb-root tb-banner",
28
+ role: "region",
29
+ "aria-label": copy.title,
30
+ ...themeAttrs
31
+ },
32
+ [
33
+ h("div", { class: "tb-banner-text" }, [
34
+ h("p", { class: "tb-banner-title" }, copy.title),
35
+ h("p", { class: "tb-banner-desc" }, copy.description)
36
+ ]),
37
+ h("div", { class: "tb-banner-actions" }, [
38
+ props.policyUrl ? h("a", { class: "tb-link", href: props.policyUrl }, copy.policyLinkLabel) : null,
39
+ h(
40
+ "button",
41
+ {
42
+ type: "button",
43
+ class: "tb-btn tb-btn-secondary",
44
+ onClick: () => api.denyAll()
45
+ },
46
+ copy.rejectLabel
47
+ ),
48
+ h(
49
+ "button",
50
+ {
51
+ type: "button",
52
+ class: "tb-btn tb-btn-secondary",
53
+ onClick: () => {
54
+ showModal.value = true;
55
+ }
56
+ },
57
+ copy.customiseLabel
58
+ ),
59
+ h(
60
+ "button",
61
+ {
62
+ type: "button",
63
+ class: "tb-btn tb-btn-primary",
64
+ onClick: () => api.grantAll()
65
+ },
66
+ copy.acceptLabel
67
+ )
68
+ ])
69
+ ]
70
+ ),
71
+ showModal.value ? renderModal(api, copy, props.theme, () => {
72
+ showModal.value = false;
73
+ }) : null
74
+ ]);
75
+ }
76
+ function renderModal(api, copy, theme, onClose) {
77
+ const themeAttrs = theme ? { "data-tb-theme": theme } : {};
78
+ return h(
79
+ "div",
80
+ {
81
+ class: "tb-root tb-modal-backdrop",
82
+ role: "presentation",
83
+ onClick: onClose,
84
+ onKeydown: (e) => {
85
+ if (e.key === "Escape") onClose();
86
+ },
87
+ ...themeAttrs
88
+ },
89
+ [
90
+ h(
91
+ "div",
92
+ {
93
+ class: "tb-modal",
94
+ role: "dialog",
95
+ "aria-modal": "true",
96
+ "aria-label": copy.customiseLabel,
97
+ onClick: (e) => e.stopPropagation(),
98
+ onKeydown: (e) => e.stopPropagation()
99
+ },
100
+ [
101
+ h("div", { class: "tb-modal-head" }, [
102
+ h("h2", { class: "tb-modal-title" }, copy.customiseLabel),
103
+ h(
104
+ "button",
105
+ {
106
+ type: "button",
107
+ class: "tb-btn tb-btn-ghost",
108
+ "aria-label": copy.closeLabel,
109
+ onClick: onClose
110
+ },
111
+ "\u2715"
112
+ )
113
+ ]),
114
+ h(
115
+ "div",
116
+ { class: "tb-modal-body" },
117
+ api.resolved.value.map((cat) => {
118
+ const checked = api.decisions.value[cat.id] === true;
119
+ const id = `tb-cat-${cat.id}`;
120
+ return h("div", { key: cat.id, class: "tb-cat" }, [
121
+ h("div", { class: "tb-cat-text" }, [
122
+ h("p", { class: "tb-cat-name" }, [
123
+ h("label", { for: id }, cat.id),
124
+ cat.required ? h("span", { class: "tb-badge" }, copy.requiredBadge) : null
125
+ ]),
126
+ cat.description ? h("p", { class: "tb-cat-desc" }, cat.description) : null
127
+ ]),
128
+ h("label", { class: "tb-switch" }, [
129
+ h("input", {
130
+ id,
131
+ type: "checkbox",
132
+ checked,
133
+ disabled: cat.required,
134
+ onChange: (e) => {
135
+ const next = e.target.checked;
136
+ if (next) api.grant(cat.id);
137
+ else api.deny(cat.id);
138
+ }
139
+ }),
140
+ h("span", { class: "tb-switch-track" }, [
141
+ h("span", { class: "tb-switch-thumb" })
142
+ ])
143
+ ])
144
+ ]);
145
+ })
146
+ ),
147
+ h("div", { class: "tb-modal-foot" }, [
148
+ h(
149
+ "button",
150
+ {
151
+ type: "button",
152
+ class: "tb-btn tb-btn-secondary",
153
+ onClick: () => api.denyAll()
154
+ },
155
+ copy.rejectLabel
156
+ ),
157
+ h(
158
+ "button",
159
+ {
160
+ type: "button",
161
+ class: "tb-btn tb-btn-primary",
162
+ onClick: () => api.save()
163
+ },
164
+ copy.saveLabel
165
+ )
166
+ ])
167
+ ]
168
+ )
169
+ ]
170
+ );
171
+ }
172
+ var ConsentNoticeDefault = defineComponent({
173
+ name: "ConsentNoticeDefault",
174
+ props: {
175
+ copy: { type: Object, default: () => ({}) },
176
+ policyUrl: { type: String, default: void 0 },
177
+ optOutCategoryId: { type: String, default: "analytics" },
178
+ theme: { type: String, default: void 0 }
179
+ },
180
+ setup(props) {
181
+ onMounted(() => injectStyles());
182
+ return () => h(ConsentNotice, null, {
183
+ default: (api) => renderNotice(api, props)
184
+ });
185
+ }
186
+ });
187
+ function renderNotice(api, props) {
188
+ const copy = { ...DEFAULT_NOTICE_COPY, ...props.copy ?? {} };
189
+ const optOutId = props.optOutCategoryId ?? "analytics";
190
+ const themeAttrs = props.theme ? { "data-tb-theme": props.theme } : {};
191
+ return h(
192
+ "div",
193
+ {
194
+ class: "tb-root tb-notice",
195
+ role: "status",
196
+ "aria-live": "polite",
197
+ "aria-label": copy.title,
198
+ ...themeAttrs
199
+ },
200
+ [
201
+ h("p", { class: "tb-notice-title" }, copy.title),
202
+ h("p", { class: "tb-notice-desc" }, copy.description),
203
+ h("div", { class: "tb-notice-actions" }, [
204
+ props.policyUrl ? h("a", { class: "tb-link", href: props.policyUrl }, copy.policyLinkLabel) : null,
205
+ h(
206
+ "button",
207
+ {
208
+ type: "button",
209
+ class: "tb-btn tb-btn-secondary",
210
+ onClick: () => {
211
+ if (api.resolved.value.some((r) => r.id === optOutId)) {
212
+ api.deny(optOutId);
213
+ }
214
+ api.save();
215
+ }
216
+ },
217
+ copy.optOutLabel
218
+ ),
219
+ h(
220
+ "button",
221
+ {
222
+ type: "button",
223
+ class: "tb-btn tb-btn-primary",
224
+ onClick: () => api.save()
225
+ },
226
+ copy.acknowledgeLabel
227
+ )
228
+ ])
229
+ ]
230
+ );
231
+ }
232
+
233
+ export { ConsentBannerDefault, ConsentNoticeDefault };
234
+ //# sourceMappingURL=index.js.map
235
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/vue/banner.ts","../../src/vue/notice.ts"],"names":["defineComponent","onMounted","h"],"mappings":";;;;AA4BO,IAAM,uBAAuB,eAAA,CAAgB;AAAA,EAClD,IAAA,EAAM,sBAAA;AAAA,EACN,KAAA,EAAO;AAAA,IACL,MAAM,EAAE,IAAA,EAAM,QAAyC,OAAA,EAAS,OAAO,EAAC,CAAA,EAAG;AAAA,IAC3E,SAAA,EAAW,EAAE,IAAA,EAAM,MAAA,EAAwC,SAAS,MAAA,EAAU;AAAA,IAC9E,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAkD,SAAS,MAAA;AAAU,GACtF;AAAA,EACA,MAAM,KAAA,EAAO;AACX,IAAA,SAAA,CAAU,MAAM,cAAc,CAAA;AAC9B,IAAA,MAAM,SAAA,GAAY,IAAI,KAAK,CAAA;AAC3B,IAAA,OAAO,MACL,CAAA,CAAE,aAAA,EAAe,IAAA,EAAM;AAAA,MACrB,SAAS,CAAC,GAAA,KAAiB,YAAA,CAAa,GAAA,EAAmB,OAAO,SAAS;AAAA,KAC5E,CAAA;AAAA,EACL;AACF,CAAC;AAED,SAAS,YAAA,CACP,GAAA,EACA,KAAA,EACA,SAAA,EACA;AACA,EAAA,MAAM,IAAA,GAAmB,EAAE,GAAG,mBAAA,EAAqB,GAAI,KAAA,CAAM,IAAA,IAAQ,EAAC,EAAG;AAEzE,EAAA,MAAM,UAAA,GAAa,MAAM,KAAA,GAAQ,EAAE,iBAAiB,KAAA,CAAM,KAAA,KAAU,EAAC;AAErE,EAAA,OAAO,CAAA,CAAE,OAAO,IAAA,EAAM;AAAA,IACpB,CAAA;AAAA,MACE,KAAA;AAAA,MACA;AAAA,QACE,KAAA,EAAO,mBAAA;AAAA,QACP,IAAA,EAAM,QAAA;AAAA,QACN,cAAc,IAAA,CAAK,KAAA;AAAA,QACnB,GAAG;AAAA,OACL;AAAA,MACA;AAAA,QACE,CAAA,CAAE,KAAA,EAAO,EAAE,KAAA,EAAO,kBAAiB,EAAG;AAAA,UACpC,EAAE,GAAA,EAAK,EAAE,OAAO,iBAAA,EAAkB,EAAG,KAAK,KAAK,CAAA;AAAA,UAC/C,EAAE,GAAA,EAAK,EAAE,OAAO,gBAAA,EAAiB,EAAG,KAAK,WAAW;AAAA,SACrD,CAAA;AAAA,QACD,CAAA,CAAE,KAAA,EAAO,EAAE,KAAA,EAAO,qBAAoB,EAAG;AAAA,UACvC,KAAA,CAAM,SAAA,GACF,CAAA,CAAE,GAAA,EAAK,EAAE,KAAA,EAAO,SAAA,EAAW,IAAA,EAAM,KAAA,CAAM,SAAA,EAAU,EAAG,IAAA,CAAK,eAAe,CAAA,GACxE,IAAA;AAAA,UACJ,CAAA;AAAA,YACE,QAAA;AAAA,YACA;AAAA,cACE,IAAA,EAAM,QAAA;AAAA,cACN,KAAA,EAAO,yBAAA;AAAA,cACP,OAAA,EAAS,MAAM,GAAA,CAAI,OAAA;AAAQ,aAC7B;AAAA,YACA,IAAA,CAAK;AAAA,WACP;AAAA,UACA,CAAA;AAAA,YACE,QAAA;AAAA,YACA;AAAA,cACE,IAAA,EAAM,QAAA;AAAA,cACN,KAAA,EAAO,yBAAA;AAAA,cACP,SAAS,MAAM;AACb,gBAAA,SAAA,CAAU,KAAA,GAAQ,IAAA;AAAA,cACpB;AAAA,aACF;AAAA,YACA,IAAA,CAAK;AAAA,WACP;AAAA,UACA,CAAA;AAAA,YACE,QAAA;AAAA,YACA;AAAA,cACE,IAAA,EAAM,QAAA;AAAA,cACN,KAAA,EAAO,uBAAA;AAAA,cACP,OAAA,EAAS,MAAM,GAAA,CAAI,QAAA;AAAS,aAC9B;AAAA,YACA,IAAA,CAAK;AAAA;AACP,SACD;AAAA;AACH,KACF;AAAA,IACA,UAAU,KAAA,GACN,WAAA,CAAY,KAAK,IAAA,EAAM,KAAA,CAAM,OAAO,MAAM;AACxC,MAAA,SAAA,CAAU,KAAA,GAAQ,KAAA;AAAA,IACpB,CAAC,CAAA,GACD;AAAA,GACL,CAAA;AACH;AAEA,SAAS,WAAA,CACP,GAAA,EACA,IAAA,EACA,KAAA,EACA,OAAA,EACA;AACA,EAAA,MAAM,aAAa,KAAA,GAAQ,EAAE,eAAA,EAAiB,KAAA,KAAU,EAAC;AAEzD,EAAA,OAAO,CAAA;AAAA,IACL,KAAA;AAAA,IACA;AAAA,MACE,KAAA,EAAO,2BAAA;AAAA,MACP,IAAA,EAAM,cAAA;AAAA,MACN,OAAA,EAAS,OAAA;AAAA,MACT,SAAA,EAAW,CAAC,CAAA,KAAqB;AAC/B,QAAA,IAAI,CAAA,CAAE,GAAA,KAAQ,QAAA,EAAU,OAAA,EAAQ;AAAA,MAClC,CAAA;AAAA,MACA,GAAG;AAAA,KACL;AAAA,IACA;AAAA,MACE,CAAA;AAAA,QACE,KAAA;AAAA,QACA;AAAA,UACE,KAAA,EAAO,UAAA;AAAA,UACP,IAAA,EAAM,QAAA;AAAA,UACN,YAAA,EAAc,MAAA;AAAA,UACd,cAAc,IAAA,CAAK,cAAA;AAAA,UACnB,OAAA,EAAS,CAAC,CAAA,KAAkB,CAAA,CAAE,eAAA,EAAgB;AAAA,UAC9C,SAAA,EAAW,CAAC,CAAA,KAAqB,CAAA,CAAE,eAAA;AAAgB,SACrD;AAAA,QACA;AAAA,UACE,CAAA,CAAE,KAAA,EAAO,EAAE,KAAA,EAAO,iBAAgB,EAAG;AAAA,YACnC,EAAE,IAAA,EAAM,EAAE,OAAO,gBAAA,EAAiB,EAAG,KAAK,cAAc,CAAA;AAAA,YACxD,CAAA;AAAA,cACE,QAAA;AAAA,cACA;AAAA,gBACE,IAAA,EAAM,QAAA;AAAA,gBACN,KAAA,EAAO,qBAAA;AAAA,gBACP,cAAc,IAAA,CAAK,UAAA;AAAA,gBACnB,OAAA,EAAS;AAAA,eACX;AAAA,cACA;AAAA;AACF,WACD,CAAA;AAAA,UACD,CAAA;AAAA,YACE,KAAA;AAAA,YACA,EAAE,OAAO,eAAA,EAAgB;AAAA,YACzB,GAAA,CAAI,QAAA,CAAS,KAAA,CAAM,GAAA,CAAI,CAAC,GAAA,KAAQ;AAC9B,cAAA,MAAM,UAAU,GAAA,CAAI,SAAA,CAAU,KAAA,CAAM,GAAA,CAAI,EAAE,CAAA,KAAM,IAAA;AAChD,cAAA,MAAM,EAAA,GAAK,CAAA,OAAA,EAAU,GAAA,CAAI,EAAE,CAAA,CAAA;AAC3B,cAAA,OAAO,CAAA,CAAE,OAAO,EAAE,GAAA,EAAK,IAAI,EAAA,EAAI,KAAA,EAAO,UAAS,EAAG;AAAA,gBAChD,CAAA,CAAE,KAAA,EAAO,EAAE,KAAA,EAAO,eAAc,EAAG;AAAA,kBACjC,CAAA,CAAE,GAAA,EAAK,EAAE,KAAA,EAAO,eAAc,EAAG;AAAA,oBAC/B,EAAE,OAAA,EAAS,EAAE,KAAK,EAAA,EAAG,EAAG,IAAI,EAAE,CAAA;AAAA,oBAC9B,GAAA,CAAI,QAAA,GAAW,CAAA,CAAE,MAAA,EAAQ,EAAE,OAAO,UAAA,EAAW,EAAG,IAAA,CAAK,aAAa,CAAA,GAAI;AAAA,mBACvE,CAAA;AAAA,kBACD,GAAA,CAAI,WAAA,GAAc,CAAA,CAAE,GAAA,EAAK,EAAE,OAAO,aAAA,EAAc,EAAG,GAAA,CAAI,WAAW,CAAA,GAAI;AAAA,iBACvE,CAAA;AAAA,gBACD,CAAA,CAAE,OAAA,EAAS,EAAE,KAAA,EAAO,aAAY,EAAG;AAAA,kBACjC,EAAE,OAAA,EAAS;AAAA,oBACT,EAAA;AAAA,oBACA,IAAA,EAAM,UAAA;AAAA,oBACN,OAAA;AAAA,oBACA,UAAU,GAAA,CAAI,QAAA;AAAA,oBACd,QAAA,EAAU,CAAC,CAAA,KAAa;AACtB,sBAAA,MAAM,IAAA,GAAQ,EAAE,MAAA,CAA4B,OAAA;AAC5C,sBAAA,IAAI,IAAA,EAAM,GAAA,CAAI,KAAA,CAAM,GAAA,CAAI,EAAE,CAAA;AAAA,2BACrB,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,EAAE,CAAA;AAAA,oBACtB;AAAA,mBACD,CAAA;AAAA,kBACD,CAAA,CAAE,MAAA,EAAQ,EAAE,KAAA,EAAO,mBAAkB,EAAG;AAAA,oBACtC,CAAA,CAAE,MAAA,EAAQ,EAAE,KAAA,EAAO,mBAAmB;AAAA,mBACvC;AAAA,iBACF;AAAA,eACF,CAAA;AAAA,YACH,CAAC;AAAA,WACH;AAAA,UACA,CAAA,CAAE,KAAA,EAAO,EAAE,KAAA,EAAO,iBAAgB,EAAG;AAAA,YACnC,CAAA;AAAA,cACE,QAAA;AAAA,cACA;AAAA,gBACE,IAAA,EAAM,QAAA;AAAA,gBACN,KAAA,EAAO,yBAAA;AAAA,gBACP,OAAA,EAAS,MAAM,GAAA,CAAI,OAAA;AAAQ,eAC7B;AAAA,cACA,IAAA,CAAK;AAAA,aACP;AAAA,YACA,CAAA;AAAA,cACE,QAAA;AAAA,cACA;AAAA,gBACE,IAAA,EAAM,QAAA;AAAA,gBACN,KAAA,EAAO,uBAAA;AAAA,gBACP,OAAA,EAAS,MAAM,GAAA,CAAI,IAAA;AAAK,eAC1B;AAAA,cACA,IAAA,CAAK;AAAA;AACP,WACD;AAAA;AACH;AACF;AACF,GACF;AACF;ACnMO,IAAM,uBAAuBA,eAAAA,CAAgB;AAAA,EAClD,IAAA,EAAM,sBAAA;AAAA,EACN,KAAA,EAAO;AAAA,IACL,MAAM,EAAE,IAAA,EAAM,QAAyC,OAAA,EAAS,OAAO,EAAC,CAAA,EAAG;AAAA,IAC3E,SAAA,EAAW,EAAE,IAAA,EAAM,MAAA,EAAwC,SAAS,MAAA,EAAU;AAAA,IAC9E,gBAAA,EAAkB,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,WAAA,EAAY;AAAA,IACvD,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAkD,SAAS,MAAA;AAAU,GACtF;AAAA,EACA,MAAM,KAAA,EAAO;AACX,IAAAC,SAAAA,CAAU,MAAM,YAAA,EAAc,CAAA;AAC9B,IAAA,OAAO,MACLC,CAAAA,CAAE,aAAA,EAAe,IAAA,EAAM;AAAA,MACrB,OAAA,EAAS,CAAC,GAAA,KAAiB,YAAA,CAAa,KAAmB,KAAK;AAAA,KACjE,CAAA;AAAA,EACL;AACF,CAAC;AAED,SAAS,YAAA,CAAa,KAAiB,KAAA,EAAkC;AACvE,EAAA,MAAM,IAAA,GAAmB,EAAE,GAAG,mBAAA,EAAqB,GAAI,KAAA,CAAM,IAAA,IAAQ,EAAC,EAAG;AACzE,EAAA,MAAM,QAAA,GAAW,MAAM,gBAAA,IAAoB,WAAA;AAE3C,EAAA,MAAM,UAAA,GAAa,MAAM,KAAA,GAAQ,EAAE,iBAAiB,KAAA,CAAM,KAAA,KAAU,EAAC;AAErE,EAAA,OAAOA,CAAAA;AAAA,IACL,KAAA;AAAA,IACA;AAAA,MACE,KAAA,EAAO,mBAAA;AAAA,MACP,IAAA,EAAM,QAAA;AAAA,MACN,WAAA,EAAa,QAAA;AAAA,MACb,cAAc,IAAA,CAAK,KAAA;AAAA,MACnB,GAAG;AAAA,KACL;AAAA,IACA;AAAA,MACEA,EAAE,GAAA,EAAK,EAAE,OAAO,iBAAA,EAAkB,EAAG,KAAK,KAAK,CAAA;AAAA,MAC/CA,EAAE,GAAA,EAAK,EAAE,OAAO,gBAAA,EAAiB,EAAG,KAAK,WAAW,CAAA;AAAA,MACpDA,CAAAA,CAAE,KAAA,EAAO,EAAE,KAAA,EAAO,qBAAoB,EAAG;AAAA,QACvC,KAAA,CAAM,SAAA,GACFA,CAAAA,CAAE,GAAA,EAAK,EAAE,KAAA,EAAO,SAAA,EAAW,IAAA,EAAM,KAAA,CAAM,SAAA,EAAU,EAAG,IAAA,CAAK,eAAe,CAAA,GACxE,IAAA;AAAA,QACJA,CAAAA;AAAA,UACE,QAAA;AAAA,UACA;AAAA,YACE,IAAA,EAAM,QAAA;AAAA,YACN,KAAA,EAAO,yBAAA;AAAA,YACP,SAAS,MAAM;AACb,cAAA,IAAI,GAAA,CAAI,SAAS,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,QAAQ,CAAA,EAAG;AACrD,gBAAA,GAAA,CAAI,KAAK,QAAQ,CAAA;AAAA,cACnB;AACA,cAAA,GAAA,CAAI,IAAA,EAAK;AAAA,YACX;AAAA,WACF;AAAA,UACA,IAAA,CAAK;AAAA,SACP;AAAA,QACAA,CAAAA;AAAA,UACE,QAAA;AAAA,UACA;AAAA,YACE,IAAA,EAAM,QAAA;AAAA,YACN,KAAA,EAAO,uBAAA;AAAA,YACP,OAAA,EAAS,MAAM,GAAA,CAAI,IAAA;AAAK,WAC1B;AAAA,UACA,IAAA,CAAK;AAAA;AACP,OACD;AAAA;AACH,GACF;AACF","file":"index.js","sourcesContent":["import type { ConsentApi } from '@tickboxhq/vue'\nimport { ConsentBanner } from '@tickboxhq/vue'\nimport { type PropType, defineComponent, h, onMounted, ref } from 'vue'\nimport { type BannerCopy, DEFAULT_BANNER_COPY } from '../shared/copy.js'\nimport { injectStyles } from '../shared/styles.js'\n\nexport type ConsentBannerDefaultProps = {\n copy?: Partial<BannerCopy>\n policyUrl?: string\n theme?: 'light' | 'dark'\n}\n\n/**\n * Drop-in styled consent banner for Vue. Mounts only when the headless\n * `<ConsentBanner>` says it should be open. \"Customise\" opens a modal\n * with per-category toggles.\n *\n * @example\n * ```vue\n * <script setup lang=\"ts\">\n * import { ConsentBannerDefault } from '@tickboxhq/banner-default/vue'\n * import config from './consent.config'\n * </script>\n * <template>\n * <ConsentBannerDefault :policy-url=\"config.policy?.url\" />\n * </template>\n * ```\n */\nexport const ConsentBannerDefault = defineComponent({\n name: 'ConsentBannerDefault',\n props: {\n copy: { type: Object as PropType<Partial<BannerCopy>>, default: () => ({}) },\n policyUrl: { type: String as PropType<string | undefined>, default: undefined },\n theme: { type: String as PropType<'light' | 'dark' | undefined>, default: undefined },\n },\n setup(props) {\n onMounted(() => injectStyles())\n const showModal = ref(false)\n return () =>\n h(ConsentBanner, null, {\n default: (api: unknown) => renderBanner(api as ConsentApi, props, showModal),\n })\n },\n})\n\nfunction renderBanner(\n api: ConsentApi,\n props: ConsentBannerDefaultProps,\n showModal: ReturnType<typeof ref<boolean>>,\n) {\n const copy: BannerCopy = { ...DEFAULT_BANNER_COPY, ...(props.copy ?? {}) }\n\n const themeAttrs = props.theme ? { 'data-tb-theme': props.theme } : {}\n\n return h('div', null, [\n h(\n 'div',\n {\n class: 'tb-root tb-banner',\n role: 'region',\n 'aria-label': copy.title,\n ...themeAttrs,\n },\n [\n h('div', { class: 'tb-banner-text' }, [\n h('p', { class: 'tb-banner-title' }, copy.title),\n h('p', { class: 'tb-banner-desc' }, copy.description),\n ]),\n h('div', { class: 'tb-banner-actions' }, [\n props.policyUrl\n ? h('a', { class: 'tb-link', href: props.policyUrl }, copy.policyLinkLabel)\n : null,\n h(\n 'button',\n {\n type: 'button',\n class: 'tb-btn tb-btn-secondary',\n onClick: () => api.denyAll(),\n },\n copy.rejectLabel,\n ),\n h(\n 'button',\n {\n type: 'button',\n class: 'tb-btn tb-btn-secondary',\n onClick: () => {\n showModal.value = true\n },\n },\n copy.customiseLabel,\n ),\n h(\n 'button',\n {\n type: 'button',\n class: 'tb-btn tb-btn-primary',\n onClick: () => api.grantAll(),\n },\n copy.acceptLabel,\n ),\n ]),\n ],\n ),\n showModal.value\n ? renderModal(api, copy, props.theme, () => {\n showModal.value = false\n })\n : null,\n ])\n}\n\nfunction renderModal(\n api: ConsentApi,\n copy: BannerCopy,\n theme: 'light' | 'dark' | undefined,\n onClose: () => void,\n) {\n const themeAttrs = theme ? { 'data-tb-theme': theme } : {}\n\n return h(\n 'div',\n {\n class: 'tb-root tb-modal-backdrop',\n role: 'presentation',\n onClick: onClose,\n onKeydown: (e: KeyboardEvent) => {\n if (e.key === 'Escape') onClose()\n },\n ...themeAttrs,\n },\n [\n h(\n 'div',\n {\n class: 'tb-modal',\n role: 'dialog',\n 'aria-modal': 'true',\n 'aria-label': copy.customiseLabel,\n onClick: (e: MouseEvent) => e.stopPropagation(),\n onKeydown: (e: KeyboardEvent) => e.stopPropagation(),\n },\n [\n h('div', { class: 'tb-modal-head' }, [\n h('h2', { class: 'tb-modal-title' }, copy.customiseLabel),\n h(\n 'button',\n {\n type: 'button',\n class: 'tb-btn tb-btn-ghost',\n 'aria-label': copy.closeLabel,\n onClick: onClose,\n },\n '✕',\n ),\n ]),\n h(\n 'div',\n { class: 'tb-modal-body' },\n api.resolved.value.map((cat) => {\n const checked = api.decisions.value[cat.id] === true\n const id = `tb-cat-${cat.id}`\n return h('div', { key: cat.id, class: 'tb-cat' }, [\n h('div', { class: 'tb-cat-text' }, [\n h('p', { class: 'tb-cat-name' }, [\n h('label', { for: id }, cat.id),\n cat.required ? h('span', { class: 'tb-badge' }, copy.requiredBadge) : null,\n ]),\n cat.description ? h('p', { class: 'tb-cat-desc' }, cat.description) : null,\n ]),\n h('label', { class: 'tb-switch' }, [\n h('input', {\n id,\n type: 'checkbox',\n checked,\n disabled: cat.required,\n onChange: (e: Event) => {\n const next = (e.target as HTMLInputElement).checked\n if (next) api.grant(cat.id)\n else api.deny(cat.id)\n },\n }),\n h('span', { class: 'tb-switch-track' }, [\n h('span', { class: 'tb-switch-thumb' }),\n ]),\n ]),\n ])\n }),\n ),\n h('div', { class: 'tb-modal-foot' }, [\n h(\n 'button',\n {\n type: 'button',\n class: 'tb-btn tb-btn-secondary',\n onClick: () => api.denyAll(),\n },\n copy.rejectLabel,\n ),\n h(\n 'button',\n {\n type: 'button',\n class: 'tb-btn tb-btn-primary',\n onClick: () => api.save(),\n },\n copy.saveLabel,\n ),\n ]),\n ],\n ),\n ],\n )\n}\n","import type { ConsentApi } from '@tickboxhq/vue'\nimport { ConsentNotice } from '@tickboxhq/vue'\nimport { type PropType, defineComponent, h, onMounted } from 'vue'\nimport { DEFAULT_NOTICE_COPY, type NoticeCopy } from '../shared/copy.js'\nimport { injectStyles } from '../shared/styles.js'\n\nexport type ConsentNoticeDefaultProps = {\n copy?: Partial<NoticeCopy>\n policyUrl?: string\n optOutCategoryId?: string\n theme?: 'light' | 'dark'\n}\n\n/**\n * Drop-in styled notice card for sites with only `notice`-mode categories\n * (typically UK DUAA-exempt analytics). Bottom-right toast with\n * \"Got it\" / \"Opt out\" actions.\n */\nexport const ConsentNoticeDefault = defineComponent({\n name: 'ConsentNoticeDefault',\n props: {\n copy: { type: Object as PropType<Partial<NoticeCopy>>, default: () => ({}) },\n policyUrl: { type: String as PropType<string | undefined>, default: undefined },\n optOutCategoryId: { type: String, default: 'analytics' },\n theme: { type: String as PropType<'light' | 'dark' | undefined>, default: undefined },\n },\n setup(props) {\n onMounted(() => injectStyles())\n return () =>\n h(ConsentNotice, null, {\n default: (api: unknown) => renderNotice(api as ConsentApi, props),\n })\n },\n})\n\nfunction renderNotice(api: ConsentApi, props: ConsentNoticeDefaultProps) {\n const copy: NoticeCopy = { ...DEFAULT_NOTICE_COPY, ...(props.copy ?? {}) }\n const optOutId = props.optOutCategoryId ?? 'analytics'\n\n const themeAttrs = props.theme ? { 'data-tb-theme': props.theme } : {}\n\n return h(\n 'div',\n {\n class: 'tb-root tb-notice',\n role: 'status',\n 'aria-live': 'polite',\n 'aria-label': copy.title,\n ...themeAttrs,\n },\n [\n h('p', { class: 'tb-notice-title' }, copy.title),\n h('p', { class: 'tb-notice-desc' }, copy.description),\n h('div', { class: 'tb-notice-actions' }, [\n props.policyUrl\n ? h('a', { class: 'tb-link', href: props.policyUrl }, copy.policyLinkLabel)\n : null,\n h(\n 'button',\n {\n type: 'button',\n class: 'tb-btn tb-btn-secondary',\n onClick: () => {\n if (api.resolved.value.some((r) => r.id === optOutId)) {\n api.deny(optOutId)\n }\n api.save()\n },\n },\n copy.optOutLabel,\n ),\n h(\n 'button',\n {\n type: 'button',\n class: 'tb-btn tb-btn-primary',\n onClick: () => api.save(),\n },\n copy.acknowledgeLabel,\n ),\n ]),\n ],\n )\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@tickboxhq/banner-default",
3
+ "version": "0.0.12",
4
+ "description": "Drop-in styled consent banner and notice components for Tickbox",
5
+ "license": "MIT",
6
+ "homepage": "https://tickbox.dev",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/tickboxhq/tickbox.git",
10
+ "directory": "packages/banner-default"
11
+ },
12
+ "type": "module",
13
+ "exports": {
14
+ "./react": {
15
+ "types": "./dist/react/index.d.ts",
16
+ "import": "./dist/react/index.js"
17
+ },
18
+ "./vue": {
19
+ "types": "./dist/vue/index.d.ts",
20
+ "import": "./dist/vue/index.js"
21
+ }
22
+ },
23
+ "sideEffects": false,
24
+ "files": [
25
+ "dist",
26
+ "README.md"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "dependencies": {
32
+ "@tickboxhq/core": "0.0.12"
33
+ },
34
+ "peerDependencies": {
35
+ "react": "^18.0.0 || ^19.0.0",
36
+ "vue": "^3.4.0",
37
+ "@tickboxhq/react": "0.0.12",
38
+ "@tickboxhq/vue": "0.0.12"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "@tickboxhq/react": {
42
+ "optional": true
43
+ },
44
+ "@tickboxhq/vue": {
45
+ "optional": true
46
+ },
47
+ "react": {
48
+ "optional": true
49
+ },
50
+ "vue": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "devDependencies": {
55
+ "@testing-library/react": "^16.1.0",
56
+ "@types/react": "^19.0.2",
57
+ "@vue/test-utils": "^2.4.6",
58
+ "happy-dom": "^15.11.7",
59
+ "react": "^19.0.0",
60
+ "tsup": "^8.3.5",
61
+ "typescript": "^5.7.2",
62
+ "vitest": "^2.1.8",
63
+ "vue": "^3.5.13",
64
+ "@tickboxhq/react": "0.0.12",
65
+ "@tickboxhq/vue": "0.0.12"
66
+ },
67
+ "scripts": {
68
+ "build": "tsup",
69
+ "test": "vitest run",
70
+ "test:watch": "vitest",
71
+ "typecheck": "tsc --noEmit"
72
+ }
73
+ }