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

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 (241) hide show
  1. package/AGENTS.md +9 -2
  2. package/dist/aggregate-components.js +1 -1
  3. package/dist/chunk-warming.js +1 -1
  4. package/dist/component-hooks/use-headless-iab-consent-ui.js +1 -1
  5. package/dist/component-hooks/use-panel-trigger.js +1 -1
  6. package/dist/components/consent-gate/atoms.js +1 -0
  7. package/dist/components/consent-gate/consent-gate.js +2 -0
  8. package/dist/components/consent-gate/index.js +2 -0
  9. package/dist/components/consent-preferences-link/consent-preferences-link.js +1 -1
  10. package/dist/components/iab-panel/atoms/purpose-item.js +1 -1
  11. package/dist/components/iab-panel/atoms/stack-item.js +1 -1
  12. package/dist/components/iab-panel/iab-panel.js +1 -1
  13. package/dist/components/iab-panel/index.js +1 -1
  14. package/dist/components/panel/atoms/overlay.js +1 -1
  15. package/dist/components/panel/atoms/root.js +1 -1
  16. package/dist/components/panel/index.js +1 -1
  17. package/dist/components/panel/panel.js +1 -1
  18. package/dist/components/panel-trigger/atoms/button.js +1 -1
  19. package/dist/components/panel-trigger/atoms/icon.js +1 -1
  20. package/dist/components/panel-trigger/atoms/root.js +1 -1
  21. package/dist/components/panel-trigger/atoms/trigger-icons.js +1 -0
  22. package/dist/components/panel-trigger/panel-trigger-toolbar.js +1 -1
  23. package/dist/components/preferences/atoms/accordion.js +1 -1
  24. package/dist/components/preferences/atoms/root.js +1 -1
  25. package/dist/components/preferences/atoms/vendor-list.js +1 -0
  26. package/dist/components/preferences/index.js +1 -1
  27. package/dist/components/prompt/policy-actions.js +1 -1
  28. package/dist/components/shared/primitives/button.js +1 -1
  29. package/dist/components/shared/ui/accordion/accordion.js +1 -1
  30. package/dist/components/shared/ui/collapsible/collapsible.js +1 -1
  31. package/dist/components/shared/ui/logo.js +1 -1
  32. package/dist/components/shared/ui/preference-item/preference-item.js +1 -1
  33. package/dist/components/shared/ui/switch/switch.js +1 -1
  34. package/dist/components/shared/ui/tabs/tabs.js +1 -1
  35. package/dist/consent-gate.js +2 -0
  36. package/dist/consent-theme.js +1 -0
  37. package/dist/draft.js +1 -1
  38. package/dist/external-iab-context.js +1 -1
  39. package/dist/headless.js +1 -1
  40. package/dist/hooks/use-committed-ref.js +2 -0
  41. package/dist/hooks.js +1 -1
  42. package/dist/iab/styles.tw3.css +1 -41
  43. package/dist/iab-context.js +1 -1
  44. package/dist/index.js +1 -1
  45. package/dist/kernel-selector.js +1 -0
  46. package/dist/module-hooks/iframe-blocker.js +1 -1
  47. package/dist/module-hooks/network-blocker.js +1 -1
  48. package/dist/module-hooks/network-hold.js +2 -0
  49. package/dist/module-hooks/persistence.js +1 -1
  50. package/dist/panel.js +1 -1
  51. package/dist/preferences.js +1 -2
  52. package/dist/provider.js +1 -1
  53. package/dist/styles.tw3.css +1 -43
  54. package/dist/ui-save.js +1 -1
  55. package/dist/utils/deferred-module.js +2 -0
  56. package/dist/version.js +1 -1
  57. package/dist-types/aggregate-components.d.ts +2 -2
  58. package/dist-types/chunk-warming.d.ts +41 -8
  59. package/dist-types/component-hooks/use-headless-iab-consent-ui.d.ts +2 -2
  60. package/dist-types/components/consent-gate/atoms.d.ts +11 -0
  61. package/dist-types/components/consent-gate/consent-gate.d.ts +2 -0
  62. package/dist-types/components/consent-gate/index.d.ts +41 -0
  63. package/dist-types/components/{frame → consent-gate}/types.d.ts +7 -7
  64. package/dist-types/components/consent-preferences-link/consent-preferences-link.d.ts +1 -1
  65. package/dist-types/components/consent-preferences-link/index.d.ts +1 -1
  66. package/dist-types/components/iab-panel/atoms/purpose-item.d.ts +5 -2
  67. package/dist-types/components/iab-panel/atoms/stack-item.d.ts +7 -2
  68. package/dist-types/components/iab-panel/atoms/vendor-list.d.ts +1 -1
  69. package/dist-types/components/iab-panel/iab-panel.d.ts +1 -1
  70. package/dist-types/components/iab-panel/index.d.ts +25 -24
  71. package/dist-types/components/iab-prompt/index.d.ts +17 -17
  72. package/dist-types/components/panel/atoms/card.d.ts +4 -4
  73. package/dist-types/components/panel/atoms/overlay.d.ts +1 -1
  74. package/dist-types/components/panel/index.d.ts +8 -7
  75. package/dist-types/components/panel/panel.d.ts +3 -3
  76. package/dist-types/components/panel-link/index.d.ts +1 -1
  77. package/dist-types/components/panel-trigger/atoms/button.d.ts +1 -1
  78. package/dist-types/components/panel-trigger/atoms/icon.d.ts +1 -1
  79. package/dist-types/components/panel-trigger/atoms/root.d.ts +2 -2
  80. package/dist-types/components/panel-trigger/atoms/toolbar.d.ts +2 -2
  81. package/dist-types/components/panel-trigger/atoms/trigger-icons.d.ts +20 -0
  82. package/dist-types/components/panel-trigger/index.d.ts +4 -4
  83. package/dist-types/components/panel-trigger/panel-trigger-toolbar.d.ts +1 -1
  84. package/dist-types/components/panel-trigger/panel-trigger.d.ts +9 -9
  85. package/dist-types/components/panel-trigger/types.d.ts +1 -1
  86. package/dist-types/components/preferences/atoms/accordion.d.ts +6 -4
  87. package/dist-types/components/preferences/atoms/button.d.ts +1 -1
  88. package/dist-types/components/preferences/atoms/footer.d.ts +1 -1
  89. package/dist-types/components/preferences/atoms/root.d.ts +1 -1
  90. package/dist-types/components/preferences/atoms/vendor-list.d.ts +23 -0
  91. package/dist-types/components/preferences/index.d.ts +14 -11
  92. package/dist-types/components/preferences/policy-actions.d.ts +2 -2
  93. package/dist-types/components/preferences/preferences.d.ts +1 -1
  94. package/dist-types/components/preferences/types.d.ts +1 -1
  95. package/dist-types/components/prompt/components.d.ts +3 -3
  96. package/dist-types/components/prompt/index.d.ts +11 -11
  97. package/dist-types/components/prompt/policy-actions.d.ts +2 -2
  98. package/dist-types/components/prompt/prompt.d.ts +1 -1
  99. package/dist-types/components/shared/policy-actions.d.ts +2 -2
  100. package/dist-types/components/shared/primitives/box.d.ts +1 -1
  101. package/dist-types/components/shared/primitives/button.d.ts +2 -2
  102. package/dist-types/components/shared/primitives/button.types.d.ts +1 -1
  103. package/dist-types/components/shared/ui/accordion/accordion.d.ts +3 -2
  104. package/dist-types/components/shared/ui/accordion/index.d.ts +2 -2
  105. package/dist-types/components/shared/ui/animated-collapse/index.d.ts +2 -2
  106. package/dist-types/components/shared/ui/branding.d.ts +1 -1
  107. package/dist-types/components/shared/ui/button/button.d.ts +1 -1
  108. package/dist-types/components/shared/ui/button/index.d.ts +2 -2
  109. package/dist-types/components/shared/ui/collapsible/collapsible.d.ts +1 -0
  110. package/dist-types/components/shared/ui/collapsible/index.d.ts +1 -1
  111. package/dist-types/components/shared/ui/dialog/index.d.ts +1 -1
  112. package/dist-types/components/shared/ui/logo.d.ts +0 -10
  113. package/dist-types/components/shared/ui/preference-item/index.d.ts +1 -1
  114. package/dist-types/components/shared/ui/preference-item/preference-item.d.ts +14 -0
  115. package/dist-types/components/shared/ui/switch/index.d.ts +2 -2
  116. package/dist-types/components/shared/ui/switch/switch.d.ts +2 -1
  117. package/dist-types/components/shared/ui/tabs/index.d.ts +1 -1
  118. package/dist-types/components/shared/ui/tabs/tabs.d.ts +1 -0
  119. package/dist-types/consent-gate.d.ts +1 -0
  120. package/dist-types/consent-theme.d.ts +38 -0
  121. package/dist-types/context/index.d.ts +3 -3
  122. package/dist-types/context/theme-context.d.ts +1 -1
  123. package/dist-types/draft.d.ts +96 -7
  124. package/dist-types/headless.d.ts +6 -7
  125. package/dist-types/hooks/use-committed-ref.d.ts +24 -0
  126. package/dist-types/hooks/use-headless-iab-consent-ui.d.ts +1 -1
  127. package/dist-types/hooks/use-iab-manager.d.ts +1 -1
  128. package/dist-types/hooks/use-theme.d.ts +1 -1
  129. package/dist-types/hooks.d.ts +30 -1
  130. package/dist-types/iab.d.ts +5 -5
  131. package/dist-types/index.d.ts +26 -25
  132. package/dist-types/kernel-selector.d.ts +66 -0
  133. package/dist-types/module-hooks/iframe-blocker.d.ts +27 -0
  134. package/dist-types/module-hooks/network-blocker.d.ts +23 -0
  135. package/dist-types/module-hooks/network-hold.d.ts +47 -0
  136. package/dist-types/module-hooks.d.ts +4 -4
  137. package/dist-types/panel-link.d.ts +1 -1
  138. package/dist-types/panel-trigger.d.ts +1 -1
  139. package/dist-types/panel.d.ts +13 -1
  140. package/dist-types/preferences.d.ts +12 -1
  141. package/dist-types/primitives/accordion.d.ts +1 -1
  142. package/dist-types/primitives/button.d.ts +1 -1
  143. package/dist-types/primitives/collapsible.d.ts +1 -1
  144. package/dist-types/primitives/dialog.d.ts +1 -1
  145. package/dist-types/primitives/preference-item.d.ts +1 -1
  146. package/dist-types/primitives/switch.d.ts +1 -1
  147. package/dist-types/primitives/tabs.d.ts +1 -1
  148. package/dist-types/primitives.d.ts +7 -7
  149. package/dist-types/prompt.d.ts +1 -1
  150. package/dist-types/provider.d.ts +62 -12
  151. package/dist-types/server/fetch-ssr-data.d.ts +1 -1
  152. package/dist-types/server/index.d.ts +5 -5
  153. package/dist-types/theme-provider.d.ts +2 -2
  154. package/dist-types/types/index.d.ts +3 -3
  155. package/dist-types/types/manager.d.ts +1 -1
  156. package/dist-types/types/theme/index.d.ts +1 -1
  157. package/dist-types/ui-config-context.d.ts +3 -1
  158. package/dist-types/ui-save.d.ts +21 -1
  159. package/dist-types/utils/deferred-module.d.ts +13 -0
  160. package/dist-types/utils/index.d.ts +4 -4
  161. package/dist-types/utils/merge-slot-props.d.ts +2 -2
  162. package/dist-types/utils/merge-styles.d.ts +1 -1
  163. package/dist-types/utils/theme-utils.d.ts +1 -1
  164. package/dist-types/version.d.ts +1 -1
  165. package/docs/README.md +9 -2
  166. package/docs/customization/overview.md +4 -3
  167. package/docs/customization/recipes.md +4 -2
  168. package/docs/customization/tokens.md +66 -3
  169. package/docs/frameworks/react/components/consent-dialog.md +46 -7
  170. package/docs/frameworks/react/components/{frame.md → consent-gate.md} +35 -31
  171. package/docs/frameworks/react/components/consent-manager-provider.md +35 -11
  172. package/docs/frameworks/react/components/consent-widget.md +48 -6
  173. package/docs/frameworks/react/concepts/consent-categories.md +38 -2
  174. package/docs/frameworks/react/hooks/{use-consent-manager/overview.md → overview.md} +7 -5
  175. package/docs/frameworks/react/iab/overview.md +121 -1
  176. package/docs/frameworks/react/network-blocker.md +146 -0
  177. package/docs/frameworks/react/script-loader.md +6 -0
  178. package/docs/frameworks/react/styling/overview.md +71 -22
  179. package/docs/guides/consent-state.md +327 -0
  180. package/docs/guides/deployment-modes.md +12 -0
  181. package/docs/guides/shared-consent-controls.md +158 -0
  182. package/docs/integrations/adobe-analytics.md +1 -1
  183. package/docs/integrations/ahrefs-analytics.md +1 -1
  184. package/docs/integrations/amplitude.md +1 -1
  185. package/docs/integrations/building-integrations.md +5 -0
  186. package/docs/integrations/clearbit.md +1 -1
  187. package/docs/integrations/cloudflare-web-analytics.md +1 -1
  188. package/docs/integrations/cloudflare-zaraz.md +399 -0
  189. package/docs/integrations/crisp.md +1 -1
  190. package/docs/integrations/databuddy.md +1 -1
  191. package/docs/integrations/fathom-analytics.md +1 -1
  192. package/docs/integrations/front-chat.md +322 -0
  193. package/docs/integrations/google-maps.md +21 -21
  194. package/docs/integrations/google-tag-manager.md +1 -1
  195. package/docs/integrations/google-tag.md +1 -1
  196. package/docs/integrations/granular-consent.md +210 -0
  197. package/docs/integrations/heap.md +1 -1
  198. package/docs/integrations/hightouch.md +1 -1
  199. package/docs/integrations/hotjar.md +1 -1
  200. package/docs/integrations/intercom.md +1 -1
  201. package/docs/integrations/linkedin-insights.md +1 -1
  202. package/docs/integrations/logrocket.md +1 -1
  203. package/docs/integrations/matomo-analytics.md +1 -1
  204. package/docs/integrations/meta-pixel.md +1 -1
  205. package/docs/integrations/microsoft-clarity.md +1 -1
  206. package/docs/integrations/microsoft-uet.md +1 -1
  207. package/docs/integrations/mixpanel-analytics.md +1 -1
  208. package/docs/integrations/one-dollar-stats.md +305 -0
  209. package/docs/integrations/openai-pixel.md +1 -1
  210. package/docs/integrations/overview.md +22 -18
  211. package/docs/integrations/pinterest-tag.md +321 -0
  212. package/docs/integrations/pirsch.md +1 -1
  213. package/docs/integrations/plausible-analytics.md +1 -1
  214. package/docs/integrations/posthog.md +1 -1
  215. package/docs/integrations/promptwatch.md +1 -1
  216. package/docs/integrations/reddit-pixel.md +1 -1
  217. package/docs/integrations/rudderstack.md +1 -1
  218. package/docs/integrations/rybbit-analytics.md +1 -1
  219. package/docs/integrations/segment.md +1 -1
  220. package/docs/integrations/snapchat-pixel.md +1 -1
  221. package/docs/integrations/tiktok-pixel.md +1 -1
  222. package/docs/integrations/umami-analytics.md +1 -1
  223. package/docs/integrations/vercel-analytics.md +1 -1
  224. package/docs/integrations/x-pixel.md +1 -1
  225. package/docs/integrations/youtube.md +27 -22
  226. package/docs/upgrade-v3.md +176 -1
  227. package/package.json +26 -16
  228. package/dist/component-hooks/use-manager.js +0 -2
  229. package/dist/components/frame/atoms.js +0 -1
  230. package/dist/components/frame/frame.js +0 -2
  231. package/dist/components/frame/index.js +0 -2
  232. package/dist/frame.js +0 -2
  233. package/dist-types/component-hooks/use-manager.d.ts +0 -40
  234. package/dist-types/components/frame/atoms.d.ts +0 -11
  235. package/dist-types/components/frame/frame.d.ts +0 -2
  236. package/dist-types/components/frame/index.d.ts +0 -24
  237. package/dist-types/frame.d.ts +0 -1
  238. package/readme.json +0 -48
  239. package/src/iab/styles.css +0 -11
  240. package/src/styles.css +0 -10
  241. /package/dist/components/{frame → consent-gate}/types.js +0 -0
@@ -0,0 +1,146 @@
1
+ ---
2
+ title: Block network requests
3
+ description: Block fetch and XHR calls to third-party domains in a React app
4
+ until the visitor grants their consent category.
5
+ group: frameworks
6
+ ---
7
+
8
+ ## Add rules to the provider
9
+
10
+ The network blocker stops `fetch` and `XMLHttpRequest` calls to domains you
11
+ list until the visitor grants their category. Use it for beacons and API
12
+ calls that bypass script loading, such as a pixel fired by an SDK that is
13
+ already on the page.
14
+
15
+ Add `networkBlocker` to the `ConsentProvider` options from your
16
+ [quickstart](./quickstart.md). Set the URL to the endpoint
17
+ supplied by your Inth project.
18
+
19
+ ```tsx title="components/consent.tsx"
20
+ 'use client';
21
+
22
+ import type { ReactNode } from 'react';
23
+ import { ConsentBanner, ConsentDialog, ConsentProvider, hosted } from 'c15t/react';
24
+
25
+ const mode = hosted({ url: 'https://your-project.inth.app' });
26
+ const networkBlocker = {
27
+ rules: [
28
+ { id: 'google-analytics', domain: 'google-analytics.com', category: 'measurement' },
29
+ { id: 'meta-pixel', domain: 'facebook.com', pathIncludes: '/tr', category: 'marketing' },
30
+ ],
31
+ };
32
+
33
+ export function Consent({ children }: { children: ReactNode }) {
34
+ return (
35
+ <ConsentProvider options={{ mode, networkBlocker }}>
36
+ {children}
37
+ <ConsentBanner />
38
+ <ConsentDialog />
39
+ </ConsentProvider>
40
+ );
41
+ }
42
+ ```
43
+
44
+ Render your app inside `ConsentProvider`. Blocking starts when it renders, so
45
+ components outside it that send requests while rendering, or modules evaluated
46
+ before it, are not covered. See the limits below.
47
+
48
+ ## Match requests with rules
49
+
50
+ Each rule names a `domain` and the consent `category` a request needs. The
51
+ domain also matches its subdomains: `google-analytics.com` covers
52
+ `www.google-analytics.com`. `pathIncludes` narrows the rule to paths that
53
+ contain a substring, and `methods` narrows it to HTTP methods. A request is
54
+ blocked when a matching rule's condition is not met by the visitor's
55
+ effective permissions.
56
+
57
+ `category` takes the same conditions as scripts:
58
+
59
+ ```ts
60
+ { category: 'measurement' }
61
+ { category: { and: ['measurement', 'marketing'] } }
62
+ { category: { or: ['measurement', 'marketing'] } }
63
+ ```
64
+
65
+ Add `vendor` to also block the request while the visitor has turned that
66
+ vendor off. IAB TCF rules use `vendorId` and the `iab*` purpose fields
67
+ instead.
68
+
69
+ | Option | Default | Purpose |
70
+ | -------------------- | -------- | ------------------------------------------------------------ |
71
+ | `rules` | required | Rules described above |
72
+ | `enabled` | `true` | Set `false` to keep the rules but stop blocking |
73
+ | `logBlockedRequests` | `true` | Log each blocked request with `console.warn` |
74
+ | `onRequestBlocked` | none | Called with `{ method, url, rule }` for each blocked request |
75
+
76
+ ## What a blocked request looks like
77
+
78
+ The blocker wraps `window.fetch` and `XMLHttpRequest`. A blocked `fetch`
79
+ resolves to a `451` response with the status text
80
+ `Request blocked by consent`, and nothing is sent. A blocked XHR is aborted
81
+ and fires an `error` event. Requests that match no rule are not delayed.
82
+
83
+ ## When blocking starts
84
+
85
+ The provider holds matching requests from its first render in the browser,
86
+ before any of its children render or run effects. That covers requests from
87
+ child components, including their mount effects, and from effects in
88
+ components rendered next to the provider. The blocker module itself loads
89
+ after mount and decides each held request. Apps without `networkBlocker` do
90
+ not download it.
91
+
92
+ The standalone `useNetworkBlocker` hook works the same way from the first
93
+ render of the component that calls it. That render patches `fetch` and
94
+ `XMLHttpRequest`. If React throws the render away and never commits it, the
95
+ hold ends after 10 seconds. Nothing checked consent for the requests it held,
96
+ so they fail the way the blocker fails a blocked request: a 451 response for
97
+ `fetch`, a failed XHR. The same happens when the component unmounts before
98
+ the blocker loads.
99
+
100
+ While consent is unknown, a matching request that would be blocked waits
101
+ instead of failing. Consent is unknown until the policy has loaded, which is
102
+ also when a returning visitor's stored choice takes effect. The request is
103
+ then sent if the choice allows it and blocked otherwise. If the policy fails
104
+ to load, optional categories stay denied and waiting requests are blocked.
105
+ If the policy request never finishes, they keep waiting and are never sent.
106
+
107
+ A synchronous XHR cannot wait. Before the blocker module has loaded, a
108
+ matching one throws a `NetworkError` from `send()`. After that, one that
109
+ consent does not allow yet is blocked.
110
+
111
+ ## What the network blocker cannot stop
112
+
113
+ The blocker only sees requests made after the provider starts rendering in
114
+ the browser, through `fetch` or `XMLHttpRequest`. It cannot stop:
115
+
116
+ * Code that runs before the provider renders: inline scripts in the HTML,
117
+ third-party tags in `<head>`, scripts loaded before hydration (such as
118
+ `next/script` with `beforeInteractive`), and client modules that evaluate
119
+ earlier. Webpack builds evaluate a route's client component modules when
120
+ its chunk loads, so their top-level code runs first. Turbopack evaluates a
121
+ client component module when its first element renders, which inside the
122
+ provider is after blocking starts.
123
+ * Code that saved its own reference to `fetch` or `XMLHttpRequest` before the
124
+ provider rendered.
125
+ * `navigator.sendBeacon`, `WebSocket`, `EventSource`, and requests made by
126
+ `<img>`, `<script>` or `<iframe>` elements, web workers and service
127
+ workers.
128
+ * With the standalone `useNetworkBlocker` hook instead of the provider
129
+ option, requests sent before the component that calls it renders.
130
+ Blocking starts in that component's first render, not the provider's.
131
+
132
+ Keep tracking calls out of that window:
133
+
134
+ * Send them from an effect or an event handler, never at module top level.
135
+ * Load vendor SDKs through `scripts` instead of a `<script>` tag or
136
+ `next/script`, so they wait for consent before they run at all.
137
+ * Check `useConsent('measurement')` (or the category you need) before you
138
+ call a vendor from your own code, and treat the blocker as a backstop.
139
+
140
+ ## Verify
141
+
142
+ Open the browser's network panel, clear site data and reload. Requests
143
+ matching a rule should not appear until you grant their category; with
144
+ logging on, the console lists each blocked request. Reject, reload, and
145
+ check that they still do not appear. The
146
+ [verification guide](../../guides/verify-consent.md) covers automated checks.
@@ -61,3 +61,9 @@ already wrote. Add `clearOnRevocation` to `ConsentProvider.options` to remove
61
61
  declared data when its category is denied. See
62
62
  [clear on revocation](../../integrations/clear-on-revocation.md) for configuration
63
63
  and browser limits.
64
+
65
+ ## Shared lifecycle controls
66
+
67
+ See [shared consent controls](../../guides/shared-consent-controls.md) for external CMPs,
68
+ preference delegation, withdrawal reloads, and application events. These controls
69
+ use the same core runtime across frameworks.
@@ -29,28 +29,47 @@ and `utilities` directives:
29
29
 
30
30
  ## Tokens
31
31
 
32
- The provider snippets below are partial configuration examples. Apply them to
33
- your existing `ConsentProvider` from `c15t/react`, keeping its `mode`, children
34
- and other options.
35
-
36
32
  Colors, radii, shadows, typography, and motion come from `--c15t-*` custom
37
- properties. Set them on the provider through `theme`, or override the
38
- variables directly in your CSS.
39
-
40
- React and Next.js render the theme stylesheet in the server HTML before
41
- the banner. Pass the same theme on the server and the first client render
42
- so your custom tokens apply before hydration. The provider's `nonce`
43
- option also applies to this stylesheet for Content Security Policy.
44
-
45
- `colorScheme: 'dark'` applies dark tokens in the server HTML.
46
- `colorScheme: 'system'` uses a CSS media query to follow the visitor's
47
- preference before hydration. If your app manages dark mode through a
48
- root class instead, include that class in the server HTML too.
49
-
50
- ```tsx
51
- import { ConsentProvider, defineTheme } from 'c15t/react';
52
-
53
- const theme = defineTheme({
33
+ properties. The package stylesheet sets the default values. To change them,
34
+ render `ConsentTheme` with your theme where your app renders on the server,
35
+ or override the variables directly in your CSS.
36
+
37
+ `ConsentTheme` renders a `<style id="c15t-theme">` element with the
38
+ variables for `theme`. It is not a client component: rendered from a Server
39
+ Component or another server-only file, the theme generator stays out of the
40
+ browser bundle. Rendered from a client component it still works, but the
41
+ browser downloads the generator (about 1.7 KB gzip). The provider no longer
42
+ turns `theme` tokens into CSS. It still reads `consentActions` and slot
43
+ styles from its `theme` option, and warns in development when `theme` holds
44
+ tokens but the page has no `c15t-theme` stylesheet.
45
+
46
+ | `ConsentTheme` prop | Purpose |
47
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
48
+ | `theme` | Colors, `dark` colors, typography, spacing, radius, shadows and motion. Defaults to the built-in theme. |
49
+ | `colorScheme` | `'dark'` makes the dark tokens the default before hydration. `'system'` follows `prefers-color-scheme` with a CSS media query. Omit it to follow a `dark` or `c15t-dark` class on `<html>`. |
50
+ | `nonce` | Content Security Policy nonce for the `<style>` element. |
51
+
52
+ Pass the same `colorScheme` to `ConsentTheme` and to the provider. The
53
+ provider keeps the `c15t-dark` class on `<html>` in sync after hydration.
54
+ If your app manages dark mode through a root class, such as next-themes'
55
+ `dark` class, omit `colorScheme` and include that class in the server
56
+ HTML.
57
+
58
+ To switch between light and dark at runtime, render `ConsentTheme` without
59
+ `colorScheme` and toggle the `dark` class on `<html>`: its output already
60
+ holds the variables for both schemes. To switch between different token
61
+ sets, render `ConsentTheme` from a client component and change its props,
62
+ which ships the generator.
63
+
64
+ Outside React, call `generateThemeCSS(theme, colorScheme)` from
65
+ `@c15t/ui/theme` on the server or at build time and put the result in a
66
+ `<style>` element or your stylesheet. It escapes `<`, so the result is safe
67
+ inside `<style>`.
68
+
69
+ ```ts title="src/consent-theme.ts"
70
+ import { defineTheme } from 'c15t/react';
71
+
72
+ export const theme = defineTheme({
54
73
  colors: { primary: '#2f6f4e' },
55
74
  radius: { lg: '4px' },
56
75
  consentActions: {
@@ -58,10 +77,40 @@ const theme = defineTheme({
58
77
  dismiss: { variant: 'neutral', mode: 'stroke' },
59
78
  },
60
79
  });
80
+ ```
61
81
 
62
- <ConsentProvider options={{ mode, theme }} />;
82
+ With server rendering, render `ConsentTheme` in the server-only part of your
83
+ document, next to the provider. In a single-page app with no server render,
84
+ either render it next to the provider, which ships the generator, or write the
85
+ variables into your stylesheet. This partial example keeps the provider's
86
+ `mode`, children and other options:
87
+
88
+ ```tsx
89
+ import { ConsentProvider, ConsentTheme } from 'c15t/react';
90
+
91
+ import { theme } from './consent-theme';
92
+
93
+ <>
94
+ <ConsentTheme theme={theme} />
95
+ <ConsentProvider options={{ mode, theme }}>{children}</ConsentProvider>
96
+ </>;
63
97
  ```
64
98
 
99
+ The provider's `theme` option only supplies `consentActions` and slot styles.
100
+
101
+ Earlier v3 alphas generated theme CSS in the browser from the provider's
102
+ `theme` option. To migrate:
103
+
104
+ 1. Move the theme into a module without `'use client'`, so server and
105
+ client files can both import it.
106
+ 2. Render `<ConsentTheme theme={theme} />` next to the consent root, from a
107
+ Server Component or another server-rendered file. Pass `colorScheme` and
108
+ `nonce` if you set them on the provider.
109
+ 3. Keep passing `theme` to the provider only for `consentActions` and slot
110
+ styles. A theme with tokens only can be removed from the provider.
111
+ 4. Keep the package stylesheet import. It carries the default tokens the
112
+ provider used to inject.
113
+
65
114
  `consentActions` decides how each action role looks: `default` applies to
66
115
  every button, `primary` to whichever actions the policy or your props mark
67
116
  primary, and `accept`, `reject`, `customize`, and `dismiss` to one role each.
@@ -20,6 +20,23 @@ grant.
20
20
  | Explain regional behavior | `policyRule` |
21
21
  | Diagnose initialization | `resolution` |
22
22
 
23
+ ## Gate IAB vendors on the TC string
24
+
25
+ Under an IAB policy, a script, network rule or iframe that names a `vendorId`
26
+ or IAB purposes is an IAB target. It runs only while a confirmed TC string
27
+ grants what it declares: purpose and vendor consent for `iabPurposes`, purpose
28
+ and vendor legitimate interest for `iabLegIntPurposes`, and opt-ins for
29
+ `iabSpecialFeatures`. Every category it names must also be free of
30
+ restrictions, so GPC, an opt-out directive or strict scope blocks it.
31
+
32
+ A refused category is the one exception. It does not block an IAB target that
33
+ processes only on legitimate interest, after publisher restrictions, because
34
+ the TCF lets that processing run without consent. The visitor's control for it
35
+ is the objection, which the legitimate interest signals record. The category
36
+ itself stays refused: its `effectivePermissions` entry is `false`, and targets
37
+ that name only the category, or that also declare a consent purpose or special
38
+ feature, stay blocked.
39
+
23
40
  ## Record only explicit visitor actions
24
41
 
25
42
  ```ts
@@ -37,6 +54,45 @@ confirmation times. Do not call all three in one handler.
37
54
  changes in effective permissions, including changes caused by expiry or privacy
38
55
  signals. Hydration must not be counted as another visitor choice.
39
56
 
57
+ ## When a choice is saved
58
+
59
+ A save records the choice in the browser first and sends it to the backend
60
+ afterwards. The stock banner, preference dialog and IAB surfaces in every
61
+ framework adapter, and the browser client's `acceptAll()`, `rejectAll()`
62
+ and `save()`, close without waiting for the backend. In order:
63
+
64
+ 1. In the click task, the explicit choice and effective permissions change,
65
+ `onChoiceRecorded` and `onPermissionsChanged` run, gated scripts, iframes
66
+ and network rules follow the new permissions, and the surface leaves the
67
+ active state. Its exit animation still plays.
68
+ 2. In the next task, the choice is written to the cookie and localStorage.
69
+ 3. After that write is queued, the request to the backend starts.
70
+ 4. When the request settles, the kernel emits `command:save:completed`. The
71
+ promise returned by `kernel.commands.save()`, React's `performAction()` and
72
+ Svelte's `saveConsents()` resolves or rejects only then.
73
+ 5. If the save turned off a category or vendor that was granted, the page
74
+ reloads in the next task, after `onBeforeConsentRevocationReload` runs.
75
+ Removing a script cannot stop code that already ran, so the reload starts a
76
+ page with only permitted code. With several saves in flight, it waits for
77
+ the last one. Set `reloadOnConsentRevoked: false` to handle revocation
78
+ yourself.
79
+
80
+ A failed request does not reopen the surface or roll the choice back. The
81
+ kernel emits `command:error`, which reaches the `onError` callback in adapters
82
+ that accept one, and queues the payload in localStorage. The queue is replayed
83
+ after the next successful initialization and when the browser comes back
84
+ online, up to 10 attempts over 7 days. A replay carries the original action
85
+ time and policy snapshot token, so the backend records when the visitor
86
+ decided, and a duplicate submission resolves to the same consent record. A
87
+ backend that signs policy snapshot tokens rejects a replay made after the
88
+ token expires, which is 30 minutes by default for the self-hosted backend. A
89
+ save still queued by then is recorded only in the browser.
90
+
91
+ IAB surfaces close in the click task too, but an IAB choice is recorded only
92
+ after its TC string is encoded, which can wait for the TCF library to load.
93
+ If that local step records nothing, for example because the vendor list
94
+ failed to load, the surface comes back so the visitor can try again.
95
+
40
96
  ## Treat notices and privacy signals separately
41
97
 
42
98
  `commands.dismissNotice()` acknowledges the current notice. It does not grant
@@ -58,3 +114,274 @@ would invent a grant and lose its original confirmation time.
58
114
  Valid v2 records can be read without a startup rewrite. The next explicit action
59
115
  writes the v3 format. See [migration](../upgrade-v3.md) for expiry, partial saves,
60
116
  custom transports and backend contract changes.
117
+
118
+ ## Keep open tabs in step
119
+
120
+ Tabs and windows on the same origin (scheme, host and port) share the consent
121
+ cookie and localStorage. When a visitor rejects in one tab, every other open tab
122
+ on that origin applies the rejection without a reload. Scripts and features
123
+ gated on `effectivePermissions` lose permission, subscribers are notified once
124
+ and `onPermissionsChanged` fires. Clearing records in one tab returns the others
125
+ to the active policy's defaults: an opt-in policy denies optional categories and
126
+ shows the prompt again.
127
+
128
+ Tabs on another subdomain that shares the consent cookie do not share
129
+ localStorage, so the browser sends them no `storage` event. They pick up the
130
+ change on their next focus or visibility change, or when you reconcile
131
+ yourself (see below).
132
+
133
+ The `storage` event only exists for localStorage. When localStorage is
134
+ unavailable (blocked by the browser, a sandboxed frame or a privacy mode) and
135
+ c15t stores in the cookie alone, another tab's change arrives only on the next
136
+ focus or visibility change, or when you call `runtime.reconcileStorage()` or
137
+ `persistence.reconcile()`. c15t does not poll the cookie or use a
138
+ `BroadcastChannel` for this.
139
+
140
+ Browser persistence reads stored records again at these moments:
141
+
142
+ | Moment | What happens |
143
+ | ------------------------------------------------------------------------ | --------------------------------- |
144
+ | Another tab or window on the same origin changes a c15t localStorage key | Reconciles on the `storage` event |
145
+ | The page becomes visible again | Reconciles on `visibilitychange` |
146
+ | The window regains focus | Reconciles on `focus` |
147
+ | The network reconnects | No storage read |
148
+
149
+ Several triggers in quick succession run one reconciliation in a later task.
150
+ Reconnecting does not read storage, because going online changes nothing in
151
+ browser storage. On reconnect the kernel retries saves the backend did not
152
+ accept and a failed initialization. Save retries do not write the cookie or
153
+ localStorage. A write that follows initialization, such as a Global Privacy
154
+ Control directive, obeys the ordering described below.
155
+
156
+ Every adapter that mounts browser persistence does this: the React, Next.js and
157
+ TanStack Start providers, Vue, Svelte, Astro, the script tag and
158
+ `createConsentRuntime`. With `persistence: false` nothing is stored or read.
159
+
160
+ ### What a reconciliation applies
161
+
162
+ Stored records are merged into the ones in memory:
163
+
164
+ * Category decisions merge per category. Each category keeps the decision with
165
+ the newer confirmation time, so a tab that only changed marketing never
166
+ reverts another tab's newer measurement decision.
167
+ * Privacy directives, such as a Global Privacy Control opt-out, merge as a
168
+ union. A directive only restricts, so none is dropped.
169
+ * The notice dismissal and the vendor record are single decisions. A stored one
170
+ at least as new as the one in memory replaces it; an older one is ignored.
171
+ * When two tabs record decisions in the same millisecond, the one stored first
172
+ wins in both tabs.
173
+ * Tabs keep one subject. A stored subject is considered only when the record
174
+ that carries it changed, never on focus alone. It replaces a subject the tab
175
+ generated on its first save or copied from storage, so a tab that opened
176
+ before another tab stored a subject joins it. A subject id the server
177
+ resolved (at init, from a prefetch or in a save response) is replaced only by
178
+ a strictly newer stored choice, and an identity set with `identify()` is
179
+ never replaced.
180
+ * Under an IAB policy, the `@c15t/iab` module then loads the TC string the other
181
+ tab stored, together with its purpose, vendor and special-feature selections,
182
+ so `__tcfapi` and the preference controls show the choice now in force.
183
+ Selections the visitor changed in this tab without saving are kept. A missing
184
+ or older stored TC string leaves the current one in place, unless it conflicts
185
+ with the reconciled choice: then it is withdrawn and `__tcfapi` reports no
186
+ consent until the next save. A category denied after the TC string was saved
187
+ conflicts if the TC string grants any of its purposes. A partial selection
188
+ saved through IAB, which records its category as denied because not every
189
+ purpose is granted, keeps its TC string. A TC string confirmed before the
190
+ choice's newest decision no longer describes it and is withdrawn too, unless
191
+ a newer receipt replaces it. That receipt is adopted even when its TC string
192
+ is identical, since TC strings round their time to the day and custom-vendor
193
+ selections live only in the receipt; its expiry then applies. The TC
194
+ string and its receipt (`euconsent-v2`, `c15t-iab-authority-v1`) belong to one
195
+ origin, so after a save on a sibling subdomain that shares the consent cookie,
196
+ this subdomain reports no consent through `__tcfapi` until it saves again.
197
+ Vendors never receive the stale TC string in between: it is withdrawn, or
198
+ held back, before `__tcfapi` publishes. A tab also reloads the TC string
199
+ when another tab on the same origin stores a new one, which covers a save in
200
+ the same millisecond or one that changed only vendors. When two tabs save in
201
+ the same millisecond with different selections, the more restrictive TC
202
+ string wins in both, so a revoked vendor is never advertised again. If each
203
+ grants something the other denies, neither is published until the next save:
204
+ the stored receipt is removed, so a page opened later does not restore it.
205
+ When another tab removes the receipt, or clears localStorage, the TC string
206
+ is withdrawn here too, since a page opened now would find none, and a
207
+ receipt this tab was still decoding is not installed. The removal after a
208
+ tie checks that the stored receipt is the one it read, but localStorage has
209
+ no conditional removal, so a receipt another tab stored a moment earlier can
210
+ still be removed. Every tab then withdraws its TC string until the next save,
211
+ so the race only ever withholds consent.
212
+ * A record removed from readable storage since this tab last saw it present is
213
+ cleared, and the active policy decides again. A record this tab never saw in
214
+ storage, such as a receipt merged from the server after `identify()` or a
215
+ choice seeded while storage was blocked, stays.
216
+ * Blocked storage, or bytes that do not decode, change nothing. A failed read
217
+ never grants a category.
218
+
219
+ Expiry is not decided here. The applied records are evaluated at the time of the
220
+ read, the same way as at startup.
221
+
222
+ ### Clearing records across tabs
223
+
224
+ `clearRecords()` removes every record and then stores the time of the clear,
225
+ the clear epoch, under its own key: `c15t-epoch` in localStorage and a cookie
226
+ of the same name (`<storageKey>-epoch` with a custom `storageKey`). Clearing
227
+ never removes it. Every consent record written afterwards also records the
228
+ epoch it was written under.
229
+
230
+ A decision confirmed before the epoch was made before the clear, so it is void
231
+ wherever it turns up. A decision stamped in the very millisecond of the clear
232
+ counts only if it comes from a tab that had already seen that clear, so a tab's
233
+ own choice right after its own clear stands while another tab's decision in the
234
+ same millisecond does not. A stored record left with no decisions after the
235
+ clear is void too, subject included.
236
+
237
+ * A tab that reconciles only after another tab cleared and saved again drops
238
+ its pre-clear decisions instead of merging them back. Its subject from before
239
+ the clear is dropped too.
240
+ * A tab that missed the clear writes only decisions it made after it. Its queued
241
+ write of an earlier decision is discarded, so it cannot bring back a cleared
242
+ record.
243
+ * Browser hydration and server reads (`readStoredRecordsFromCookieHeader`, used
244
+ by the Next.js, TanStack Start, Nuxt, SvelteKit and Astro helpers) apply the
245
+ same rule, so a server render agrees with the browser.
246
+
247
+ Records from before any clear, including v2 and legacy records, read as epoch 0
248
+ and are unaffected. A corrupt epoch, or one that cannot be read, also reads as
249
+ 0: it voids nothing, so a failed read never grants a category. A consent record
250
+ whose own epoch field is corrupt is kept, and only its epoch is ignored.
251
+
252
+ Each clear moves the epoch forward, even when the device clock went back, so a
253
+ later clear never lets earlier decisions back in. Two clears in the same
254
+ millisecond therefore leave the epoch a millisecond ahead, and a decision saved
255
+ in that millisecond is void. An epoch up to one hour ahead
256
+ of the clock is kept, as it is when the clock was set back after a clear. Until
257
+ the clock catches up, decisions saved in that window are void too. An epoch more
258
+ than an hour ahead is treated as corrupt and reads as 0, so a clear never writes
259
+ one: after the clock went back more than an hour, the new epoch is capped at an
260
+ hour ahead of the clock.
261
+
262
+ That cap is a known limit. A cleared record carries times from before the clock
263
+ went back, and a runtime that missed the clear can write those times back. The
264
+ capped epoch is lower than them, so once the clock has recovered, such a
265
+ decision counts again. Leaving the epoch uncapped does not help: every tab
266
+ whose clock is still behind reads it as corrupt, which voids nothing. Times
267
+ alone cannot order a clear against decisions stamped by a clock that went back
268
+ more than an hour.
269
+
270
+ This changes the stored format. After a clear, the consent cookie carries
271
+ `&e=<time>` (16 bytes) and the localStorage record an `epoch` field (22 bytes),
272
+ and the epoch cookie itself holds a 13-digit time. Visitors who never cleared
273
+ their records store exactly what they did before. An older c15t build rejects
274
+ both the cookie and the localStorage record once they carry the epoch, so a page
275
+ still running one treats the visitor as undecided. Under an opt-out policy that
276
+ page grants optional categories by default until a new choice is saved, and the
277
+ configured prompt may appear again. Deploy the new build to every page of the
278
+ site before visitors can clear their records.
279
+
280
+ ### When the cookie and localStorage disagree
281
+
282
+ When both copies hold a decision for a category from the same millisecond and
283
+ the two conflict, the denial wins.
284
+
285
+ The subject and IAB metadata come from the cookie. A server response, such as
286
+ server-side consent restoration, and a sibling subdomain sharing the cookie
287
+ with `crossSubdomain` can both rewrite it without touching this origin's
288
+ localStorage, so the local copy can be the older one. The local copy's subject
289
+ is used only when this browser's last consent write reached localStorage but
290
+ not the cookie, for example because the cookie grew past the size limit, and
291
+ the cookie has not changed since. Such a write stores the cookie as it stood
292
+ under `<storageKey>-cookie-miss` in localStorage; the next write that reaches
293
+ the cookie, or a clear, removes it. When localStorage rejects a write that the
294
+ cookie takes, for example because storage is full, the older localStorage copy
295
+ is removed.
296
+
297
+ When a server render seeded the page from the consent cookie
298
+ (`skipHydration`), that seed stays authoritative. A denial or privacy directive
299
+ that reached only localStorage is still applied on top of it when the page
300
+ mounts, since it can only restrict, if it is newer than the seeded decision or
301
+ from the same millisecond as a seeded grant. A stored grant is not.
302
+
303
+ The notice dismissal, privacy directives and vendor denials are stored twice
304
+ as well. Privacy directives from both copies all apply. A vendor list in
305
+ localStorage at least as new as the cookie's adds its denials but never lifts
306
+ one the cookie holds; a copy confirmed before the last clear is ignored, so its
307
+ denials never come back. The newer notice dismissal applies; it still only hides a
308
+ notice with the fingerprint it names. The consent record follows the rules
309
+ below.
310
+
311
+ The consent record is stored twice: as a cookie, which a server render reads,
312
+ and in localStorage. The cookie is authoritative. A well-formed cookie wins
313
+ even when it has expired, so a local copy can never bring back a grant the
314
+ cookie no longer carries.
315
+
316
+ A browser can still drop a cookie write, for example when the record grows past
317
+ the cookie size limit, while localStorage takes it. A denial in the local copy
318
+ that is newer than the cookie's decision for that category is therefore applied
319
+ on top of the cookie. A newer local grant is not, so a dropped cookie write only
320
+ ever leaves the visitor with less permission. A server render sees only the
321
+ cookie; after a dropped write that carried a denial, the browser is the stricter
322
+ of the two.
323
+
324
+ When the two copies were written under different clear epochs, each loses its
325
+ decisions from before the later epoch first, and the same rule applies to what
326
+ remains. A later epoch in the local copy never lets its grant replace a cookie
327
+ denial. The subject and IAB metadata come from the copy written under the later
328
+ epoch; a record written before the clear in force keeps its later decisions but
329
+ no subject.
330
+
331
+ A server render cannot know about a clear that never reached a cookie. If the
332
+ page could write localStorage but its cookie writes failed during
333
+ `clearRecords()` (cookies blocked for the page, or a cookie setter that
334
+ throws), the removal of the consent cookie failed too, and so did the epoch
335
+ cookie. The browser then applies the clear from localStorage, while a server
336
+ render still reads the old consent cookie until the next successful cookie
337
+ write. The same applies to a consent cookie set with a different `domain` than
338
+ the current `storageConfig` uses, which the clear cannot remove.
339
+
340
+ ### Ordering with pending writes
341
+
342
+ A tab writes its own choices in a later task, not during the click. Before it
343
+ reads storage, it lands its own queued writes, so a reconciliation never undoes
344
+ the visitor's latest action in that tab. A queued write follows the same rules
345
+ as a read: it stores the per-category merge of its choice and the stored one,
346
+ the union of directives, and never replaces a newer notice or vendor record. It
347
+ keeps the stored subject unless this tab identified a different user.
348
+ When a slow save response returns a server subject id, the tab adds it only to
349
+ the record it wrote; it does not recreate records another tab cleared or
350
+ overwrite another tab's newer choice.
351
+
352
+ Two tabs that write at the same moment can both read storage before either
353
+ writes, and the later write can then drop the other tab's category or privacy
354
+ directive. The tab whose decision was dropped still holds it, and on its next
355
+ reconciliation it writes it back, merged with what storage holds. It does so
356
+ only for decisions it actually stored itself, never for one a clear voided or
357
+ another tab replaced with a newer one. A directive write stores the directives
358
+ it kept from storage too, so if another write drops one of those, this tab
359
+ restores it and applies it as well.
360
+
361
+ If another tab's change reaches this tab while one of its saves is pending, the
362
+ save depends on how far it got. A save not yet sent is dropped. A request
363
+ already sent still reaches the backend, which may record it; this tab ignores
364
+ the response, so it queues no retry and applies no subject id from it. A save
365
+ already queued for retry keeps its original decision time.
366
+
367
+ ### Reconcile yourself or turn it off
368
+
369
+ Browsers send no event for a change made in the same document, or for a cookie
370
+ rewritten without a localStorage change, such as a `Set-Cookie` response header
371
+ or another subdomain sharing the cookie. The next focus or visibility change
372
+ picks it up. To apply it at once, call the method yourself:
373
+
374
+ ```ts
375
+ // Runtime owners: returns true when any record changed.
376
+ runtime.reconcileStorage();
377
+
378
+ // Kernel owners with createPersistence from c15t/modules/persistence:
379
+ persistence.reconcile();
380
+ ```
381
+
382
+ To keep storage but stop automatic reconciliation, pass `sync: false` in the
383
+ persistence options, for example `createConsentRuntime({ persistence: { sync:
384
+ false } })` or `createPersistence({ kernel, sync: false })`. The manual methods
385
+ still work.
386
+
387
+ `dispose()` removes the listeners and cancels a scheduled reconciliation.
@@ -30,6 +30,18 @@ in your application while consent writes still go to Inth. Regular backend
30
30
  browser setup. See [data fetching and transports](./data-fetching.md) for
31
31
  the comparison, including custom transports and offline mode.
32
32
 
33
+ Manifest resolution removes the per-visitor `/init` request, and with it the
34
+ backend's only count of visitors. The server adapters replace it with a session
35
+ report: after each resolution, on a server-rendered page or the same-origin
36
+ init route, the host posts a small report to the backend's `POST /sessions`,
37
+ server-to-server and detached from the response. The browser makes no request
38
+ and the report stores no identity; the visitor's IP and user agent travel as
39
+ forwarded headers under the backend's usual IP handling. Each report is one
40
+ resolution; a page view can produce a `render` and a `route` report, and the
41
+ consuming side groups them into sessions by address and user agent within a
42
+ window. Static output resolves in the browser and sends none. Set `reportSessions: false` on an adapter to turn
43
+ it off.
44
+
33
45
  ## Match initialization to your application output
34
46
 
35
47
  | Application output | Initial state | Required setup |