@guard-rail/core 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +135 -4
  2. package/dist/adapters/supabase/db.js +33 -0
  3. package/dist/adapters/supabase/db.js.map +1 -1
  4. package/dist/cli/doctor.js +175 -6
  5. package/dist/cli/doctor.js.map +1 -1
  6. package/dist/core/adapter-types.d.ts +35 -2
  7. package/dist/core/csp.d.ts +32 -0
  8. package/dist/core/csp.js +26 -0
  9. package/dist/core/csp.js.map +1 -0
  10. package/dist/index.client.d.ts +3 -2
  11. package/dist/index.client.js +8 -1
  12. package/dist/index.client.js.map +1 -1
  13. package/dist/index.d.ts +3 -2
  14. package/dist/index.js +8 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/react/Admin.d.ts +5 -3
  17. package/dist/react/Admin.js +29 -41
  18. package/dist/react/Admin.js.map +1 -1
  19. package/dist/react/AuthDrawer.js +22 -34
  20. package/dist/react/AuthDrawer.js.map +1 -1
  21. package/dist/react/GRail/client.js +95 -56
  22. package/dist/react/GRail/client.js.map +1 -1
  23. package/dist/react/Icons.d.ts +3 -1
  24. package/dist/react/Icons.js +5 -0
  25. package/dist/react/Icons.js.map +1 -1
  26. package/dist/react/MediaPicker.js +12 -16
  27. package/dist/react/MediaPicker.js.map +1 -1
  28. package/dist/react/Repeater/client.js +12 -13
  29. package/dist/react/Repeater/client.js.map +1 -1
  30. package/dist/react/Team.d.ts +1 -7
  31. package/dist/react/Team.js +21 -46
  32. package/dist/react/Team.js.map +1 -1
  33. package/dist/react/guardrail.css +563 -0
  34. package/dist/server/actions.d.ts +19 -2
  35. package/dist/server/actions.js +35 -0
  36. package/dist/server/actions.js.map +1 -1
  37. package/package.json +11 -10
  38. package/supabase/admin/check-grants.sql +12 -3
  39. package/supabase/migrations/20261103000000_content_history.sql +211 -0
  40. package/dist/react/styles.d.ts +0 -504
  41. package/dist/react/styles.js +0 -500
  42. package/dist/react/styles.js.map +0 -1
@@ -0,0 +1,563 @@
1
+ /*
2
+ * Guardrail's editing chrome — the sign-in bar, toolbar, editors, drawers and
3
+ * the outlines drawn around editable fields.
4
+ *
5
+ * Imported by the package itself (see GRailAdmin), so a site doesn't link it by
6
+ * hand, and shipped as a real stylesheet rather than inline styles so it works
7
+ * under a strict Content-Security-Policy (style-src 'self', no 'unsafe-inline').
8
+ *
9
+ * Theming: every color is a --gr-* variable. Override any of them in your own
10
+ * CSS to restyle the chrome, e.g.
11
+ *
12
+ * :root { --gr-accent: #7c3aed; --gr-field-outline: #7c3aed; }
13
+ *
14
+ * Three layers: change a base color (--gr-bg, --gr-fg, --gr-border …) to retheme
15
+ * everything, or set the --gr-overlay-* variables to restyle only the floating
16
+ * surfaces (sign-in bar, toolbar, popups, drawers, picker) — they default to the
17
+ * base colors, so you only set what you want to differ.
18
+ *
19
+ * The defaults below are declared in :where(:root), which has no specificity, so
20
+ * a plain :root rule of yours wins whatever order the stylesheets load in — no
21
+ * !important, no selector tricks.
22
+ *
23
+ * Every class is prefixed gr-, so nothing here can collide with a site's own.
24
+ */
25
+
26
+ /* ---- theme ---------------------------------------------------------------- */
27
+
28
+ :where(:root) { --gr-bg: #ffffff; --gr-bg-glass: rgba(255,255,255,0.97); --gr-fg: #111827; --gr-fg2: #4b5563; --gr-muted: #6b7280; --gr-subtle: #9ca3af; --gr-border: #e5e7eb; --gr-border-strong: #d1d5db; --gr-border-soft: #f3f4f6; --gr-input-bg: #ffffff; --gr-soft: #f3f4f6; --gr-accent: #2563eb; --gr-accent-soft: #eff6ff; --gr-accent-text: #1d4ed8; --gr-primary-bg: #111827; --gr-primary-fg: #ffffff; --gr-danger: #dc2626; --gr-label: #1e3a8a; --gr-shadow: rgba(0,0,0,0.25); --gr-field-outline: #60a5fa; --gr-field-outline-active: #2563eb; --gr-on-accent: #ffffff; --gr-scrim: rgba(0,0,0,0.4); --gr-scrim-modal: rgba(0,0,0,0.5); --gr-badge-bg: #fef3c7; --gr-badge-fg: #92400e; --gr-overlay-bg: var(--gr-bg); --gr-overlay-bg-glass: var(--gr-bg-glass); --gr-overlay-fg: var(--gr-fg); --gr-overlay-border: var(--gr-border); }
29
+ [data-guardrail] { color-scheme: light; }
30
+ @media (prefers-color-scheme: dark) {
31
+ :where(:root) { --gr-bg: #0f1b2d; --gr-bg-glass: rgba(15,27,45,0.97); --gr-fg: #e6edf7; --gr-fg2: #b6c4d8; --gr-muted: #9fb0c7; --gr-subtle: #7d8ea6; --gr-border: rgba(148,163,184,0.22); --gr-border-strong: rgba(148,163,184,0.35); --gr-border-soft: rgba(148,163,184,0.14); --gr-input-bg: #14243b; --gr-soft: #1a2d49; --gr-accent: #3b82f6; --gr-accent-soft: rgba(59,130,246,0.16); --gr-accent-text: #93c5fd; --gr-primary-bg: #3b82f6; --gr-primary-fg: #ffffff; --gr-danger: #f87171; --gr-label: #dbeafe; --gr-shadow: rgba(0,0,0,0.5); }
32
+ [data-guardrail] { color-scheme: dark; }
33
+ }
34
+
35
+ /* ---- components ------------------------------------------------------------ */
36
+
37
+ .gr-banner {
38
+ position: fixed;
39
+ bottom: 0;
40
+ left: 0;
41
+ width: 100%;
42
+ background: var(--gr-overlay-bg);
43
+ color: var(--gr-overlay-fg);
44
+ padding: 12px 16px;
45
+ z-index: 10000;
46
+ border-top: 1px solid var(--gr-overlay-border);
47
+ font-family: system-ui, -apple-system, sans-serif;
48
+ }
49
+ .gr-banner-form {
50
+ display: flex;
51
+ gap: 12px;
52
+ justify-content: center;
53
+ align-items: center;
54
+ flex-wrap: wrap;
55
+ }
56
+ .gr-brand {
57
+ font-weight: 900;
58
+ letter-spacing: 0.15em;
59
+ font-size: 12px;
60
+ text-transform: uppercase;
61
+ }
62
+ .gr-brand-stack {
63
+ display: flex;
64
+ flex-direction: column;
65
+ align-items: flex-start;
66
+ gap: 2px;
67
+ }
68
+ .gr-error {
69
+ color: var(--gr-danger);
70
+ font-size: 12px;
71
+ font-weight: 700;
72
+ }
73
+ .gr-input {
74
+ border: 1px solid var(--gr-border-strong);
75
+ border-radius: 4px;
76
+ padding: 6px 8px;
77
+ font-size: 13px;
78
+ color: var(--gr-fg);
79
+ background: var(--gr-input-bg);
80
+ outline: none;
81
+ }
82
+ .gr-primary-btn {
83
+ background: var(--gr-primary-bg);
84
+ color: var(--gr-primary-fg);
85
+ padding: 7px 16px;
86
+ border-radius: 4px;
87
+ font-size: 12px;
88
+ font-weight: 700;
89
+ border: none;
90
+ cursor: pointer;
91
+ }
92
+ .gr-toolbar {
93
+ position: fixed;
94
+ bottom: 24px;
95
+ left: 50%;
96
+ transform: translateX(-50%);
97
+ z-index: 9999;
98
+ background: var(--gr-overlay-bg-glass);
99
+ backdrop-filter: blur(8px);
100
+ color: var(--gr-overlay-fg);
101
+ padding: 12px 20px;
102
+ border-radius: 9999px;
103
+ box-shadow: 0 20px 40px var(--gr-shadow);
104
+ display: flex;
105
+ align-items: center;
106
+ gap: 20px;
107
+ border: 1px solid var(--gr-overlay-border);
108
+ font-family: system-ui, -apple-system, sans-serif;
109
+ }
110
+ .gr-dot {
111
+ display: inline-block;
112
+ width: 8px;
113
+ height: 8px;
114
+ border-radius: 50%;
115
+ background: var(--gr-accent);
116
+ }
117
+ .gr-toolbar-label {
118
+ font-size: 10px;
119
+ font-weight: 900;
120
+ text-transform: uppercase;
121
+ letter-spacing: 0.2em;
122
+ color: var(--gr-label);
123
+ }
124
+ .gr-divider {
125
+ width: 1px;
126
+ height: 16px;
127
+ background: var(--gr-border-strong);
128
+ }
129
+ .gr-link-btn {
130
+ font-size: 10px;
131
+ font-weight: 700;
132
+ text-transform: uppercase;
133
+ letter-spacing: 0.15em;
134
+ color: var(--gr-muted);
135
+ background: none;
136
+ border: none;
137
+ cursor: pointer;
138
+ }
139
+ .gr-popover {
140
+ position: fixed;
141
+ bottom: 90px;
142
+ left: 50%;
143
+ transform: translateX(-50%);
144
+ z-index: 10001;
145
+ background: var(--gr-overlay-bg);
146
+ color: var(--gr-overlay-fg);
147
+ padding: 16px;
148
+ border-radius: 8px;
149
+ box-shadow: 0 20px 40px var(--gr-shadow);
150
+ border: 1px solid var(--gr-overlay-border);
151
+ min-width: 300px;
152
+ cursor: default;
153
+ font-family: system-ui, -apple-system, sans-serif;
154
+ }
155
+ .gr-label {
156
+ display: block;
157
+ font-size: 10px;
158
+ font-weight: 700;
159
+ color: var(--gr-subtle);
160
+ margin-bottom: 4px;
161
+ text-transform: uppercase;
162
+ }
163
+ .gr-field {
164
+ border: 1px solid var(--gr-border-strong);
165
+ border-radius: 4px;
166
+ padding: 6px 8px;
167
+ font-size: 13px;
168
+ color: var(--gr-fg);
169
+ background: var(--gr-input-bg);
170
+ width: 100%;
171
+ margin-bottom: 8px;
172
+ box-sizing: border-box;
173
+ }
174
+ .gr-row {
175
+ display: flex;
176
+ gap: 8px;
177
+ margin-top: 8px;
178
+ padding-top: 8px;
179
+ border-top: 1px solid var(--gr-border-soft);
180
+ }
181
+ .gr-cancel-btn {
182
+ background: var(--gr-soft);
183
+ color: var(--gr-fg2);
184
+ padding: 7px 14px;
185
+ border-radius: 4px;
186
+ font-size: 12px;
187
+ border: none;
188
+ cursor: pointer;
189
+ }
190
+ .gr-anchored-bar {
191
+ position: fixed;
192
+ z-index: 10001;
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 4px;
196
+ background: var(--gr-overlay-bg-glass);
197
+ color: var(--gr-overlay-fg);
198
+ backdrop-filter: blur(8px);
199
+ padding: 4px;
200
+ border-radius: 8px;
201
+ box-shadow: 0 8px 24px var(--gr-shadow);
202
+ border: 1px solid var(--gr-overlay-border);
203
+ font-family: system-ui, -apple-system, sans-serif;
204
+ }
205
+ .gr-anchored-panel {
206
+ position: fixed;
207
+ z-index: 10001;
208
+ max-height: calc(100vh - 24px);
209
+ overflow-y: auto;
210
+ background: var(--gr-overlay-bg);
211
+ color: var(--gr-overlay-fg);
212
+ padding: 12px;
213
+ border-radius: 8px;
214
+ box-shadow: 0 12px 32px var(--gr-shadow);
215
+ border: 1px solid var(--gr-overlay-border);
216
+ min-width: 260px;
217
+ max-width: min(340px, calc(100vw - 16px));
218
+ cursor: default;
219
+ font-family: system-ui, -apple-system, sans-serif;
220
+ }
221
+ .gr-icon-btn {
222
+ display: inline-flex;
223
+ align-items: center;
224
+ justify-content: center;
225
+ width: 26px;
226
+ height: 26px;
227
+ border-radius: 6px;
228
+ border: none;
229
+ background: transparent;
230
+ color: var(--gr-fg);
231
+ cursor: pointer;
232
+ padding: 0;
233
+ }
234
+ .gr-icon-btn-primary {
235
+ display: inline-flex;
236
+ align-items: center;
237
+ justify-content: center;
238
+ width: 26px;
239
+ height: 26px;
240
+ border-radius: 6px;
241
+ border: none;
242
+ background: var(--gr-accent);
243
+ color: var(--gr-on-accent);
244
+ cursor: pointer;
245
+ padding: 0;
246
+ }
247
+ .gr-scrim {
248
+ position: fixed;
249
+ inset: 0;
250
+ background: var(--gr-scrim);
251
+ z-index: 10002;
252
+ }
253
+ .gr-drawer {
254
+ position: fixed;
255
+ top: 0;
256
+ right: 0;
257
+ height: 100%;
258
+ width: min(420px, 100vw);
259
+ background: var(--gr-overlay-bg);
260
+ color: var(--gr-overlay-fg);
261
+ z-index: 10003;
262
+ box-shadow: -12px 0 32px var(--gr-shadow);
263
+ display: flex;
264
+ flex-direction: column;
265
+ font-family: system-ui, -apple-system, sans-serif;
266
+ }
267
+ .gr-drawer-head {
268
+ display: flex;
269
+ align-items: center;
270
+ justify-content: space-between;
271
+ padding: 16px 20px;
272
+ border-bottom: 1px solid var(--gr-overlay-border);
273
+ }
274
+ .gr-drawer-title {
275
+ font-size: 14px;
276
+ font-weight: 800;
277
+ margin: 0;
278
+ }
279
+ .gr-drawer-body {
280
+ padding: 20px;
281
+ overflow-y: auto;
282
+ flex: 1;
283
+ }
284
+ .gr-member-row {
285
+ display: flex;
286
+ align-items: center;
287
+ gap: 8px;
288
+ padding: 10px 0;
289
+ border-bottom: 1px solid var(--gr-border-soft);
290
+ }
291
+ .gr-member-email {
292
+ flex: 1;
293
+ font-size: 13px;
294
+ overflow: hidden;
295
+ text-overflow: ellipsis;
296
+ white-space: nowrap;
297
+ }
298
+ .gr-select {
299
+ border: 1px solid var(--gr-border-strong);
300
+ border-radius: 4px;
301
+ padding: 4px 6px;
302
+ font-size: 12px;
303
+ background: var(--gr-input-bg);
304
+ color: var(--gr-fg);
305
+ }
306
+ .gr-badge {
307
+ font-size: 9px;
308
+ font-weight: 800;
309
+ text-transform: uppercase;
310
+ letter-spacing: 0.1em;
311
+ padding: 2px 6px;
312
+ border-radius: 999px;
313
+ background: var(--gr-badge-bg);
314
+ color: var(--gr-badge-fg);
315
+ }
316
+ .gr-section-label {
317
+ font-size: 10px;
318
+ font-weight: 800;
319
+ text-transform: uppercase;
320
+ letter-spacing: 0.15em;
321
+ color: var(--gr-subtle);
322
+ margin: 20px 0 8px;
323
+ }
324
+ .gr-picker-scrim {
325
+ position: fixed;
326
+ inset: 0;
327
+ background: var(--gr-scrim-modal);
328
+ z-index: 10010;
329
+ }
330
+ .gr-picker {
331
+ position: fixed;
332
+ top: 50%;
333
+ left: 50%;
334
+ transform: translate(-50%, -50%);
335
+ width: min(720px, calc(100vw - 32px));
336
+ max-height: min(640px, calc(100vh - 32px));
337
+ background: var(--gr-overlay-bg);
338
+ color: var(--gr-overlay-fg);
339
+ border-radius: 12px;
340
+ z-index: 10011;
341
+ box-shadow: 0 24px 64px var(--gr-shadow);
342
+ display: flex;
343
+ flex-direction: column;
344
+ overflow: hidden;
345
+ font-family: system-ui, -apple-system, sans-serif;
346
+ }
347
+ .gr-picker-grid {
348
+ display: grid;
349
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
350
+ gap: 10px;
351
+ }
352
+ .gr-picker-tile {
353
+ position: relative;
354
+ aspect-ratio: 1;
355
+ border-radius: 8px;
356
+ overflow: hidden;
357
+ border: 2px solid transparent;
358
+ background: var(--gr-soft);
359
+ cursor: pointer;
360
+ padding: 0;
361
+ }
362
+ .gr-picker-tile-active {
363
+ border-color: var(--gr-accent);
364
+ }
365
+ .gr-picker-thumb {
366
+ width: 100%;
367
+ height: 100%;
368
+ object-fit: cover;
369
+ display: block;
370
+ }
371
+ .gr-dropzone {
372
+ border: 1.5px dashed var(--gr-border-strong);
373
+ border-radius: 8px;
374
+ padding: 16px;
375
+ text-align: center;
376
+ font-size: 13px;
377
+ color: var(--gr-muted);
378
+ margin-bottom: 14px;
379
+ cursor: pointer;
380
+ }
381
+ .gr-dropzone-active {
382
+ border-color: var(--gr-accent);
383
+ background: var(--gr-accent-soft);
384
+ color: var(--gr-accent-text);
385
+ }
386
+ .gr-preview-thumb {
387
+ width: 100%;
388
+ max-width: 300px;
389
+ height: auto;
390
+ max-height: 200px;
391
+ object-fit: contain;
392
+ background: var(--gr-soft);
393
+ border-radius: 6px;
394
+ display: block;
395
+ margin-bottom: 8px;
396
+ }
397
+ .gr-secondary-btn {
398
+ background: var(--gr-soft);
399
+ color: var(--gr-fg);
400
+ padding: 7px 12px;
401
+ border-radius: 4px;
402
+ font-size: 12px;
403
+ font-weight: 600;
404
+ border: none;
405
+ cursor: pointer;
406
+ width: 100%;
407
+ margin-bottom: 10px;
408
+ }
409
+ .gr-marker-outline {
410
+ position: fixed;
411
+ z-index: 9990;
412
+ pointer-events: none;
413
+ box-sizing: border-box;
414
+ border-radius: 2px;
415
+ }
416
+ .gr-marker-handle {
417
+ position: fixed;
418
+ z-index: 9991;
419
+ display: inline-flex;
420
+ align-items: center;
421
+ gap: 5px;
422
+ padding: 4px 9px 4px 7px;
423
+ border-radius: 6px;
424
+ border: none;
425
+ background: var(--gr-accent);
426
+ color: var(--gr-on-accent);
427
+ font-size: 11px;
428
+ font-weight: 700;
429
+ font-family: system-ui, -apple-system, sans-serif;
430
+ cursor: pointer;
431
+ box-shadow: 0 4px 12px var(--gr-shadow);
432
+ }
433
+ .gr-repeater-item {
434
+ position: relative;
435
+ outline: 1px dashed var(--gr-field-outline);
436
+ outline-offset: 2px;
437
+ }
438
+ .gr-repeater-toolbar {
439
+ position: absolute;
440
+ top: 4px;
441
+ right: 4px;
442
+ z-index: 20;
443
+ display: flex;
444
+ gap: 2px;
445
+ background: var(--gr-overlay-bg-glass);
446
+ color: var(--gr-overlay-fg);
447
+ backdrop-filter: blur(6px);
448
+ padding: 3px;
449
+ border-radius: 6px;
450
+ box-shadow: 0 4px 12px var(--gr-shadow);
451
+ border: 1px solid var(--gr-overlay-border);
452
+ }
453
+ .gr-repeater-add-row {
454
+ display: flex;
455
+ gap: 8px;
456
+ align-items: center;
457
+ margin-top: 8px;
458
+ font-family: system-ui, -apple-system, sans-serif;
459
+ }
460
+ .gr-repeater-add-btn {
461
+ display: inline-flex;
462
+ align-items: center;
463
+ gap: 6px;
464
+ padding: 8px 14px;
465
+ border-radius: 6px;
466
+ border: 1.5px dashed var(--gr-accent);
467
+ background: transparent;
468
+ color: var(--gr-accent);
469
+ font-size: 12px;
470
+ font-weight: 700;
471
+ cursor: pointer;
472
+ }
473
+ .gr-repeater-slug-input {
474
+ border: 1px solid var(--gr-border-strong);
475
+ border-radius: 4px;
476
+ padding: 7px 9px;
477
+ font-size: 13px;
478
+ color: var(--gr-fg);
479
+ background: var(--gr-input-bg);
480
+ min-width: 180px;
481
+ }
482
+
483
+ /* ---- modifiers and utilities ----------------------------------------------
484
+ * Single-class rules that follow the components above, so they win over a
485
+ * component's own value when both are on one element. */
486
+
487
+ .gr-text-fg { color: var(--gr-fg); }
488
+ .gr-text-muted { color: var(--gr-muted); }
489
+ .gr-text-subtle { color: var(--gr-subtle); }
490
+ .gr-text-danger { color: var(--gr-danger); }
491
+ .gr-fs-11 { font-size: 11px; }
492
+ .gr-fs-12 { font-size: 12px; }
493
+ .gr-fs-13 { font-size: 13px; }
494
+ .gr-lh-15 { line-height: 1.5; }
495
+ .gr-m-0 { margin: 0; }
496
+ .gr-mx-6 { margin-left: 6px; margin-right: 6px; }
497
+ .gr-mt-8 { margin-top: 8px; }
498
+ .gr-mt-10 { margin-top: 10px; }
499
+ .gr-mt-16 { margin-top: 16px; }
500
+ .gr-mb-0 { margin-bottom: 0; }
501
+ .gr-mb-8 { margin-bottom: 8px; }
502
+ .gr-mb-12 { margin-bottom: 12px; }
503
+ .gr-mb-16 { margin-bottom: 16px; }
504
+ .gr-p-0 { padding: 0; }
505
+ .gr-w-full { width: 100%; }
506
+ .gr-w-auto { width: auto; }
507
+ .gr-size-22 { width: 22px; height: 22px; }
508
+ .gr-size-24 { width: 24px; height: 24px; }
509
+ .gr-flex { display: flex; }
510
+ .gr-inline-flex { display: inline-flex; }
511
+ .gr-flex-1 { flex: 1; }
512
+ .gr-items-center { align-items: center; }
513
+ .gr-justify-between { justify-content: space-between; }
514
+ .gr-justify-end { justify-content: flex-end; }
515
+ .gr-gap-4 { gap: 4px; }
516
+ .gr-gap-6 { gap: 6px; }
517
+ .gr-gap-8 { gap: 8px; }
518
+ .gr-no-underline { text-decoration: none; }
519
+
520
+ /* A text-only button, e.g. "Forgot password?". */
521
+ .gr-text-btn {
522
+ background: none;
523
+ border: none;
524
+ cursor: pointer;
525
+ color: var(--gr-muted);
526
+ font-size: 12px;
527
+ text-decoration: underline;
528
+ padding: 0 4px;
529
+ white-space: nowrap;
530
+ }
531
+
532
+ /* ---- editable fields ------------------------------------------------------
533
+ * Drawn on the developer's own elements by <GRail>, which marks them with a
534
+ * data attribute instead of a style attribute. Images get a separate marker. */
535
+
536
+ [data-gr-field] { cursor: pointer; }
537
+ [data-gr-field="text"] { outline: 1px dashed var(--gr-field-outline); outline-offset: 2px; }
538
+ [data-gr-field="text"][data-gr-editing] { outline: 2px solid var(--gr-field-outline-active); cursor: text; }
539
+
540
+ .gr-marker-outline { border: 1px dashed var(--gr-field-outline); }
541
+ .gr-marker-outline[data-gr-editing] { border: 2px solid var(--gr-field-outline-active); }
542
+
543
+ /* ---- drawer animation ------------------------------------------------------ */
544
+ @keyframes gr-fade-in { from { opacity: 0 } to { opacity: 1 } }
545
+ @keyframes gr-slide-in {
546
+ from { transform: translateX(100%) }
547
+ to { transform: translateX(0) }
548
+ }
549
+ [data-guardrail-scrim] { animation: gr-fade-in 160ms ease-out both }
550
+ [data-guardrail-team] { animation: gr-slide-in 220ms cubic-bezier(0.32, 0.72, 0, 1) both }
551
+ [data-guardrail-team] [data-gr-row] { animation: gr-fade-in 240ms ease-out both }
552
+ @keyframes gr-fade-out { from { opacity: 1 } to { opacity: 0 } }
553
+ @keyframes gr-slide-out {
554
+ from { transform: translateX(0) }
555
+ to { transform: translateX(100%) }
556
+ }
557
+ [data-guardrail-scrim][data-closing] { animation: gr-fade-out 180ms ease-in both }
558
+ [data-guardrail-team][data-closing] { animation: gr-slide-out 180ms cubic-bezier(0.4, 0, 1, 1) both }
559
+ @media (prefers-reduced-motion: reduce) {
560
+ [data-guardrail-scrim], [data-guardrail-team], [data-guardrail-team] [data-gr-row] {
561
+ animation: none
562
+ }
563
+ }
@@ -1,5 +1,5 @@
1
1
  import { GRRole, GRAsset, GRMember, OverrideValue, SetPasswordState, ActionState, OverrideRow } from '../core/types.js';
2
- import { GRUploadTarget, AuthRedirectParams } from '../core/adapter-types.js';
2
+ import { GRUploadTarget, GRRevertInfo, AuthRedirectParams } from '../core/adapter-types.js';
3
3
  import 'react';
4
4
 
5
5
  /** `notice` is for a success the admin should still read, e.g. "no email was sent because...". */
@@ -28,6 +28,23 @@ declare function saveOverride(row: OverrideRow): Promise<Result>;
28
28
  * <GRail>, and vice versa.
29
29
  */
30
30
  declare function setFieldValue(id: string, value: string | OverrideValue): Promise<Result>;
31
+ /**
32
+ * What reverting this field would do — "previous" (with the date of the version
33
+ * it goes back to), "default" (back to what the page itself says), or "none".
34
+ * The editor asks when it opens, so visitors never pay for it.
35
+ */
36
+ declare function getRevertInfo(id: string): Promise<GRRevertInfo>;
37
+ /**
38
+ * One step back for a field. Restores its previous saved value and returns it,
39
+ * or, with none left, removes the saved value (`action: "reset"`) so the field
40
+ * shows the default written in the page. Same permission as saving.
41
+ */
42
+ declare function revertField(id: string): Promise<{
43
+ success: boolean;
44
+ error?: string;
45
+ action?: "restored" | "reset" | "none";
46
+ value?: OverrideValue;
47
+ }>;
31
48
  /**
32
49
  * Authorizes an upload and returns where the browser should send the bytes.
33
50
  * Only metadata comes through here — the file itself goes straight to storage.
@@ -100,4 +117,4 @@ declare function skipSetPasswordAction(): Promise<{
100
117
  success: boolean;
101
118
  }>;
102
119
 
103
- export { completeAuthRedirectAction, createAssetUpload, forgotPasswordAction, inviteMember, listAssets, listTeam, loginAction, logoutAction, removeMember, saveOverride, sendPasswordResetAction, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole };
120
+ export { completeAuthRedirectAction, createAssetUpload, forgotPasswordAction, getRevertInfo, inviteMember, listAssets, listTeam, loginAction, logoutAction, removeMember, revertField, saveOverride, sendPasswordResetAction, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole };
@@ -57,6 +57,39 @@ async function writeOverride(rawPathKey, rawValue) {
57
57
  return { success: false, error: "Database write failed." };
58
58
  }
59
59
  }
60
+ async function getRevertInfo(id) {
61
+ const viewer = await requireRole(["admin", "editor"]);
62
+ const pathKey = validPathKey(id);
63
+ if (!viewer || !pathKey) return { kind: "none" };
64
+ const db = getDB();
65
+ if (!db.getRevertInfo) return { kind: "none" };
66
+ try {
67
+ return await db.getRevertInfo(pathKey, await resolveSiteId());
68
+ } catch (e) {
69
+ rethrowIfBailout(e);
70
+ return { kind: "none" };
71
+ }
72
+ }
73
+ async function revertField(id) {
74
+ const viewer = await requireRole(["admin", "editor"]);
75
+ if (!viewer) return DENIED;
76
+ const pathKey = validPathKey(id);
77
+ if (!pathKey) return { success: false, error: "A valid field id is required." };
78
+ const db = getDB();
79
+ if (!db.revertOverride) {
80
+ return { success: false, error: "This site's database setup doesn't keep history, so there's nothing to revert to." };
81
+ }
82
+ try {
83
+ const res = await db.revertOverride(pathKey, await resolveSiteId());
84
+ if (!res.success) return { success: false, error: res.error };
85
+ revalidatePath("/", "layout");
86
+ return res.action === "restored" ? { success: true, action: "restored", value: res.value } : { success: true, action: res.action };
87
+ } catch (e) {
88
+ rethrowIfBailout(e);
89
+ console.error("Guardrail: revert failed", e);
90
+ return { success: false, error: "Couldn't revert that." };
91
+ }
92
+ }
60
93
  const IMAGE_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif", "image/avif"];
61
94
  const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
62
95
  async function createAssetUpload(file) {
@@ -381,12 +414,14 @@ export {
381
414
  completeAuthRedirectAction,
382
415
  createAssetUpload,
383
416
  forgotPasswordAction,
417
+ getRevertInfo,
384
418
  inviteMember,
385
419
  listAssets,
386
420
  listTeam,
387
421
  loginAction,
388
422
  logoutAction,
389
423
  removeMember,
424
+ revertField,
390
425
  saveOverride,
391
426
  sendPasswordResetAction,
392
427
  setFieldValue,