@c15t/react 3.0.0-alpha.3 → 3.0.0-alpha.5

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 (219) hide show
  1. package/AGENTS.md +92 -72
  2. package/README.md +21 -17
  3. package/SKILL.md +34 -0
  4. package/dist/component-hooks/use-headless-consent-ui.js +1 -1
  5. package/dist/component-hooks/use-headless-iab-consent-ui.js +1 -1
  6. package/dist/components/consent-gate/atoms.js +1 -1
  7. package/dist/components/consent-gate/consent-gate.js +1 -1
  8. package/dist/components/iab-panel/atoms/card.js +1 -1
  9. package/dist/components/iab-panel/atoms/feature-item.js +2 -0
  10. package/dist/components/iab-panel/atoms/header.js +1 -1
  11. package/dist/components/iab-panel/iab-panel.js +1 -1
  12. package/dist/components/iab-panel/index.js +1 -1
  13. package/dist/components/iab-prompt/atoms/overlay.js +1 -1
  14. package/dist/components/iab-prompt/atoms/root.js +1 -1
  15. package/dist/components/panel/atoms/card.js +1 -1
  16. package/dist/components/panel/atoms/root.js +1 -1
  17. package/dist/components/panel-trigger/atoms/button.js +1 -1
  18. package/dist/components/panel-trigger/atoms/toolbar.js +1 -1
  19. package/dist/components/preferences/atoms/accordion.js +1 -1
  20. package/dist/components/prompt/atoms/overlay.js +1 -1
  21. package/dist/components/prompt/atoms/root.js +1 -1
  22. package/dist/components/prompt/components.js +1 -1
  23. package/dist/components/shared/libs/compose-refs.js +1 -0
  24. package/dist/components/shared/libs/slot.js +1 -1
  25. package/dist/components/shared/primitives/button.js +1 -1
  26. package/dist/components/shared/ui/dialog/dialog.js +1 -1
  27. package/dist/components/shared/ui/icon.js +1 -1
  28. package/dist/context/consent-tracking-context.js +1 -1
  29. package/dist/devtools.js +1 -1
  30. package/dist/draft-context.js +2 -0
  31. package/dist/draft.js +1 -1
  32. package/dist/external-iab-context.js +1 -1
  33. package/dist/gpp.js +2 -0
  34. package/dist/hooks/use-color-scheme.js +1 -1
  35. package/dist/hooks/use-focus-trap.js +1 -1
  36. package/dist/hooks.js +1 -1
  37. package/dist/iab/styles.tw3.css +4 -4
  38. package/dist/index-parts/consent-banner.js +1 -0
  39. package/dist/index-parts/consent-dialog-link.js +1 -0
  40. package/dist/index-parts/consent-dialog-trigger.js +1 -0
  41. package/dist/index-parts/consent-dialog.js +1 -0
  42. package/dist/index-parts/consent-gate.js +1 -0
  43. package/dist/index-parts/core.js +1 -0
  44. package/dist/index-parts/draft.js +1 -0
  45. package/dist/index-parts/hooks.js +1 -0
  46. package/dist/index-parts/module-hooks.js +1 -0
  47. package/dist/index-parts/provider.js +1 -0
  48. package/dist/index-parts/theme.js +1 -0
  49. package/dist/index-parts/translations.js +1 -0
  50. package/dist/index.js +1 -1
  51. package/dist/kernel-selector.js +1 -1
  52. package/dist/module-hooks/iframe-blocker.js +1 -1
  53. package/dist/postcss-tailwind3.js +1 -0
  54. package/dist/provider.js +1 -1
  55. package/dist/server/fetch-ssr-data.js +1 -1
  56. package/dist/server/headers.js +1 -1
  57. package/dist/server/normalize-url.js +1 -1
  58. package/dist/styles.tw3.css +16 -16
  59. package/dist/transports/offline.js +1 -1
  60. package/dist/utils/merge-slot-props.js +1 -1
  61. package/dist/utils/theme-utils.js +1 -1
  62. package/dist/version.js +1 -1
  63. package/dist-types/component-hooks/use-headless-consent-ui.d.ts +1 -1
  64. package/dist-types/components/iab-panel/atoms/feature-item.d.ts +28 -0
  65. package/dist-types/components/iab-panel/iab-panel.d.ts +2 -2
  66. package/dist-types/components/iab-panel/index.d.ts +5 -2
  67. package/dist-types/components/iab-panel/use-iab-translations.d.ts +4 -0
  68. package/dist-types/components/iab-prompt/iab-prompt.d.ts +2 -2
  69. package/dist-types/components/iab-prompt/index.d.ts +2 -2
  70. package/dist-types/components/panel/atoms/card.d.ts +1 -0
  71. package/dist-types/components/prompt/components.d.ts +6 -0
  72. package/dist-types/components/shared/libs/compose-refs.d.ts +20 -0
  73. package/dist-types/components/shared/ui/icon.d.ts +2 -1
  74. package/dist-types/context/consent-tracking-context.d.ts +10 -0
  75. package/dist-types/context/theme-context.d.ts +2 -2
  76. package/dist-types/context.d.ts +6 -0
  77. package/dist-types/devtools.d.ts +13 -2
  78. package/dist-types/draft-context.d.ts +31 -0
  79. package/dist-types/draft.d.ts +13 -30
  80. package/dist-types/gpp.d.ts +27 -0
  81. package/dist-types/hooks/use-focus-trap.d.ts +3 -1
  82. package/dist-types/hooks.d.ts +57 -13
  83. package/dist-types/iab.d.ts +1 -1
  84. package/dist-types/index-parts/consent-banner.d.ts +7 -0
  85. package/dist-types/index-parts/consent-dialog-link.d.ts +7 -0
  86. package/dist-types/index-parts/consent-dialog-trigger.d.ts +7 -0
  87. package/dist-types/index-parts/consent-dialog.d.ts +7 -0
  88. package/dist-types/index-parts/consent-gate.d.ts +7 -0
  89. package/dist-types/index-parts/core.d.ts +8 -0
  90. package/dist-types/index-parts/draft.d.ts +7 -0
  91. package/dist-types/index-parts/hooks.d.ts +7 -0
  92. package/dist-types/index-parts/module-hooks.d.ts +7 -0
  93. package/dist-types/index-parts/provider.d.ts +7 -0
  94. package/dist-types/index-parts/theme.d.ts +8 -0
  95. package/dist-types/index-parts/translations.d.ts +7 -0
  96. package/dist-types/index.d.ts +14 -15
  97. package/dist-types/kernel-selector.d.ts +0 -8
  98. package/dist-types/module-hooks/iframe-blocker.d.ts +12 -7
  99. package/dist-types/postcss-tailwind3.d.ts +21 -0
  100. package/dist-types/provider.d.ts +57 -17
  101. package/dist-types/server/headers.d.ts +14 -4
  102. package/dist-types/server/normalize-url.d.ts +14 -7
  103. package/dist-types/server/types.d.ts +12 -1
  104. package/dist-types/transports/offline.d.ts +7 -16
  105. package/dist-types/ui-config-context.d.ts +7 -1
  106. package/dist-types/utils/merge-slot-props.d.ts +15 -1
  107. package/dist-types/utils/theme-utils.d.ts +17 -2
  108. package/dist-types/version.d.ts +1 -1
  109. package/docs/README.md +92 -72
  110. package/docs/assets/v3/bottom-bar.png +0 -0
  111. package/docs/assets/v3/brand-card.png +0 -0
  112. package/docs/assets/v3/brand-preferences.png +0 -0
  113. package/docs/assets/v3/choice-wall.png +0 -0
  114. package/docs/assets/v3/headless-bar-html.png +0 -0
  115. package/docs/assets/v3/headless-bar-mobile.png +0 -0
  116. package/docs/assets/v3/headless-bar.png +0 -0
  117. package/docs/assets/v3/slim-bar.png +0 -0
  118. package/docs/concepts/choose-your-setup.md +87 -0
  119. package/docs/{frameworks/react/concepts → concepts}/consent-categories.md +22 -7
  120. package/docs/{guides → concepts}/consent-state.md +89 -105
  121. package/docs/{guides → concepts}/data-fetching.md +31 -27
  122. package/docs/concepts/how-consent-works.md +123 -0
  123. package/docs/{frameworks/react/concepts/policy-presets.md → concepts/policies.md} +7 -59
  124. package/docs/customization/class-names.md +202 -0
  125. package/docs/customization/dark-mode.md +157 -0
  126. package/docs/customization/motion.md +119 -0
  127. package/docs/customization/overview.md +67 -34
  128. package/docs/customization/recipes.md +839 -49
  129. package/docs/customization/slots.md +216 -35
  130. package/docs/customization/stylesheets.md +147 -0
  131. package/docs/customization/tailwind.md +842 -0
  132. package/docs/customization/tokens.md +163 -96
  133. package/docs/customization/translations.md +60 -3
  134. package/docs/frameworks/react/callbacks.md +118 -0
  135. package/docs/frameworks/react/clear-on-revocation.md +106 -0
  136. package/docs/frameworks/react/components/consent-banner.md +42 -28
  137. package/docs/frameworks/react/components/consent-dialog-link.md +13 -11
  138. package/docs/frameworks/react/components/consent-dialog-trigger.md +81 -76
  139. package/docs/frameworks/react/components/consent-dialog.md +56 -50
  140. package/docs/frameworks/react/components/consent-gate.md +37 -20
  141. package/docs/frameworks/react/components/consent-provider.md +130 -0
  142. package/docs/frameworks/react/components/consent-widget.md +23 -20
  143. package/docs/frameworks/react/components/dev-tools.md +53 -85
  144. package/docs/frameworks/react/components.md +105 -0
  145. package/docs/frameworks/react/compose.md +354 -0
  146. package/docs/frameworks/react/content-security-policy.md +126 -0
  147. package/docs/frameworks/react/customize.md +250 -0
  148. package/docs/frameworks/react/embeds.md +89 -0
  149. package/docs/frameworks/react/gpp.md +163 -0
  150. package/docs/frameworks/react/{hooks/overview.md → hooks.md} +21 -16
  151. package/docs/frameworks/react/{iab/overview.md → iab.md} +80 -31
  152. package/docs/frameworks/react/network-blocker.md +35 -41
  153. package/docs/frameworks/react/quickstart.md +139 -101
  154. package/docs/frameworks/react/rendering.md +128 -0
  155. package/docs/frameworks/react/scripts.md +115 -0
  156. package/docs/frameworks/react/translations.md +155 -0
  157. package/docs/frameworks/react/troubleshooting.md +92 -47
  158. package/docs/frameworks/react/vendor-consent.md +170 -0
  159. package/docs/guides/banner-experiments.md +654 -0
  160. package/docs/guides/troubleshooting.md +120 -47
  161. package/docs/guides/verify-consent.md +81 -49
  162. package/docs/integrations/adobe-analytics.md +167 -159
  163. package/docs/integrations/ahrefs-analytics.md +152 -154
  164. package/docs/integrations/amplitude.md +162 -156
  165. package/docs/integrations/building-integrations.md +136 -37
  166. package/docs/integrations/clearbit.md +154 -154
  167. package/docs/integrations/cloudflare-web-analytics.md +156 -156
  168. package/docs/integrations/cloudflare-zaraz.md +209 -261
  169. package/docs/integrations/crisp.md +164 -158
  170. package/docs/integrations/databuddy.md +157 -173
  171. package/docs/integrations/fathom-analytics.md +158 -156
  172. package/docs/integrations/front-chat.md +167 -167
  173. package/docs/integrations/google-maps.md +118 -83
  174. package/docs/integrations/google-tag-manager.md +178 -163
  175. package/docs/integrations/google-tag.md +163 -160
  176. package/docs/integrations/heap.md +163 -155
  177. package/docs/integrations/hightouch.md +161 -157
  178. package/docs/integrations/hotjar.md +159 -155
  179. package/docs/integrations/intercom.md +183 -153
  180. package/docs/integrations/klaviyo.md +486 -0
  181. package/docs/integrations/linkedin-insights.md +174 -150
  182. package/docs/integrations/logrocket.md +160 -156
  183. package/docs/integrations/matomo-analytics.md +188 -178
  184. package/docs/integrations/meta-pixel.md +188 -150
  185. package/docs/integrations/microsoft-clarity.md +163 -155
  186. package/docs/integrations/microsoft-uet.md +148 -154
  187. package/docs/integrations/mixpanel-analytics.md +155 -160
  188. package/docs/integrations/one-dollar-stats.md +166 -165
  189. package/docs/integrations/openai-pixel.md +204 -301
  190. package/docs/integrations/overview.md +137 -80
  191. package/docs/integrations/pinterest-tag.md +191 -183
  192. package/docs/integrations/pirsch.md +169 -159
  193. package/docs/integrations/plausible-analytics.md +172 -158
  194. package/docs/integrations/posthog.md +313 -240
  195. package/docs/integrations/promptwatch.md +154 -154
  196. package/docs/integrations/reddit-pixel.md +185 -157
  197. package/docs/integrations/rudderstack.md +201 -186
  198. package/docs/integrations/rybbit-analytics.md +171 -160
  199. package/docs/integrations/segment.md +182 -154
  200. package/docs/integrations/snapchat-pixel.md +186 -156
  201. package/docs/integrations/tiktok-pixel.md +171 -150
  202. package/docs/integrations/umami-analytics.md +163 -158
  203. package/docs/integrations/vercel-analytics.md +166 -157
  204. package/docs/integrations/x-pixel.md +176 -150
  205. package/docs/integrations/youtube.md +121 -86
  206. package/docs/upgrade-v3.md +496 -467
  207. package/package.json +20 -9
  208. package/dist/ui-save.js +0 -1
  209. package/dist-types/ui-save.d.ts +0 -25
  210. package/docs/assets/v3/brand-bar.png +0 -0
  211. package/docs/assets/v3/mobile-card.png +0 -0
  212. package/docs/assets/v3/preferences.png +0 -0
  213. package/docs/frameworks/react/components/consent-manager-provider.md +0 -95
  214. package/docs/frameworks/react/script-loader.md +0 -69
  215. package/docs/frameworks/react/styling/overview.md +0 -266
  216. package/docs/guides/deployment-modes.md +0 -75
  217. package/docs/guides/shared-consent-controls.md +0 -158
  218. package/docs/integrations/clear-on-revocation.md +0 -167
  219. package/docs/integrations/granular-consent.md +0 -210
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  title: Google Maps
3
- description: Prevent a map iframe from mounting before the required permission.
3
+ description: Gate a Google Maps iframe embed with c15t v3 so the map loads only
4
+ after the visitor allows its consent category, in Next.js, TanStack Start,
5
+ React, Nuxt, Vue, Astro, Svelte, SvelteKit, HTML or JavaScript.
6
+ icon: google-maps
4
7
  group: integrations
5
8
  ---
6
9
 
@@ -11,7 +14,8 @@ application; this example uses functionality permission.
11
14
 
12
15
  ```ts title="src/embed-config.ts"
13
16
  export const embedCategory = 'functionality' as const;
14
- export const embedURL = 'https://www.google.com/maps/embed?pb=YOUR_EMBED_PARAMETERS';
17
+ export const embedURL =
18
+ 'https://www.google.com/maps/embed?pb=YOUR_EMBED_PARAMETERS';
15
19
  export const embedTitle = 'Map showing our office location';
16
20
  export const embedAspectRatio = '4 / 3';
17
21
  ```
@@ -107,64 +111,66 @@ on revocation. Keep your existing consent styles and preferences dialog.
107
111
 
108
112
  **Nuxt**
109
113
 
110
- Use the reactive snapshot from the existing consent runtime. The Nuxt module auto-imports the consent composables.
114
+ The Nuxt module registers `ConsentGate` and `ConsentPreferencesLink`, so
115
+ this component imports only the embed configuration.
111
116
 
112
117
  ```vue title="app/components/ConsentEmbed.vue"
113
118
  <script setup lang="ts">
114
119
  import { embedCategory, embedURL, embedTitle, embedAspectRatio } from '../../src/embed-config';
115
-
116
- const snapshot = useConsentSnapshot();
117
- const activeUI = useConsentActiveUI();
118
120
  </script>
119
121
 
120
122
  <template>
121
- <iframe
122
- v-if="snapshot.effectivePermissions[embedCategory]"
123
- :src="embedURL"
124
- :title="embedTitle"
125
- loading="lazy"
126
- allowfullscreen
127
- :style="{ width: '100%', aspectRatio: embedAspectRatio, minHeight: '200px', border: 0 }"
128
- />
129
- <button v-else type="button" @click="activeUI = 'manager'">
130
- Open privacy settings to view this content
131
- </button>
123
+ <ConsentGate :category="embedCategory">
124
+ <iframe
125
+ :src="embedURL"
126
+ :title="embedTitle"
127
+ loading="lazy"
128
+ allowfullscreen
129
+ :style="{ width: '100%', aspectRatio: embedAspectRatio, minHeight: '200px', border: 0 }"
130
+ />
131
+ <template #placeholder>
132
+ <p>Allow {{ embedCategory }} to load this content.</p>
133
+ <ConsentPreferencesLink>Open privacy settings</ConsentPreferencesLink>
134
+ </template>
135
+ </ConsentGate>
132
136
  </template>
133
137
  ```
134
138
 
135
- Use `v-if` so a denied iframe is removed from the DOM. Hiding an existing
136
- iframe with `v-show` or CSS does not prevent its requests.
139
+ `ConsentGate` keeps the iframe out of the server HTML and the DOM while
140
+ permission is denied, and removes it on revocation. See
141
+ [Nuxt scripts and embeds](https://c15t.com/docs/frameworks/nuxt/scripts).
137
142
 
138
143
  **Vue**
139
144
 
140
- Use the reactive snapshot from the existing consent runtime. The c15t Vue plugin must already be installed on this app.
145
+ Render this component inside the app that installed the c15t Vue plugin.
141
146
 
142
147
  ```vue title="src/ConsentEmbed.vue"
143
148
  <script setup lang="ts">
144
- import { useConsentSnapshot, useConsentActiveUI } from 'c15t/vue/vue-plugin';
149
+ import ConsentGate from 'c15t/vue/runtime/components/consent-gate.vue';
150
+ import ConsentPreferencesLink from 'c15t/vue/runtime/components/consent-preferences-link.vue';
145
151
  import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
146
-
147
- const snapshot = useConsentSnapshot();
148
- const activeUI = useConsentActiveUI();
149
152
  </script>
150
153
 
151
154
  <template>
152
- <iframe
153
- v-if="snapshot.effectivePermissions[embedCategory]"
154
- :src="embedURL"
155
- :title="embedTitle"
156
- loading="lazy"
157
- allowfullscreen
158
- :style="{ width: '100%', aspectRatio: embedAspectRatio, minHeight: '200px', border: 0 }"
159
- />
160
- <button v-else type="button" @click="activeUI = 'manager'">
161
- Open privacy settings to view this content
162
- </button>
155
+ <ConsentGate :category="embedCategory">
156
+ <iframe
157
+ :src="embedURL"
158
+ :title="embedTitle"
159
+ loading="lazy"
160
+ allowfullscreen
161
+ :style="{ width: '100%', aspectRatio: embedAspectRatio, minHeight: '200px', border: 0 }"
162
+ />
163
+ <template #placeholder>
164
+ <p>Allow {{ embedCategory }} to load this content.</p>
165
+ <ConsentPreferencesLink>Open privacy settings</ConsentPreferencesLink>
166
+ </template>
167
+ </ConsentGate>
163
168
  </template>
164
169
  ```
165
170
 
166
- Use `v-if` so a denied iframe is removed from the DOM. Hiding an existing
167
- iframe with `v-show` or CSS does not prevent its requests.
171
+ `ConsentGate` keeps the iframe out of the DOM while permission is denied,
172
+ and removes it on revocation. See
173
+ [Vue scripts and embeds](https://c15t.com/docs/frameworks/vue/scripts).
168
174
 
169
175
  **Astro**
170
176
 
@@ -273,16 +279,37 @@ The Svelte `ConsentGate` waits until the browser is mounted and the category is
273
279
  allowed. Its default placeholder opens preferences. Revocation removes the
274
280
  iframe.
275
281
 
282
+ **HTML**
283
+
284
+ The c15t script tag gates iframes that name a category. Put the embed's URL
285
+ in `data-src` instead of `src`, using the values from the configuration on
286
+ this page:
287
+
288
+ ```html
289
+ <iframe
290
+ data-src="https://www.youtube-nocookie.com/embed/VIDEO_ID?playsinline=1"
291
+ data-category="measurement"
292
+ title="Product video"
293
+ loading="lazy"
294
+ allowfullscreen
295
+ ></iframe>
296
+ ```
297
+
298
+ c15t sets `src` once the category is allowed and removes it on revocation.
299
+ Without `src` the iframe loads nothing, so hide it with CSS and show a link
300
+ to `#c15t-preferences` in its place. See
301
+ [HTML embeds](https://c15t.com/docs/frameworks/html/embeds).
302
+
276
303
  **JavaScript**
277
304
 
278
- Use the shared browser helper below with your existing kernel. Put an
305
+ Use the shared browser helper below with your existing client. Put an
279
306
  empty container where the embed should appear:
280
307
 
281
308
  ```html
282
309
  <div id="consent-embed"></div>
283
310
  ```
284
311
 
285
- In your browser entry point, after creating the kernel:
312
+ In your browser entry point, after `init()`:
286
313
 
287
314
  ```ts
288
315
  import { mountConsentEmbed } from './consent-embed';
@@ -290,13 +317,16 @@ import { mountConsentEmbed } from './consent-embed';
290
317
  const container = document.querySelector<HTMLElement>('#consent-embed');
291
318
  if (!container) throw new Error('Missing consent embed container');
292
319
 
293
- const disposeEmbed = mountConsentEmbed(container, kernel, openPreferences);
320
+ const disposeEmbed = mountConsentEmbed(container, consent.kernel, () =>
321
+ consent.openDialog(),
322
+ );
294
323
  ```
295
324
 
296
- `kernel` is the instance from your quickstart. `openPreferences` is your
297
- application's function for showing its consent preferences UI. Call
298
- `disposeEmbed()` when the page or component is destroyed. The helper observes
299
- both the current snapshot and later changes.
325
+ `consent` is the client from `init()` in your quickstart. With
326
+ `createConsentRuntime`, pass `runtime.kernel` and your own function that
327
+ opens preferences. Call `disposeEmbed()` when the page or component is
328
+ destroyed. Iframe markup with `data-src` and `data-category` also works
329
+ without the helper, because both setups gate iframes by default.
300
330
 
301
331
  ## Browser helper for Astro and JavaScript
302
332
 
@@ -305,51 +335,56 @@ when permission allows it, keeps an existing player mounted across unrelated
305
335
  snapshot updates, and removes it on revocation.
306
336
 
307
337
  ```ts title="src/consent-embed.ts"
308
- import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
338
+ import {
339
+ embedCategory,
340
+ embedURL,
341
+ embedTitle,
342
+ embedAspectRatio,
343
+ } from './embed-config';
309
344
 
310
345
  type EmbedKernel = {
311
- getSnapshot: () => {
312
- effectivePermissions: Record<typeof embedCategory, boolean>;
313
- };
314
- subscribe: (listener: () => void) => () => void;
346
+ getSnapshot: () => {
347
+ effectivePermissions: Record<typeof embedCategory, boolean>;
348
+ };
349
+ subscribe: (listener: () => void) => () => void;
315
350
  };
316
351
 
317
352
  export function mountConsentEmbed(
318
- container: HTMLElement,
319
- kernel: EmbedKernel,
320
- openPreferences: () => void,
353
+ container: HTMLElement,
354
+ kernel: EmbedKernel,
355
+ openPreferences: () => void
321
356
  ) {
322
- const render = () => {
323
- if (kernel.getSnapshot().effectivePermissions[embedCategory]) {
324
- if (container.querySelector('iframe')) return;
325
- const frame = document.createElement('iframe');
326
- frame.src = embedURL;
327
- frame.title = embedTitle;
328
- frame.loading = 'lazy';
329
- frame.allowFullscreen = true;
330
- Object.assign(frame.style, {
331
- width: '100%',
332
- aspectRatio: embedAspectRatio,
333
- minHeight: '200px',
334
- border: '0',
335
- });
336
- container.replaceChildren(frame);
337
- } else {
338
- if (container.querySelector('button')) return;
339
- const button = document.createElement('button');
340
- button.type = 'button';
341
- button.textContent = 'Open privacy settings to view this content';
342
- button.onclick = openPreferences;
343
- container.replaceChildren(button);
344
- }
345
- };
346
-
347
- render();
348
- const unsubscribe = kernel.subscribe(render);
349
- return () => {
350
- unsubscribe();
351
- container.replaceChildren();
352
- };
357
+ const render = () => {
358
+ if (kernel.getSnapshot().effectivePermissions[embedCategory]) {
359
+ if (container.querySelector('iframe')) return;
360
+ const frame = document.createElement('iframe');
361
+ frame.src = embedURL;
362
+ frame.title = embedTitle;
363
+ frame.loading = 'lazy';
364
+ frame.allowFullscreen = true;
365
+ Object.assign(frame.style, {
366
+ width: '100%',
367
+ aspectRatio: embedAspectRatio,
368
+ minHeight: '200px',
369
+ border: '0',
370
+ });
371
+ container.replaceChildren(frame);
372
+ } else {
373
+ if (container.querySelector('button')) return;
374
+ const button = document.createElement('button');
375
+ button.type = 'button';
376
+ button.textContent = 'Open privacy settings to view this content';
377
+ button.onclick = openPreferences;
378
+ container.replaceChildren(button);
379
+ }
380
+ };
381
+
382
+ render();
383
+ const unsubscribe = kernel.subscribe(render);
384
+ return () => {
385
+ unsubscribe();
386
+ container.replaceChildren();
387
+ };
353
388
  }
354
389
  ```
355
390