@openpolicy/react 0.0.19 → 0.0.20
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/package.json +9 -4
- package/skills/cookie-banner/SKILL.md +338 -0
- package/skills/render-policies/SKILL.md +237 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpolicy/react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React components and hooks for OpenPolicy",
|
|
6
6
|
"license": "GPL-3.0-only",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
14
14
|
"styles.css",
|
|
15
|
-
"README.md"
|
|
15
|
+
"README.md",
|
|
16
|
+
"skills"
|
|
16
17
|
],
|
|
17
18
|
"exports": {
|
|
18
19
|
".": {
|
|
@@ -36,7 +37,8 @@
|
|
|
36
37
|
"@openpolicy/core": "workspace:*",
|
|
37
38
|
"@openpolicy/tooling": "workspace:*",
|
|
38
39
|
"@types/react": "^19.0.0",
|
|
39
|
-
"react": "^19.0.0"
|
|
40
|
+
"react": "^19.0.0",
|
|
41
|
+
"@tanstack/intent": "^0.0.29"
|
|
40
42
|
},
|
|
41
43
|
"publishConfig": {
|
|
42
44
|
"exports": {
|
|
@@ -46,5 +48,8 @@
|
|
|
46
48
|
},
|
|
47
49
|
"./styles.css": "./styles.css"
|
|
48
50
|
}
|
|
49
|
-
}
|
|
51
|
+
},
|
|
52
|
+
"keywords": [
|
|
53
|
+
"tanstack-intent"
|
|
54
|
+
]
|
|
50
55
|
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cookie-banner
|
|
3
|
+
description: >
|
|
4
|
+
Cookie consent banner with useCookies hook, route state machine (cookie | preferences | closed), acceptAll, acceptNecessary, save, categories, toggle, ConsentGate conditional rendering, has expression DSL, localStorage persistence under op_consent key.
|
|
5
|
+
type: framework
|
|
6
|
+
library: openpolicy
|
|
7
|
+
framework: react
|
|
8
|
+
library_version: "0.0.19"
|
|
9
|
+
requires:
|
|
10
|
+
- openpolicy/define-config
|
|
11
|
+
- openpolicy/render-policies
|
|
12
|
+
sources:
|
|
13
|
+
- jamiedavenport/openpolicy:packages/react/src/context.tsx
|
|
14
|
+
- jamiedavenport/openpolicy:apps/www/registry/cookie-banner.tsx
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
This skill builds on openpolicy/define-config and openpolicy/render-policies. Read them first.
|
|
18
|
+
|
|
19
|
+
## Route State Machine
|
|
20
|
+
|
|
21
|
+
`useCookies()` exposes a `route` value that drives which UI to show:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
"cookie" — consent not yet given; show the banner
|
|
25
|
+
"preferences" — user clicked "Manage"; show the preferences panel
|
|
26
|
+
"closed" — consent resolved; render nothing
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The provider sets `route` to `"cookie"` automatically when `status === "undecided"` (no prior consent in localStorage). Once the user acts (`acceptAll`, `acceptNecessary`, or `save`), the provider sets `route` to `"closed"`. `setRoute` lets you navigate between `"cookie"` and `"preferences"` manually.
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
The following is a complete minimal implementation using plain divs and buttons — no UI library required.
|
|
34
|
+
|
|
35
|
+
### 1. Config — include a `cookie` section
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// openpolicy.ts
|
|
39
|
+
import { defineConfig } from "@openpolicy/sdk";
|
|
40
|
+
|
|
41
|
+
export default defineConfig({
|
|
42
|
+
company: {
|
|
43
|
+
name: "Acme",
|
|
44
|
+
legalName: "Acme, Inc.",
|
|
45
|
+
address: "123 Main St, San Francisco, CA",
|
|
46
|
+
contact: "privacy@acme.com",
|
|
47
|
+
},
|
|
48
|
+
cookie: {
|
|
49
|
+
cookies: {
|
|
50
|
+
essential: true,
|
|
51
|
+
analytics: true,
|
|
52
|
+
marketing: false,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 2. Provider — wrap your app root
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// App.tsx
|
|
62
|
+
import { OpenPolicy } from "@openpolicy/react";
|
|
63
|
+
import config from "./openpolicy";
|
|
64
|
+
import { CookieBanner } from "./CookieBanner";
|
|
65
|
+
import { CookiePreferences } from "./CookiePreferences";
|
|
66
|
+
|
|
67
|
+
export function App() {
|
|
68
|
+
return (
|
|
69
|
+
<OpenPolicy config={config}>
|
|
70
|
+
<CookieBanner />
|
|
71
|
+
<CookiePreferences />
|
|
72
|
+
{/* rest of app */}
|
|
73
|
+
</OpenPolicy>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 3. Banner — gate on `route === "cookie"`
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
// CookieBanner.tsx
|
|
82
|
+
import { useCookies } from "@openpolicy/react";
|
|
83
|
+
|
|
84
|
+
export function CookieBanner() {
|
|
85
|
+
const { route, setRoute, acceptAll, acceptNecessary } = useCookies();
|
|
86
|
+
|
|
87
|
+
if (route !== "cookie") return null;
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
style={{
|
|
92
|
+
position: "fixed",
|
|
93
|
+
bottom: "1rem",
|
|
94
|
+
right: "1rem",
|
|
95
|
+
padding: "1.5rem",
|
|
96
|
+
background: "#fff",
|
|
97
|
+
border: "1px solid #e5e7eb",
|
|
98
|
+
borderRadius: "0.5rem",
|
|
99
|
+
maxWidth: "24rem",
|
|
100
|
+
zIndex: 50,
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<p style={{ marginBottom: "1rem" }}>
|
|
104
|
+
We use cookies to improve your experience and analyse site traffic.
|
|
105
|
+
</p>
|
|
106
|
+
<div style={{ display: "flex", gap: "0.5rem", justifyContent: "flex-end" }}>
|
|
107
|
+
<button onClick={() => setRoute("preferences")}>Manage Cookies</button>
|
|
108
|
+
<button onClick={acceptNecessary}>Necessary Only</button>
|
|
109
|
+
<button onClick={acceptAll}>Accept All</button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 4. Preferences panel — toggle categories, then save
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
// CookiePreferences.tsx
|
|
120
|
+
import { useCookies } from "@openpolicy/react";
|
|
121
|
+
|
|
122
|
+
export function CookiePreferences() {
|
|
123
|
+
const { route, setRoute, categories, toggle, save, acceptNecessary } =
|
|
124
|
+
useCookies();
|
|
125
|
+
|
|
126
|
+
if (route !== "preferences") return null;
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
style={{
|
|
131
|
+
position: "fixed",
|
|
132
|
+
inset: 0,
|
|
133
|
+
background: "rgba(0,0,0,0.5)",
|
|
134
|
+
display: "flex",
|
|
135
|
+
alignItems: "center",
|
|
136
|
+
justifyContent: "center",
|
|
137
|
+
zIndex: 50,
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<div
|
|
141
|
+
style={{
|
|
142
|
+
background: "#fff",
|
|
143
|
+
padding: "2rem",
|
|
144
|
+
borderRadius: "0.5rem",
|
|
145
|
+
minWidth: "20rem",
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
<h2>Cookie preferences</h2>
|
|
149
|
+
<p>Choose which cookies you allow.</p>
|
|
150
|
+
|
|
151
|
+
<ul style={{ listStyle: "none", padding: 0 }}>
|
|
152
|
+
{categories.map(({ key, label, enabled, locked }) => (
|
|
153
|
+
<li
|
|
154
|
+
key={key}
|
|
155
|
+
style={{
|
|
156
|
+
display: "flex",
|
|
157
|
+
justifyContent: "space-between",
|
|
158
|
+
padding: "0.5rem 0",
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
<span>{label}</span>
|
|
162
|
+
<input
|
|
163
|
+
type="checkbox"
|
|
164
|
+
checked={enabled}
|
|
165
|
+
disabled={locked}
|
|
166
|
+
onChange={() => toggle(key)}
|
|
167
|
+
/>
|
|
168
|
+
</li>
|
|
169
|
+
))}
|
|
170
|
+
</ul>
|
|
171
|
+
|
|
172
|
+
<div style={{ display: "flex", gap: "0.5rem", justifyContent: "flex-end" }}>
|
|
173
|
+
<button onClick={acceptNecessary}>Reject All</button>
|
|
174
|
+
<button onClick={() => save()}>Save Preferences</button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Core Patterns
|
|
183
|
+
|
|
184
|
+
### 1. Route-gated rendering
|
|
185
|
+
|
|
186
|
+
Always check `route` before rendering. Each component owns one route value and returns `null` for all others.
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
// Banner owns "cookie"
|
|
190
|
+
if (route !== "cookie") return null;
|
|
191
|
+
|
|
192
|
+
// Preferences owns "preferences"
|
|
193
|
+
if (route !== "preferences") return null;
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Navigating between them:
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
// Open preferences from banner
|
|
200
|
+
<button onClick={() => setRoute("preferences")}>Manage Cookies</button>
|
|
201
|
+
|
|
202
|
+
// Close preferences without saving (returns to "closed", not back to "cookie")
|
|
203
|
+
<button onClick={() => setRoute("closed")}>Cancel</button>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 2. Preferences panel with categories, toggle, and save
|
|
207
|
+
|
|
208
|
+
`categories` is derived from your config's `cookie.cookies` keys. Each entry:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
type CookieCategory = {
|
|
212
|
+
key: string; // "essential" | "analytics" | "marketing" | ...
|
|
213
|
+
label: string; // human-readable, e.g. "Analytics"
|
|
214
|
+
enabled: boolean;
|
|
215
|
+
locked: boolean; // true for "essential" — cannot be toggled
|
|
216
|
+
};
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`toggle(key)` updates local draft state without persisting. `save()` merges draft into localStorage and closes the banner. Until `save()` is called, the user's changes are not committed.
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
{categories.map(({ key, label, enabled, locked }) => (
|
|
223
|
+
<label key={key}>
|
|
224
|
+
<input
|
|
225
|
+
type="checkbox"
|
|
226
|
+
checked={enabled}
|
|
227
|
+
disabled={locked}
|
|
228
|
+
onChange={() => toggle(key)}
|
|
229
|
+
/>
|
|
230
|
+
{label}
|
|
231
|
+
</label>
|
|
232
|
+
))}
|
|
233
|
+
<button onClick={() => save()}>Save</button>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### 3. ConsentGate — conditional rendering by consent
|
|
237
|
+
|
|
238
|
+
Wrap any content that should only appear when a category has consent:
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
import { ConsentGate } from "@openpolicy/react";
|
|
242
|
+
|
|
243
|
+
// Simple category string
|
|
244
|
+
<ConsentGate requires="analytics">
|
|
245
|
+
<AnalyticsDashboard />
|
|
246
|
+
</ConsentGate>
|
|
247
|
+
|
|
248
|
+
// Compound expression
|
|
249
|
+
<ConsentGate requires={{ and: ["analytics", "marketing"] }}>
|
|
250
|
+
<RetargetingPixel />
|
|
251
|
+
</ConsentGate>
|
|
252
|
+
|
|
253
|
+
<ConsentGate requires={{ or: ["analytics", "functional"] }}>
|
|
254
|
+
<EnhancedFeatures />
|
|
255
|
+
</ConsentGate>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`has()` from `useCookies()` evaluates the same `HasExpression` DSL imperatively:
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
const { has } = useCookies();
|
|
262
|
+
|
|
263
|
+
// Load analytics script only when consent is given
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
if (has("analytics")) {
|
|
266
|
+
loadAnalytics();
|
|
267
|
+
}
|
|
268
|
+
}, [has]);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`HasExpression` type: `string | { and: HasExpression[] } | { or: HasExpression[] } | { not: HasExpression }`
|
|
272
|
+
|
|
273
|
+
### 4. shadcn registry shortcut
|
|
274
|
+
|
|
275
|
+
For a pre-styled banner and preferences panel that matches your shadcn/ui theme:
|
|
276
|
+
|
|
277
|
+
```sh
|
|
278
|
+
shadcn add @openpolicy/cookie-banner
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
This installs `CookieBanner` and `CookiePreferences` components using shadcn Card, Dialog, Switch, and Button primitives. The logic is identical to the manual implementation above — only the styling differs.
|
|
282
|
+
|
|
283
|
+
## Common Mistakes
|
|
284
|
+
|
|
285
|
+
### Mistake 1 — Not gating banner render on `route === "cookie"` (HIGH)
|
|
286
|
+
|
|
287
|
+
Without the route check the banner renders permanently, ignoring whether consent has already been given.
|
|
288
|
+
|
|
289
|
+
```tsx
|
|
290
|
+
// WRONG: renders always, no route check
|
|
291
|
+
function CookieBanner() {
|
|
292
|
+
const { acceptAll, acceptNecessary } = useCookies();
|
|
293
|
+
return <div>...</div>;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Correct
|
|
297
|
+
function CookieBanner() {
|
|
298
|
+
const { route, acceptAll, acceptNecessary } = useCookies();
|
|
299
|
+
if (route !== "cookie") return null;
|
|
300
|
+
return <div>...</div>;
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Mistake 2 — Building custom consent state with `useState` instead of `useCookies()` (HIGH)
|
|
305
|
+
|
|
306
|
+
Manual `useState` consent flags miss localStorage persistence, cross-tab sync, and the `document.body` `data-consent-*` attributes that `useCookies()` maintains automatically.
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
// WRONG: custom state, no persistence or sync
|
|
310
|
+
const [accepted, setAccepted] = useState(false);
|
|
311
|
+
// manually writing to localStorage, missing cross-tab broadcast
|
|
312
|
+
|
|
313
|
+
// Correct: use the hook
|
|
314
|
+
const { status, acceptAll, acceptNecessary, has } = useCookies();
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The provider writes consent under key `"op_consent"` in localStorage and syncs via `useSyncExternalStore` — all tabs sharing the same origin update simultaneously.
|
|
318
|
+
|
|
319
|
+
### Mistake 3 — Using `useCookies()` without `<OpenPolicy>` provider (HIGH)
|
|
320
|
+
|
|
321
|
+
Without the provider, `useCookies()` reads from the default context: `consent: null`, `categories: []`, `route: "closed"`. The banner never appears and the preferences panel renders no toggles. There is no thrown error — it fails silently.
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
// WRONG: no provider
|
|
325
|
+
function App() {
|
|
326
|
+
return <CookieBanner />;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Correct: provider at root, banner and preferences inside it
|
|
330
|
+
function App() {
|
|
331
|
+
return (
|
|
332
|
+
<OpenPolicy config={config}>
|
|
333
|
+
<CookieBanner />
|
|
334
|
+
<CookiePreferences />
|
|
335
|
+
</OpenPolicy>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
```
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: render-policies
|
|
3
|
+
description: >
|
|
4
|
+
Render OpenPolicy privacy, terms of service, and cookie policy documents as
|
|
5
|
+
React components using @openpolicy/react. Components — PrivacyPolicy,
|
|
6
|
+
TermsOfService, CookiePolicy — read config from the OpenPolicyProvider
|
|
7
|
+
(alias OpenPolicy) context and accept optional config and components props
|
|
8
|
+
for per-page overrides and full rendering customization via PolicyComponents.
|
|
9
|
+
type: framework
|
|
10
|
+
library: openpolicy
|
|
11
|
+
framework: react
|
|
12
|
+
library_version: "0.0.19"
|
|
13
|
+
requires:
|
|
14
|
+
- openpolicy/define-config
|
|
15
|
+
sources:
|
|
16
|
+
- jamiedavenport/openpolicy:packages/react/src/context.tsx
|
|
17
|
+
- jamiedavenport/openpolicy:packages/react/src/render.tsx
|
|
18
|
+
- jamiedavenport/openpolicy:packages/react/src/types.ts
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
This skill builds on openpolicy/define-config. Read it first.
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
Install the package:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
bun add @openpolicy/react
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Wrap your app with the provider, import styles, and render a policy page:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// layout.tsx (or _app.tsx)
|
|
35
|
+
import '@openpolicy/react/styles.css';
|
|
36
|
+
import { OpenPolicy } from '@openpolicy/react';
|
|
37
|
+
import config from './openpolicy';
|
|
38
|
+
|
|
39
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
40
|
+
return (
|
|
41
|
+
<OpenPolicy config={config}>
|
|
42
|
+
{children}
|
|
43
|
+
</OpenPolicy>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
// app/privacy/page.tsx
|
|
50
|
+
import { PrivacyPolicy } from '@openpolicy/react';
|
|
51
|
+
|
|
52
|
+
export default function PrivacyPage() {
|
|
53
|
+
return <PrivacyPolicy />;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Core Patterns
|
|
58
|
+
|
|
59
|
+
### 1. Provider at app root
|
|
60
|
+
|
|
61
|
+
`OpenPolicyProvider` (exported as `OpenPolicy`) must wrap the entire application. All policy components and hooks read from this context.
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { OpenPolicy } from '@openpolicy/react';
|
|
65
|
+
import config from './openpolicy';
|
|
66
|
+
|
|
67
|
+
<OpenPolicy config={config}>
|
|
68
|
+
<App />
|
|
69
|
+
</OpenPolicy>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`config` must be an `OpenPolicyConfig` (the nested shape produced by `defineConfig()`). The provider also injects default styles via a React `<style>` tag, so the CSS import is optional when using the provider — but import it explicitly for non-provider rendering paths.
|
|
73
|
+
|
|
74
|
+
### 2. Rendering all three policy types
|
|
75
|
+
|
|
76
|
+
Each component reads its slice of the config from context automatically:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { PrivacyPolicy, TermsOfService, CookiePolicy } from '@openpolicy/react';
|
|
80
|
+
|
|
81
|
+
// Privacy policy page
|
|
82
|
+
export default function PrivacyPage() {
|
|
83
|
+
return <PrivacyPolicy />;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Terms of service page
|
|
87
|
+
export default function TermsPage() {
|
|
88
|
+
return <TermsOfService />;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Cookie policy page
|
|
92
|
+
export default function CookiePolicyPage() {
|
|
93
|
+
return <CookiePolicy />;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
All three components accept the same optional props:
|
|
98
|
+
|
|
99
|
+
| Prop | Type | Description |
|
|
100
|
+
|------|------|-------------|
|
|
101
|
+
| `config` | `OpenPolicyConfig \| PrivacyPolicyConfig` (or equivalent) | Per-page config override; falls back to context |
|
|
102
|
+
| `components` | `PolicyComponents` | Override default renderers for individual node types |
|
|
103
|
+
| `style` | `CSSProperties` | Inline styles applied to the wrapper `<div>` |
|
|
104
|
+
|
|
105
|
+
### 3. Component customization with PolicyComponents
|
|
106
|
+
|
|
107
|
+
Override any node renderer by passing a `components` prop. The `PolicyComponents` interface:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
type PolicyComponents = {
|
|
111
|
+
Section?: ComponentType<{ section: DocumentSection; children: ReactNode }>;
|
|
112
|
+
Heading?: ComponentType<{ node: HeadingNode }>;
|
|
113
|
+
Paragraph?: ComponentType<{ node: ParagraphNode; children: ReactNode }>;
|
|
114
|
+
List?: ComponentType<{ node: ListNode; children: ReactNode }>;
|
|
115
|
+
Text?: ComponentType<{ node: TextNode }>;
|
|
116
|
+
Bold?: ComponentType<{ node: BoldNode }>;
|
|
117
|
+
Italic?: ComponentType<{ node: ItalicNode }>;
|
|
118
|
+
Link?: ComponentType<{ node: LinkNode }>;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
All fields are optional — unspecified slots use the default renderers. Example: swap in a custom heading:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { PrivacyPolicy } from '@openpolicy/react';
|
|
126
|
+
import type { HeadingNode } from '@openpolicy/core';
|
|
127
|
+
|
|
128
|
+
function MyHeading({ node }: { node: HeadingNode }) {
|
|
129
|
+
return <h2 className="text-2xl font-bold text-slate-900">{node.text}</h2>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default function PrivacyPage() {
|
|
133
|
+
return <PrivacyPolicy components={{ Heading: MyHeading }} />;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 4. Theming with CSS custom properties
|
|
138
|
+
|
|
139
|
+
The `.op-policy` wrapper exposes CSS custom properties. Override them globally or scoped:
|
|
140
|
+
|
|
141
|
+
```css
|
|
142
|
+
.op-policy {
|
|
143
|
+
--op-heading-color: #0f172a;
|
|
144
|
+
--op-body-color: #475569;
|
|
145
|
+
--op-link-color: #6366f1;
|
|
146
|
+
--op-link-color-hover: #4f46e5;
|
|
147
|
+
--op-font-family: 'Inter', sans-serif;
|
|
148
|
+
--op-font-size-body: 0.9375rem;
|
|
149
|
+
--op-font-size-heading: 1.125rem;
|
|
150
|
+
--op-font-weight-heading: 600;
|
|
151
|
+
--op-line-height: 1.75;
|
|
152
|
+
--op-section-gap: 2.5rem;
|
|
153
|
+
--op-border-color: #e2e8f0;
|
|
154
|
+
--op-border-radius: 0.5rem;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 5. shadcn/ui registry install
|
|
159
|
+
|
|
160
|
+
Pre-styled variants are available via the shadcn registry. Install individual components:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
shadcn add @openpolicy/privacy-policy
|
|
164
|
+
shadcn add @openpolicy/terms-of-service
|
|
165
|
+
shadcn add @openpolicy/cookie-policy
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
This copies styled component files into your project that you can edit freely. They use the same `PolicyComponents` customization interface under the hood.
|
|
169
|
+
|
|
170
|
+
## Common Mistakes
|
|
171
|
+
|
|
172
|
+
### CRITICAL — Using `openPolicy()` from `@openpolicy/vite` to generate static files
|
|
173
|
+
|
|
174
|
+
The `openPolicy()` Vite plugin emits `.md` / `.html` / `.pdf` files at build time. It is a separate, legacy path. Agents trained on older docs may reach for it when asked to "add a privacy policy page."
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
// vite.config.ts — WRONG
|
|
178
|
+
import { openPolicy } from '@openpolicy/vite';
|
|
179
|
+
plugins: [openPolicy({ formats: ['html'], outDir: 'public' })]
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The correct approach is React runtime rendering — render `<PrivacyPolicy />` as a page component. The static files produced by `openPolicy()` are never read by the React components.
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
// correct: render as a React page
|
|
186
|
+
import { PrivacyPolicy } from '@openpolicy/react';
|
|
187
|
+
export default function PrivacyPage() {
|
|
188
|
+
return <PrivacyPolicy />;
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### HIGH — Rendering policy components outside `<OpenPolicy>` provider
|
|
193
|
+
|
|
194
|
+
`PrivacyPolicy`, `TermsOfService`, and `CookiePolicy` call `useContext(OpenPolicyContext)` and return `null` when no config is found. Without the provider, nothing renders and no error is thrown — the bug is invisible in development.
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
// privacy-page.tsx — WRONG: no provider anywhere in the tree
|
|
198
|
+
export default function PrivacyPage() {
|
|
199
|
+
return <PrivacyPolicy />;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Wrap at the application root, not inside individual pages:
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
// layout.tsx — correct
|
|
207
|
+
import config from './openpolicy';
|
|
208
|
+
export default function RootLayout({ children }) {
|
|
209
|
+
return (
|
|
210
|
+
<OpenPolicy config={config}>
|
|
211
|
+
{children}
|
|
212
|
+
</OpenPolicy>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// privacy-page.tsx — correct: provider is already in the tree
|
|
217
|
+
export default function PrivacyPage() {
|
|
218
|
+
return <PrivacyPolicy />;
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### MEDIUM — Not importing `@openpolicy/react/styles.css`
|
|
223
|
+
|
|
224
|
+
Without the CSS import, policy components render as unstyled HTML. The provider injects inline styles via a React `<style>` tag, but this may not work in all SSR or bundler setups.
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
// WRONG: no styles imported
|
|
228
|
+
import { PrivacyPolicy } from '@openpolicy/react';
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
// correct
|
|
233
|
+
import '@openpolicy/react/styles.css';
|
|
234
|
+
import { PrivacyPolicy } from '@openpolicy/react';
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The default styles scope all rules to `.op-policy` so they do not leak to the rest of the page.
|