@guard-rail/core 0.5.0 → 0.7.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.
- package/README.md +71 -9
- package/dist/core/defs.d.ts +13 -1
- package/dist/core/defs.js +2 -0
- package/dist/core/defs.js.map +1 -1
- package/dist/core/types.d.ts +8 -0
- package/dist/index.client.d.ts +5 -2
- package/dist/index.client.js +18 -1
- package/dist/index.client.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/react/Admin.js +14 -12
- package/dist/react/Admin.js.map +1 -1
- package/dist/react/GRail/client.js +43 -3
- package/dist/react/GRail/client.js.map +1 -1
- package/dist/react/Icons.d.ts +2 -1
- package/dist/react/Icons.js +6 -0
- package/dist/react/Icons.js.map +1 -1
- package/dist/react/Repeater/client.d.ts +2 -0
- package/dist/react/Repeater/client.js +15 -1
- package/dist/react/Repeater/client.js.map +1 -1
- package/dist/react/Repeater/index.d.ts +8 -1
- package/dist/react/Repeater/index.js +19 -1
- package/dist/react/Repeater/index.js.map +1 -1
- package/dist/react/Syncer.d.ts +3 -2
- package/dist/react/Syncer.js +6 -2
- package/dist/react/Syncer.js.map +1 -1
- package/dist/react/openHref.d.ts +23 -0
- package/dist/react/openHref.js +25 -0
- package/dist/react/openHref.js.map +1 -0
- package/dist/server/access.d.ts +77 -0
- package/dist/server/access.js +33 -0
- package/dist/server/access.js.map +1 -0
- package/dist/server/actions.d.ts +20 -7
- package/dist/server/actions.js +20 -9
- package/dist/server/actions.js.map +1 -1
- package/dist/server/fieldData.d.ts +4 -2
- package/dist/server/fieldData.js.map +1 -1
- package/dist/server/repeaterActions.js +1 -1
- package/dist/server/repeaterActions.js.map +1 -1
- package/dist/server/wrapper.js +5 -5
- package/dist/server/wrapper.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,16 +119,26 @@ Links take `as="a"` (so `href` type-checks) and edit both the text and the URL:
|
|
|
119
119
|
<GRail id="nav.cta" as="a" type="link" href="/contact">Get in touch</GRail>
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
Clicking a link in edit mode opens its editor rather than following it, so the
|
|
123
|
+
editor has an **Open** button to go to the page — same tab for your own pages
|
|
124
|
+
(you stay signed in and in edit mode), a new tab for other sites. That's how an
|
|
125
|
+
editor moves around a site whose navigation is all `<GRail>` links.
|
|
126
|
+
|
|
122
127
|
To edit, visit any page with `?edit=true`, sign in with a Supabase user, and
|
|
123
128
|
click a field. `?edit=true` itself only grants a short (~10 minute) window to
|
|
124
129
|
reach the sign-in form — a normal visitor never sees any Guardrail chrome, and
|
|
125
130
|
a stray copy of that window (a shared computer, an old tab) self-expires
|
|
126
131
|
rather than showing a sign-in bar indefinitely. It's a dismissable prompt, not
|
|
127
132
|
a standing invitation: a signed-in editor's toolbar isn't on that timer at
|
|
128
|
-
all, and persists for as long as their real session does
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
all, and persists for as long as their real session does.
|
|
134
|
+
|
|
135
|
+
The toolbar's exit button ends the editing session, not the sign-in: the
|
|
136
|
+
overlay goes away, and `?edit=true` brings it straight back with no password.
|
|
137
|
+
The sign-in form only reappears once the session itself is gone (expired, or
|
|
138
|
+
ended elsewhere). `?edit=false` does the same exit from the URL, and an exit
|
|
139
|
+
holds until the next `?edit=true` rather than lapsing on a timer. (Someone
|
|
140
|
+
signed in to an account with no role on the site gets a real **Sign out**
|
|
141
|
+
instead, to switch accounts.)
|
|
132
142
|
|
|
133
143
|
### In Client Components
|
|
134
144
|
|
|
@@ -344,9 +354,13 @@ Editors get add/remove/reorder controls; anonymous visitors get the template
|
|
|
344
354
|
rendered N times with no admin JS, same SSR/CSR split as `<GRail>` itself.
|
|
345
355
|
|
|
346
356
|
**Routed items** — a product, a blog post — additionally get their own page.
|
|
347
|
-
Guardrail never owns routing: pass `routed
|
|
348
|
-
|
|
349
|
-
|
|
357
|
+
Guardrail never owns routing: pass `routed` with the base path your dynamic
|
|
358
|
+
route lives at (`routed="/products"` for `app/products/[slug]/page.tsx`). That
|
|
359
|
+
prompts for a slug when an item is added, and gives each item's editing toolbar
|
|
360
|
+
an **Open** button to its page — otherwise an editor could only reach it by
|
|
361
|
+
typing the URL, since clicking its fields edits them. (`routed` alone, without
|
|
362
|
+
a path, still works; it just has no Open button.) The developer writes an
|
|
363
|
+
ordinary dynamic route that resolves the item by that slug:
|
|
350
364
|
|
|
351
365
|
```tsx
|
|
352
366
|
// app/products/[slug]/page.tsx
|
|
@@ -389,6 +403,52 @@ scratch. A visual page builder (Puck, say) is a better fit for that and can
|
|
|
389
403
|
sit alongside `<GRepeater>` later — it would decide composition, while
|
|
390
404
|
`<GRail>` still owns each block's leaf content.
|
|
391
405
|
|
|
406
|
+
## Custom editing UI
|
|
407
|
+
|
|
408
|
+
`<GRail>` edits what's on the page. Some fields have nothing to click — an SEO
|
|
409
|
+
title, a meta description, an og:image — and some data isn't copy at all, like
|
|
410
|
+
stock or price. For those, build a small drawer of your own and keep the
|
|
411
|
+
authorization in Guardrail's hands:
|
|
412
|
+
|
|
413
|
+
- **`getFieldValue(id)`** reads any field (server-side).
|
|
414
|
+
- **`setFieldValue(id, value)`** writes one. A string is shorthand for a text
|
|
415
|
+
field; pass `{ type: "image", src, alt }` etc. for the others. It checks the
|
|
416
|
+
caller is an editor or admin itself, and writes the same row a `<GRail>` with
|
|
417
|
+
that id would, so the two stay interchangeable.
|
|
418
|
+
- **`isEditor()` / `isAdmin()`** are the role checks for your *own* Server
|
|
419
|
+
Actions. `isEditor()` is true for editors and admins; `isAdmin()` only for
|
|
420
|
+
admins (team management).
|
|
421
|
+
- **`canEdit()`** is true when the viewer holds a role *and* the site is in
|
|
422
|
+
edit mode — the one check for "show my editing UI." Use it in Server
|
|
423
|
+
Components; Client Components read the same flag as `useGRail().canEdit`.
|
|
424
|
+
|
|
425
|
+
```tsx
|
|
426
|
+
// app/products/[slug]/actions.ts — a hand-rolled save for your own data
|
|
427
|
+
"use server";
|
|
428
|
+
import { isEditor } from "@guard-rail/core";
|
|
429
|
+
|
|
430
|
+
export async function restock(sku: string, quantity: number) {
|
|
431
|
+
if (!(await isEditor())) return { success: false, error: "Unauthorized." };
|
|
432
|
+
// ...write to your own stock table
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Show the trigger only when `canEdit` is true — not `isEditMode` alone, since
|
|
437
|
+
edit mode can be switched on by anyone visiting `?edit=true`; `canEdit` also
|
|
438
|
+
requires a role on the site. In a small client component:
|
|
439
|
+
|
|
440
|
+
```tsx
|
|
441
|
+
"use client";
|
|
442
|
+
function StockEditTrigger({ children }: { children: React.ReactNode }) {
|
|
443
|
+
const { canEdit } = useGRail();
|
|
444
|
+
if (!canEdit) return <>{children}</>;
|
|
445
|
+
return <button onClick={openDrawer}>{children}</button>;
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
`canEdit` only decides what to *show*: every action behind it must still call
|
|
450
|
+
`isEditor()` itself, because a hidden button is not authorization.
|
|
451
|
+
|
|
392
452
|
## Appearance
|
|
393
453
|
|
|
394
454
|
Guardrail's own chrome (sign-in bar, toolbar, edit popups, image picker, team
|
|
@@ -406,7 +466,7 @@ surrounding `<Link>`).
|
|
|
406
466
|
|
|
407
467
|
## Extending the toolbar
|
|
408
468
|
|
|
409
|
-
The bundled toolbar (edit indicator, Team,
|
|
469
|
+
The bundled toolbar (edit indicator, Team, Exit edit mode) has one extension point:
|
|
410
470
|
|
|
411
471
|
```tsx
|
|
412
472
|
withGuardrail(RootLayout, {
|
|
@@ -450,7 +510,9 @@ export default withGuardrail(RootLayout, {
|
|
|
450
510
|
});
|
|
451
511
|
```
|
|
452
512
|
|
|
453
|
-
Only `getSession()` and `logout()` are required
|
|
513
|
+
Only `getSession()` and `logout()` are required (`logout()` backs the **Sign out** shown to
|
|
514
|
+
an account with no role on the site — the toolbar's normal exit button ends edit mode but
|
|
515
|
+
keeps the session, so it doesn't call it). `authenticate` is optional —
|
|
454
516
|
adapters built on magic links, SSO, or a hosted sign-in page omit it and set
|
|
455
517
|
`signInUrl`, and Guardrail links there instead of rendering a password form it
|
|
456
518
|
cannot fulfil. `inviteUser` is optional too; without it, invites fall back to
|
package/dist/core/defs.d.ts
CHANGED
|
@@ -36,6 +36,18 @@ declare const GR_COOKIES: {
|
|
|
36
36
|
* logout.
|
|
37
37
|
*/
|
|
38
38
|
declare const GR_EDIT_MODE_TTL_SECONDS: number;
|
|
39
|
+
/**
|
|
40
|
+
* How long an explicit "off" (the toolbar's exit button, ?edit=false, or the
|
|
41
|
+
* sign-in bar's dismiss) is remembered. Long, unlike "true" above, because the
|
|
42
|
+
* two fail differently: a stale "true" can put a sign-in bar in front of
|
|
43
|
+
* someone who never asked for it, so it's kept short — a stale "false" only
|
|
44
|
+
* ever hides chrome, so there's nothing to bound. It has to last as long as
|
|
45
|
+
* the session it sits on top of: an editor who exited stays exited (instead of
|
|
46
|
+
* the overlay reappearing by itself once a short timer lapsed) until they ask
|
|
47
|
+
* for edit mode again with ?edit=true. Matches the Supabase session cookie's
|
|
48
|
+
* 400 days.
|
|
49
|
+
*/
|
|
50
|
+
declare const GR_EDIT_MODE_OFF_TTL_SECONDS: number;
|
|
39
51
|
/**
|
|
40
52
|
* How long the set-a-password prompt stays up after an invite/recovery link
|
|
41
53
|
* completes. Longer than the edit-mode window above — picking a password
|
|
@@ -60,4 +72,4 @@ declare const GR_TABLE = "gr_content";
|
|
|
60
72
|
declare const GR_MEMBERS_TABLE = "gr_members";
|
|
61
73
|
declare const GR_REPEATER_TABLE = "gr_repeater_items";
|
|
62
74
|
|
|
63
|
-
export { GR_COOKIES, GR_EDIT_MODE_TTL_SECONDS, GR_MEMBERS_TABLE, GR_NEEDS_PASSWORD_TTL_SECONDS, GR_REPEATER_TABLE, GR_SCHEMA, GR_TABLE };
|
|
75
|
+
export { GR_COOKIES, GR_EDIT_MODE_OFF_TTL_SECONDS, GR_EDIT_MODE_TTL_SECONDS, GR_MEMBERS_TABLE, GR_NEEDS_PASSWORD_TTL_SECONDS, GR_REPEATER_TABLE, GR_SCHEMA, GR_TABLE };
|
package/dist/core/defs.js
CHANGED
|
@@ -26,6 +26,7 @@ const GR_COOKIES = {
|
|
|
26
26
|
NEEDS_PASSWORD: "gr_needs_password"
|
|
27
27
|
};
|
|
28
28
|
const GR_EDIT_MODE_TTL_SECONDS = 10 * 60;
|
|
29
|
+
const GR_EDIT_MODE_OFF_TTL_SECONDS = 400 * 24 * 60 * 60;
|
|
29
30
|
const GR_NEEDS_PASSWORD_TTL_SECONDS = 30 * 60;
|
|
30
31
|
const GR_SCHEMA = "guardrail";
|
|
31
32
|
const GR_TABLE = "gr_content";
|
|
@@ -33,6 +34,7 @@ const GR_MEMBERS_TABLE = "gr_members";
|
|
|
33
34
|
const GR_REPEATER_TABLE = "gr_repeater_items";
|
|
34
35
|
export {
|
|
35
36
|
GR_COOKIES,
|
|
37
|
+
GR_EDIT_MODE_OFF_TTL_SECONDS,
|
|
36
38
|
GR_EDIT_MODE_TTL_SECONDS,
|
|
37
39
|
GR_MEMBERS_TABLE,
|
|
38
40
|
GR_NEEDS_PASSWORD_TTL_SECONDS,
|
package/dist/core/defs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/defs.ts"],"sourcesContent":["export const GR_COOKIES = {\n /**\n * Signals intent to edit. Carries no authority on its own — every write\n * still goes through a real, server-verified role check, independent of\n * this — but it does decide whether the admin chrome (the sign-in bar, or\n * the toolbar) renders at all, so it is not harmless either: a stale copy\n * of it is a real, if low-stakes, exposure (\"there's a CMS here\") to\n * whoever's browser it's sitting in next, not just its original visitor.\n *\n * Values: \"true\" (show chrome — the short-lived grace window below, for\n * someone not yet authenticated to reach the sign-in form), \"false\"\n * (explicitly suppressed, e.g. ?edit=false previewing without chrome while\n * still signed in), or absent. Absent defers entirely to whether a real\n * session exists (see withGuardrail) — an authenticated editor's chrome\n * persists for as long as their actual session does, with no separate\n * timer of its own to fall out of sync with it.\n */\n EDIT_MODE: \"gr_edit_mode\",\n /**\n * Set server-side the moment a redirect-carried session (an invite or\n * password-recovery link) completes, so the next render — which has no\n * other way to know that just happened — prompts to set a password\n * instead of showing the normal toolbar. Cleared once they do, or once\n * they explicitly skip.\n */\n NEEDS_PASSWORD: \"gr_needs_password\",\n} as const;\n\n/**\n * How long the edit-mode cookie's \"true\"/\"false\" values last before\n * reverting to the session-derived default. Short on purpose: it only ever\n * needs to outlive the handful of clicks between landing on ?edit=true and\n * actually typing a password (or, for ?edit=false, a quick chrome-free\n * preview) — not a whole working session, which persists through real auth\n * instead. Bounds how long a stray copy of this cookie can show anything to\n * whoever's browser it's left in, self-healing without needing an explicit\n * logout.\n */\nexport const GR_EDIT_MODE_TTL_SECONDS = 10 * 60;\n\n/**\n * How long the set-a-password prompt stays up after an invite/recovery link\n * completes. Longer than the edit-mode window above — picking a password\n * takes more than one click — but still bounded, rather than nagging\n * indefinitely if it's skipped and the cookie is somehow never cleared.\n */\nexport const GR_NEEDS_PASSWORD_TTL_SECONDS = 30 * 60;\n\n/**\n * Dedicated Postgres schema, not `public`. A name prefix alone only guards\n * against accidental collisions; it does nothing against a `supabase db\n * reset`, a broad `DROP ... CASCADE`, or a migration tool that diffs and\n * \"reconciles\" everything in `public` as if it owns that namespace. A\n * separate schema keeps Guardrail's tables outside that blast radius —\n * the same reason auth.* and storage.* are their own schemas, not public.*.\n *\n * Requires \"guardrail\" to be added under Settings > API > Exposed schemas in\n * the Supabase dashboard — PostgREST only serves schemas on that list, and no\n * migration can add itself to it.\n */\nexport const GR_SCHEMA = \"guardrail\";\n\nexport const GR_TABLE = \"gr_content\";\nexport const GR_MEMBERS_TABLE = \"gr_members\";\nexport const GR_REPEATER_TABLE = \"gr_repeater_items\";\n"],"mappings":"AAAO,MAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBxB,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQX,gBAAgB;AAClB;AAYO,MAAM,2BAA2B,KAAK;
|
|
1
|
+
{"version":3,"sources":["../../src/core/defs.ts"],"sourcesContent":["export const GR_COOKIES = {\n /**\n * Signals intent to edit. Carries no authority on its own — every write\n * still goes through a real, server-verified role check, independent of\n * this — but it does decide whether the admin chrome (the sign-in bar, or\n * the toolbar) renders at all, so it is not harmless either: a stale copy\n * of it is a real, if low-stakes, exposure (\"there's a CMS here\") to\n * whoever's browser it's sitting in next, not just its original visitor.\n *\n * Values: \"true\" (show chrome — the short-lived grace window below, for\n * someone not yet authenticated to reach the sign-in form), \"false\"\n * (explicitly suppressed, e.g. ?edit=false previewing without chrome while\n * still signed in), or absent. Absent defers entirely to whether a real\n * session exists (see withGuardrail) — an authenticated editor's chrome\n * persists for as long as their actual session does, with no separate\n * timer of its own to fall out of sync with it.\n */\n EDIT_MODE: \"gr_edit_mode\",\n /**\n * Set server-side the moment a redirect-carried session (an invite or\n * password-recovery link) completes, so the next render — which has no\n * other way to know that just happened — prompts to set a password\n * instead of showing the normal toolbar. Cleared once they do, or once\n * they explicitly skip.\n */\n NEEDS_PASSWORD: \"gr_needs_password\",\n} as const;\n\n/**\n * How long the edit-mode cookie's \"true\"/\"false\" values last before\n * reverting to the session-derived default. Short on purpose: it only ever\n * needs to outlive the handful of clicks between landing on ?edit=true and\n * actually typing a password (or, for ?edit=false, a quick chrome-free\n * preview) — not a whole working session, which persists through real auth\n * instead. Bounds how long a stray copy of this cookie can show anything to\n * whoever's browser it's left in, self-healing without needing an explicit\n * logout.\n */\nexport const GR_EDIT_MODE_TTL_SECONDS = 10 * 60;\n\n/**\n * How long an explicit \"off\" (the toolbar's exit button, ?edit=false, or the\n * sign-in bar's dismiss) is remembered. Long, unlike \"true\" above, because the\n * two fail differently: a stale \"true\" can put a sign-in bar in front of\n * someone who never asked for it, so it's kept short — a stale \"false\" only\n * ever hides chrome, so there's nothing to bound. It has to last as long as\n * the session it sits on top of: an editor who exited stays exited (instead of\n * the overlay reappearing by itself once a short timer lapsed) until they ask\n * for edit mode again with ?edit=true. Matches the Supabase session cookie's\n * 400 days.\n */\nexport const GR_EDIT_MODE_OFF_TTL_SECONDS = 400 * 24 * 60 * 60;\n\n/**\n * How long the set-a-password prompt stays up after an invite/recovery link\n * completes. Longer than the edit-mode window above — picking a password\n * takes more than one click — but still bounded, rather than nagging\n * indefinitely if it's skipped and the cookie is somehow never cleared.\n */\nexport const GR_NEEDS_PASSWORD_TTL_SECONDS = 30 * 60;\n\n/**\n * Dedicated Postgres schema, not `public`. A name prefix alone only guards\n * against accidental collisions; it does nothing against a `supabase db\n * reset`, a broad `DROP ... CASCADE`, or a migration tool that diffs and\n * \"reconciles\" everything in `public` as if it owns that namespace. A\n * separate schema keeps Guardrail's tables outside that blast radius —\n * the same reason auth.* and storage.* are their own schemas, not public.*.\n *\n * Requires \"guardrail\" to be added under Settings > API > Exposed schemas in\n * the Supabase dashboard — PostgREST only serves schemas on that list, and no\n * migration can add itself to it.\n */\nexport const GR_SCHEMA = \"guardrail\";\n\nexport const GR_TABLE = \"gr_content\";\nexport const GR_MEMBERS_TABLE = \"gr_members\";\nexport const GR_REPEATER_TABLE = \"gr_repeater_items\";\n"],"mappings":"AAAO,MAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBxB,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQX,gBAAgB;AAClB;AAYO,MAAM,2BAA2B,KAAK;AAatC,MAAM,+BAA+B,MAAM,KAAK,KAAK;AAQrD,MAAM,gCAAgC,KAAK;AAc3C,MAAM,YAAY;AAElB,MAAM,WAAW;AACjB,MAAM,mBAAmB;AACzB,MAAM,oBAAoB;","names":[]}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -119,6 +119,14 @@ interface GRState {
|
|
|
119
119
|
siteId: string;
|
|
120
120
|
/** Null when the signed-in user holds no role on this site. */
|
|
121
121
|
role: GRRole | null;
|
|
122
|
+
/**
|
|
123
|
+
* The viewer holds a role on this site AND the site is in edit mode — the
|
|
124
|
+
* one flag to gate custom editing UI on. Equivalent to
|
|
125
|
+
* `isEditMode && role !== null`; server-side, the same answer is canEdit().
|
|
126
|
+
* Decides what to *show* only — a Server Action behind it must still call
|
|
127
|
+
* isEditor() itself.
|
|
128
|
+
*/
|
|
129
|
+
canEdit: boolean;
|
|
122
130
|
email?: string;
|
|
123
131
|
auth: GRAuthCapabilities;
|
|
124
132
|
/** A storage adapter is configured, so images can be uploaded and picked. */
|
package/dist/index.client.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { GRail } from './react/GRail/universal.js';
|
|
2
2
|
export { useGRail } from './react/Provider.js';
|
|
3
|
-
export { createAssetUpload, inviteMember, listAssets, listTeam, removeMember, setPasswordAction, skipSetPasswordAction, updateMemberRole } from './server/actions.js';
|
|
3
|
+
export { createAssetUpload, inviteMember, listAssets, listTeam, removeMember, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole } from './server/actions.js';
|
|
4
4
|
export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from './server/repeaterActions.js';
|
|
5
5
|
export { GRepeaterProps, RepeaterItemProps } from './react/Repeater/index.js';
|
|
6
6
|
export { ActionState, GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps, SetPasswordState } from './core/types.js';
|
|
@@ -14,5 +14,8 @@ declare const GRepeater: (..._args: unknown[]) => never;
|
|
|
14
14
|
declare const listRepeaterItems: (..._args: unknown[]) => never;
|
|
15
15
|
declare const getRepeaterItemBySlug: (..._args: unknown[]) => never;
|
|
16
16
|
declare const getFieldValue: (..._args: unknown[]) => never;
|
|
17
|
+
declare const isAdmin: (..._args: unknown[]) => never;
|
|
18
|
+
declare const isEditor: (..._args: unknown[]) => never;
|
|
19
|
+
declare const canEdit: (..._args: unknown[]) => never;
|
|
17
20
|
|
|
18
|
-
export { GRepeater, configureGuardrail, getFieldValue, getRepeaterItemBySlug, listRepeaterItems, withGuardrail };
|
|
21
|
+
export { GRepeater, canEdit, configureGuardrail, getFieldValue, getRepeaterItemBySlug, isAdmin, isEditor, listRepeaterItems, withGuardrail };
|
package/dist/index.client.js
CHANGED
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
createAssetUpload,
|
|
9
9
|
listAssets,
|
|
10
10
|
setPasswordAction,
|
|
11
|
-
skipSetPasswordAction
|
|
11
|
+
skipSetPasswordAction,
|
|
12
|
+
setFieldValue
|
|
12
13
|
} from "./server/actions";
|
|
13
14
|
import {
|
|
14
15
|
addRepeaterItem,
|
|
@@ -46,21 +47,37 @@ const getFieldValue = serverOnly(
|
|
|
46
47
|
"getFieldValue()",
|
|
47
48
|
"Read the value in a Server Component and pass it down as a prop."
|
|
48
49
|
);
|
|
50
|
+
const isAdmin = serverOnly(
|
|
51
|
+
"isAdmin()",
|
|
52
|
+
"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show."
|
|
53
|
+
);
|
|
54
|
+
const isEditor = serverOnly(
|
|
55
|
+
"isEditor()",
|
|
56
|
+
"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show."
|
|
57
|
+
);
|
|
58
|
+
const canEdit = serverOnly(
|
|
59
|
+
"canEdit()",
|
|
60
|
+
"Call it in a Server Component; in a Client Component, read `canEdit` from useGRail()."
|
|
61
|
+
);
|
|
49
62
|
export {
|
|
50
63
|
GRail,
|
|
51
64
|
GRepeater,
|
|
52
65
|
addRepeaterItem,
|
|
66
|
+
canEdit,
|
|
53
67
|
configureGuardrail,
|
|
54
68
|
createAssetUpload,
|
|
55
69
|
getFieldValue,
|
|
56
70
|
getRepeaterItemBySlug,
|
|
57
71
|
inviteMember,
|
|
72
|
+
isAdmin,
|
|
73
|
+
isEditor,
|
|
58
74
|
listAssets,
|
|
59
75
|
listRepeaterItems,
|
|
60
76
|
listTeam,
|
|
61
77
|
removeMember,
|
|
62
78
|
removeRepeaterItem,
|
|
63
79
|
reorderRepeaterItems,
|
|
80
|
+
setFieldValue,
|
|
64
81
|
setPasswordAction,
|
|
65
82
|
skipSetPasswordAction,
|
|
66
83
|
updateMemberRole,
|
package/dist/index.client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.client.ts"],"sourcesContent":["// Client-graph entry. `import ... from \"@guard-rail/core\"` resolves here from a\n// Client Component (see \"exports\" in package.json) and to ./index from a\n// Server Component, so an app uses one import path everywhere.\n//\n// Nothing server-only may be imported here — this is bundled for the browser.\n// Server Actions are fine: a \"use server\" module is replaced by references in\n// the client bundle, which is what lets an app build its own team/repeater UI\n// in a Client Component.\n\nexport { GRail } from \"./react/GRail/universal\";\nexport { useGRail } from \"./react/Provider\";\n\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n} from \"./server/actions\";\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\n\n/**\n * Stand-ins for the server-only exports, so importing one from a Client\n * Component fails with an explanation instead of an undefined-is-not-a-function\n * deep in a render. Also what a tool that doesn't set the \"react-server\"\n * condition (a test runner, a plain Node script) gets from the package root —\n * `@guard-rail/core/server` is the explicit path to the real ones there.\n */\nfunction serverOnly(name: string, hint: string) {\n return (..._args: unknown[]): never => {\n throw new Error(\n `Guardrail: ${name} only works in a Server Component. ${hint} ` +\n `(If this isn't a Client Component, your tooling may not set the ` +\n `\"react-server\" condition — import from \"@guard-rail/core/server\" instead.)`,\n );\n };\n}\n\nexport const withGuardrail = serverOnly(\n \"withGuardrail()\",\n \"Call it in your root layout, which is a Server Component.\",\n);\nexport const configureGuardrail = serverOnly(\n \"configureGuardrail()\",\n \"Configuration is applied by withGuardrail() in your root layout.\",\n);\nexport const GRepeater = serverOnly(\n \"<GRepeater>\",\n \"Render it in a Server Component and pass it into this one as children or a prop.\",\n);\nexport const listRepeaterItems = serverOnly(\n \"listRepeaterItems()\",\n \"Read repeater items in a Server Component and pass them down.\",\n);\nexport const getRepeaterItemBySlug = serverOnly(\n \"getRepeaterItemBySlug()\",\n \"Resolve the item in a Server Component (your [slug]/page.tsx) and pass it down.\",\n);\nexport const getFieldValue = serverOnly(\n \"getFieldValue()\",\n \"Read the value in a Server Component and pass it down as a prop.\",\n);\n\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n} from \"./core/adapter-types\";\n"],"mappings":"AASA,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAEzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,WAAW,MAAc,MAAc;AAC9C,SAAO,IAAI,UAA4B;AACrC,UAAM,IAAI;AAAA,MACR,cAAc,IAAI,sCAAsC,IAAI;AAAA,IAG9D;AAAA,EACF;AACF;AAEO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AACF;AACO,MAAM,YAAY;AAAA,EACvB;AAAA,EACA;AACF;AACO,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AACF;AACO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AACF;AACO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.client.ts"],"sourcesContent":["// Client-graph entry. `import ... from \"@guard-rail/core\"` resolves here from a\n// Client Component (see \"exports\" in package.json) and to ./index from a\n// Server Component, so an app uses one import path everywhere.\n//\n// Nothing server-only may be imported here — this is bundled for the browser.\n// Server Actions are fine: a \"use server\" module is replaced by references in\n// the client bundle, which is what lets an app build its own team/repeater UI\n// in a Client Component.\n\nexport { GRail } from \"./react/GRail/universal\";\nexport { useGRail } from \"./react/Provider\";\n\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n setFieldValue,\n} from \"./server/actions\";\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\n\n/**\n * Stand-ins for the server-only exports, so importing one from a Client\n * Component fails with an explanation instead of an undefined-is-not-a-function\n * deep in a render. Also what a tool that doesn't set the \"react-server\"\n * condition (a test runner, a plain Node script) gets from the package root —\n * `@guard-rail/core/server` is the explicit path to the real ones there.\n */\nfunction serverOnly(name: string, hint: string) {\n return (..._args: unknown[]): never => {\n throw new Error(\n `Guardrail: ${name} only works in a Server Component. ${hint} ` +\n `(If this isn't a Client Component, your tooling may not set the ` +\n `\"react-server\" condition — import from \"@guard-rail/core/server\" instead.)`,\n );\n };\n}\n\nexport const withGuardrail = serverOnly(\n \"withGuardrail()\",\n \"Call it in your root layout, which is a Server Component.\",\n);\nexport const configureGuardrail = serverOnly(\n \"configureGuardrail()\",\n \"Configuration is applied by withGuardrail() in your root layout.\",\n);\nexport const GRepeater = serverOnly(\n \"<GRepeater>\",\n \"Render it in a Server Component and pass it into this one as children or a prop.\",\n);\nexport const listRepeaterItems = serverOnly(\n \"listRepeaterItems()\",\n \"Read repeater items in a Server Component and pass them down.\",\n);\nexport const getRepeaterItemBySlug = serverOnly(\n \"getRepeaterItemBySlug()\",\n \"Resolve the item in a Server Component (your [slug]/page.tsx) and pass it down.\",\n);\nexport const getFieldValue = serverOnly(\n \"getFieldValue()\",\n \"Read the value in a Server Component and pass it down as a prop.\",\n);\nexport const isAdmin = serverOnly(\n \"isAdmin()\",\n \"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show.\",\n);\nexport const isEditor = serverOnly(\n \"isEditor()\",\n \"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show.\",\n);\nexport const canEdit = serverOnly(\n \"canEdit()\",\n \"Call it in a Server Component; in a Client Component, read `canEdit` from useGRail().\",\n);\n\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n} from \"./core/adapter-types\";\n"],"mappings":"AASA,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAEzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,WAAW,MAAc,MAAc;AAC9C,SAAO,IAAI,UAA4B;AACrC,UAAM,IAAI;AAAA,MACR,cAAc,IAAI,sCAAsC,IAAI;AAAA,IAG9D;AAAA,EACF;AACF;AAEO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AACF;AACO,MAAM,YAAY;AAAA,EACvB;AAAA,EACA;AACF;AACO,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AACF;AACO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AACF;AACO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,UAAU;AAAA,EACrB;AAAA,EACA;AACF;AACO,MAAM,WAAW;AAAA,EACtB;AAAA,EACA;AACF;AACO,MAAM,UAAU;AAAA,EACrB;AAAA,EACA;AACF;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export { withGuardrail } from './server/wrapper.js';
|
|
2
|
-
export { createAssetUpload, inviteMember, listAssets, listTeam, removeMember, setPasswordAction, skipSetPasswordAction, updateMemberRole } from './server/actions.js';
|
|
2
|
+
export { createAssetUpload, inviteMember, listAssets, listTeam, removeMember, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole } from './server/actions.js';
|
|
3
3
|
export { GRail } from './react/GRail/index.js';
|
|
4
4
|
export { GRepeater, GRepeaterProps, RepeaterItemProps } from './react/Repeater/index.js';
|
|
5
5
|
export { configureGuardrail } from './core/config.js';
|
|
6
6
|
export { getRepeaterItemBySlug, listRepeaterItems } from './server/repeaterData.js';
|
|
7
7
|
export { getFieldValue } from './server/fieldData.js';
|
|
8
|
+
export { canEdit, isAdmin, isEditor } from './server/access.js';
|
|
8
9
|
export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from './server/repeaterActions.js';
|
|
9
10
|
export { useGRail } from './react/Provider.js';
|
|
10
11
|
export { ActionState, GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps, SetPasswordState } from './core/types.js';
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
createAssetUpload,
|
|
8
8
|
listAssets,
|
|
9
9
|
setPasswordAction,
|
|
10
|
-
skipSetPasswordAction
|
|
10
|
+
skipSetPasswordAction,
|
|
11
|
+
setFieldValue
|
|
11
12
|
} from "./server/actions";
|
|
12
13
|
import { GRail } from "./react/GRail";
|
|
13
14
|
import { GRepeater } from "./react/Repeater";
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
getRepeaterItemBySlug
|
|
18
19
|
} from "./server/repeaterData";
|
|
19
20
|
import { getFieldValue } from "./server/fieldData";
|
|
21
|
+
import { isAdmin, isEditor, canEdit } from "./server/access";
|
|
20
22
|
import {
|
|
21
23
|
addRepeaterItem,
|
|
22
24
|
removeRepeaterItem,
|
|
@@ -27,17 +29,21 @@ export {
|
|
|
27
29
|
GRail,
|
|
28
30
|
GRepeater,
|
|
29
31
|
addRepeaterItem,
|
|
32
|
+
canEdit,
|
|
30
33
|
configureGuardrail,
|
|
31
34
|
createAssetUpload,
|
|
32
35
|
getFieldValue,
|
|
33
36
|
getRepeaterItemBySlug,
|
|
34
37
|
inviteMember,
|
|
38
|
+
isAdmin,
|
|
39
|
+
isEditor,
|
|
35
40
|
listAssets,
|
|
36
41
|
listRepeaterItems,
|
|
37
42
|
listTeam,
|
|
38
43
|
removeMember,
|
|
39
44
|
removeRepeaterItem,
|
|
40
45
|
reorderRepeaterItems,
|
|
46
|
+
setFieldValue,
|
|
41
47
|
setPasswordAction,
|
|
42
48
|
skipSetPasswordAction,
|
|
43
49
|
updateMemberRole,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Server-safe entry. Client components are reached through the files that\n// carry their own \"use client\" directive, so importing this from a Server\n// Component does not pull the editor into the client bundle.\nexport { withGuardrail } from \"./server/wrapper\";\n\n// Team management and the set-password step, exported so an app can build\n// its own UI instead of using the bundled drawer / sign-in bar.\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n} from \"./server/actions\";\nexport { GRail } from \"./react/GRail\";\nexport { GRepeater } from \"./react/Repeater\";\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport { configureGuardrail } from \"./core/config\";\nexport {\n listRepeaterItems,\n getRepeaterItemBySlug,\n} from \"./server/repeaterData\";\nexport { getFieldValue } from \"./server/fieldData\";\n// Mutations, exported so an app can build its own add/remove/reorder UI\n// instead of the bundled chrome — same rationale as the team actions above.\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\nexport { useGRail } from \"./react/Provider\";\n\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n} from \"./core/adapter-types\";\n"],"mappings":"AAGA,SAAS,qBAAqB;AAI9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAE1B,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAG9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Server-safe entry. Client components are reached through the files that\n// carry their own \"use client\" directive, so importing this from a Server\n// Component does not pull the editor into the client bundle.\nexport { withGuardrail } from \"./server/wrapper\";\n\n// Team management and the set-password step, exported so an app can build\n// its own UI instead of using the bundled drawer / sign-in bar.\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n setFieldValue,\n} from \"./server/actions\";\nexport { GRail } from \"./react/GRail\";\nexport { GRepeater } from \"./react/Repeater\";\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport { configureGuardrail } from \"./core/config\";\nexport {\n listRepeaterItems,\n getRepeaterItemBySlug,\n} from \"./server/repeaterData\";\nexport { getFieldValue } from \"./server/fieldData\";\n// Role / edit-mode checks for a custom admin UI and the Server Actions behind\n// it. Plain server functions, not actions — see access.ts for why.\nexport { isAdmin, isEditor, canEdit } from \"./server/access\";\n// Mutations, exported so an app can build its own add/remove/reorder UI\n// instead of the bundled chrome — same rationale as the team actions above.\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\nexport { useGRail } from \"./react/Provider\";\n\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n} from \"./core/adapter-types\";\n"],"mappings":"AAGA,SAAS,qBAAqB;AAI9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAE1B,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAG9B,SAAS,SAAS,UAAU,eAAe;AAG3C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;","names":[]}
|
package/dist/react/Admin.js
CHANGED
|
@@ -12,7 +12,7 @@ import { useGRail } from "./Provider";
|
|
|
12
12
|
import { TeamDrawer } from "./Team";
|
|
13
13
|
import { CloseIcon, LogOutIcon, PeopleIcon } from "./Icons";
|
|
14
14
|
import { S, THEME_CSS, v } from "./styles";
|
|
15
|
-
import { GR_COOKIES,
|
|
15
|
+
import { GR_COOKIES, GR_EDIT_MODE_OFF_TTL_SECONDS } from "../core/defs";
|
|
16
16
|
const initialState = { email: "", password: "" };
|
|
17
17
|
const initialPasswordState = {};
|
|
18
18
|
function GRailAdmin(props) {
|
|
@@ -46,13 +46,13 @@ function AdminBody({ toolbarExtra }) {
|
|
|
46
46
|
await skipSetPasswordAction();
|
|
47
47
|
router.refresh();
|
|
48
48
|
};
|
|
49
|
-
const
|
|
50
|
-
|
|
49
|
+
const exitEditMode = () => {
|
|
50
|
+
document.cookie = `${GR_COOKIES.EDIT_MODE}=false; path=/; max-age=${GR_EDIT_MODE_OFF_TTL_SECONDS}; SameSite=Lax`;
|
|
51
51
|
router.replace(pathname);
|
|
52
52
|
router.refresh();
|
|
53
53
|
};
|
|
54
|
-
const
|
|
55
|
-
|
|
54
|
+
const signOut = async () => {
|
|
55
|
+
await logoutAction();
|
|
56
56
|
router.replace(pathname);
|
|
57
57
|
router.refresh();
|
|
58
58
|
};
|
|
@@ -70,7 +70,7 @@ function AdminBody({ toolbarExtra }) {
|
|
|
70
70
|
{
|
|
71
71
|
type: "button",
|
|
72
72
|
style: { ...S.iconBtn, color: v("muted") },
|
|
73
|
-
onClick:
|
|
73
|
+
onClick: exitEditMode,
|
|
74
74
|
title: "Not editing? Dismiss",
|
|
75
75
|
"aria-label": "Dismiss",
|
|
76
76
|
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
@@ -88,6 +88,7 @@ function AdminBody({ toolbarExtra }) {
|
|
|
88
88
|
{
|
|
89
89
|
name: "email",
|
|
90
90
|
type: "email",
|
|
91
|
+
autoComplete: "email",
|
|
91
92
|
placeholder: "Email",
|
|
92
93
|
defaultValue: formState.email,
|
|
93
94
|
required: true,
|
|
@@ -99,6 +100,7 @@ function AdminBody({ toolbarExtra }) {
|
|
|
99
100
|
{
|
|
100
101
|
name: "password",
|
|
101
102
|
type: "password",
|
|
103
|
+
autoComplete: "current-password",
|
|
102
104
|
placeholder: "Password",
|
|
103
105
|
required: true,
|
|
104
106
|
style: S.input
|
|
@@ -110,7 +112,7 @@ function AdminBody({ toolbarExtra }) {
|
|
|
110
112
|
{
|
|
111
113
|
type: "button",
|
|
112
114
|
style: { ...S.iconBtn, color: v("muted") },
|
|
113
|
-
onClick:
|
|
115
|
+
onClick: exitEditMode,
|
|
114
116
|
title: "Not editing? Dismiss",
|
|
115
117
|
"aria-label": "Dismiss",
|
|
116
118
|
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
@@ -158,8 +160,8 @@ function AdminBody({ toolbarExtra }) {
|
|
|
158
160
|
{
|
|
159
161
|
style: { ...S.iconBtn, color: v("muted") },
|
|
160
162
|
onClick: signOut,
|
|
161
|
-
title: "
|
|
162
|
-
"aria-label": "
|
|
163
|
+
title: "Sign out",
|
|
164
|
+
"aria-label": "Sign out",
|
|
163
165
|
children: /* @__PURE__ */ jsx(LogOutIcon, {})
|
|
164
166
|
}
|
|
165
167
|
)
|
|
@@ -190,9 +192,9 @@ function AdminBody({ toolbarExtra }) {
|
|
|
190
192
|
"button",
|
|
191
193
|
{
|
|
192
194
|
style: { ...S.iconBtn, color: v("muted") },
|
|
193
|
-
onClick:
|
|
194
|
-
title: "
|
|
195
|
-
"aria-label": "
|
|
195
|
+
onClick: exitEditMode,
|
|
196
|
+
title: "Exit edit mode",
|
|
197
|
+
"aria-label": "Exit edit mode",
|
|
196
198
|
children: /* @__PURE__ */ jsx(LogOutIcon, {})
|
|
197
199
|
}
|
|
198
200
|
)
|
package/dist/react/Admin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/Admin.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useActionState, useEffect, useState } from \"react\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport {\n loginAction,\n logoutAction,\n setPasswordAction,\n skipSetPasswordAction,\n} from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { TeamDrawer } from \"./Team\";\nimport { CloseIcon, LogOutIcon, PeopleIcon } from \"./Icons\";\nimport { S, THEME_CSS, v } from \"./styles\";\nimport { GR_COOKIES, GR_EDIT_MODE_TTL_SECONDS } from \"../core/defs\";\nimport type { ActionState, SetPasswordState } from \"../core/types\";\n\nconst initialState: ActionState = { email: \"\", password: \"\" };\nconst initialPasswordState: SetPasswordState = {};\n\n/**\n * Owns the theme stylesheet so every piece of chrome — the sign-in bar, the\n * toolbar, and the field popups portaled to <body> — picks up the same\n * light/dark variables from one place.\n */\nexport function GRailAdmin(props: { toolbarExtra?: React.ReactNode }) {\n const { isEditMode } = useGRail();\n if (!isEditMode) return null;\n return (\n <>\n {/*\n precedence + href make React hoist this into <head> and dedupe it.\n Required, not cosmetic: this whole component renders as a sibling of\n <html> (withGuardrail can't reach inside the app's own layout), and\n React 19 rejects a bare <style> anywhere outside the main document.\n */}\n <style href=\"guardrail-theme\" precedence=\"default\">\n {THEME_CSS}\n </style>\n <AdminBody {...props} />\n </>\n );\n}\n\nfunction AdminBody({ toolbarExtra }: { toolbarExtra?: React.ReactNode }) {\n const { isAuth, isEditMode, needsPassword, role, auth } = useGRail();\n const [teamOpen, setTeamOpen] = useState(false);\n const [formState, formAction, isPending] = useActionState(\n loginAction,\n initialState,\n );\n const [passwordState, passwordFormAction, isSettingPassword] = useActionState(\n setPasswordAction,\n initialPasswordState,\n );\n const router = useRouter();\n const pathname = usePathname();\n\n useEffect(() => {\n if (formState.success) router.refresh();\n }, [formState.success, router]);\n\n useEffect(() => {\n if (passwordState.success) router.refresh();\n }, [passwordState.success, router]);\n\n const skipSetPassword = async () => {\n await skipSetPasswordAction();\n router.refresh();\n };\n\n const signOut = async () => {\n // logoutAction() now clears the edit-mode cookie itself — see the\n // comment there for why a client-side hard navigation right after it\n // used to race Next's own automatic post-action route refresh and lose.\n // router.replace + refresh works with that refresh instead of against\n // it: replace drops ?edit=true from the URL (a bare reload would leave\n // it there, since the cookie, not the query, is what withGuardrail\n // reads), and refresh re-renders the tree against the now-cleared\n // cookies without a full page reload.\n await logoutAction();\n router.replace(pathname);\n router.refresh();\n };\n\n // For someone who was never authenticated and doesn't want to be here at\n // all — a stray or expired-but-not-yet-cleared cookie, say. Explicit\n // \"false\" rather than deleting: a plain delete would do nothing for an\n // authenticated visitor (their session alone is enough to keep chrome\n // visible), but this button only ever renders in the not-yet-authenticated\n // branches below, where the two are equivalent — \"false\" is used anyway\n // for consistency with the one cookie-writing convention (see Syncer.tsx).\n const dismissEditMode = () => {\n document.cookie = `${GR_COOKIES.EDIT_MODE}=false; path=/; max-age=${GR_EDIT_MODE_TTL_SECONDS}; SameSite=Lax`;\n router.replace(pathname);\n router.refresh();\n };\n\n // No intent signalled — render nothing at all.\n if (!isEditMode) return null;\n\n if (!isAuth) {\n // The adapter cannot take a password here, so hand sign-in off rather than\n // showing a form that could never succeed.\n if (!auth.password) {\n return (\n <div data-guardrail style={S.banner}>\n <div style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {!auth.signInUrl && (\n <span style={{ fontSize: 12, color: v(\"muted\") }}>\n No sign-in method is configured for this site.\n </span>\n )}\n </div>\n {auth.signInUrl && (\n <a href={auth.signInUrl} style={{ ...S.primaryBtn, textDecoration: \"none\" }}>\n Sign in\n </a>\n )}\n <button\n type=\"button\"\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={dismissEditMode}\n title=\"Not editing? Dismiss\"\n aria-label=\"Dismiss\"\n >\n <CloseIcon />\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div data-guardrail style={S.banner}>\n <form action={formAction} style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {formState.error && <span style={S.error}>{formState.error}</span>}\n </div>\n <input\n name=\"email\"\n type=\"email\"\n placeholder=\"Email\"\n defaultValue={formState.email}\n required\n style={S.input}\n />\n <input\n name=\"password\"\n type=\"password\"\n placeholder=\"Password\"\n required\n style={S.input}\n />\n <button type=\"submit\" disabled={isPending} style={S.primaryBtn}>\n {isPending ? \"Authenticating…\" : \"Sign In\"}\n </button>\n <button\n type=\"button\"\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={dismissEditMode}\n title=\"Not editing? Dismiss\"\n aria-label=\"Dismiss\"\n >\n <CloseIcon />\n </button>\n </form>\n </div>\n );\n }\n\n // A redirect-carried session (an invite or password-recovery link) just\n // completed — they're genuinely signed in, but have no password yet.\n // Ahead of the role check below: getCachedViewer() already claims a\n // pending invite as a side effect of resolving them, so role is normally\n // already set by the time this renders — this is purely about the\n // password, which is orthogonal to that. Skippable rather than a hard\n // gate: magic-link sign-in already works with no password at all, so\n // someone can dismiss this and keep using invite/magic links indefinitely.\n if (needsPassword) {\n return (\n <div data-guardrail style={S.banner}>\n <form action={passwordFormAction} style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {passwordState.error ? (\n <span style={S.error}>{passwordState.error}</span>\n ) : (\n <span style={{ fontSize: 12, color: v(\"muted\") }}>\n Set a password to sign in next time\n </span>\n )}\n </div>\n <input\n name=\"password\"\n type=\"password\"\n placeholder=\"New password\"\n required\n minLength={6}\n style={S.input}\n />\n <button type=\"submit\" disabled={isSettingPassword} style={S.primaryBtn}>\n {isSettingPassword ? \"Saving…\" : \"Set Password\"}\n </button>\n <button\n type=\"button\"\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={skipSetPassword}\n title=\"Skip for now\"\n aria-label=\"Skip\"\n >\n <CloseIcon />\n </button>\n </form>\n </div>\n );\n }\n\n // Signed in, but holds no role on this site.\n if (!role) {\n return (\n <div data-guardrail style={S.toolbar}>\n <span style={S.toolbarLabel}>No access to this site</span>\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={signOut}\n title=\"Log out\"\n aria-label=\"Log out\"\n >\n <LogOutIcon />\n </button>\n </div>\n );\n }\n\n return (\n <>\n {teamOpen && <TeamDrawer onClose={() => setTeamOpen(false)} />}\n <div data-guardrail style={S.toolbar}>\n <span style={S.dot} />\n <span style={S.toolbarLabel}>Edit Mode</span>\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\"), width: 24, height: 24 }}\n onClick={() => setTeamOpen(true)}\n title=\"Team\"\n aria-label=\"Team\"\n >\n <PeopleIcon />\n </button>\n\n {toolbarExtra && (\n <>\n <span style={S.divider} />\n {toolbarExtra}\n </>\n )}\n\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={signOut}\n title=\"Log out\"\n aria-label=\"Log out\"\n >\n <LogOutIcon />\n </button>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/Admin.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useActionState, useEffect, useState } from \"react\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport {\n loginAction,\n logoutAction,\n setPasswordAction,\n skipSetPasswordAction,\n} from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { TeamDrawer } from \"./Team\";\nimport { CloseIcon, LogOutIcon, PeopleIcon } from \"./Icons\";\nimport { S, THEME_CSS, v } from \"./styles\";\nimport { GR_COOKIES, GR_EDIT_MODE_OFF_TTL_SECONDS } from \"../core/defs\";\nimport type { ActionState, SetPasswordState } from \"../core/types\";\n\nconst initialState: ActionState = { email: \"\", password: \"\" };\nconst initialPasswordState: SetPasswordState = {};\n\n/**\n * Owns the theme stylesheet so every piece of chrome — the sign-in bar, the\n * toolbar, and the field popups portaled to <body> — picks up the same\n * light/dark variables from one place.\n */\nexport function GRailAdmin(props: { toolbarExtra?: React.ReactNode }) {\n const { isEditMode } = useGRail();\n if (!isEditMode) return null;\n return (\n <>\n {/*\n precedence + href make React hoist this into <head> and dedupe it.\n Required, not cosmetic: this whole component renders as a sibling of\n <html> (withGuardrail can't reach inside the app's own layout), and\n React 19 rejects a bare <style> anywhere outside the main document.\n */}\n <style href=\"guardrail-theme\" precedence=\"default\">\n {THEME_CSS}\n </style>\n <AdminBody {...props} />\n </>\n );\n}\n\nfunction AdminBody({ toolbarExtra }: { toolbarExtra?: React.ReactNode }) {\n const { isAuth, isEditMode, needsPassword, role, auth } = useGRail();\n const [teamOpen, setTeamOpen] = useState(false);\n const [formState, formAction, isPending] = useActionState(\n loginAction,\n initialState,\n );\n const [passwordState, passwordFormAction, isSettingPassword] = useActionState(\n setPasswordAction,\n initialPasswordState,\n );\n const router = useRouter();\n const pathname = usePathname();\n\n useEffect(() => {\n if (formState.success) router.refresh();\n }, [formState.success, router]);\n\n useEffect(() => {\n if (passwordState.success) router.refresh();\n }, [passwordState.success, router]);\n\n const skipSetPassword = async () => {\n await skipSetPasswordAction();\n router.refresh();\n };\n\n // Ends the editing session, not the sign-in: the overlay goes away and the\n // person's session stays, so ?edit=true brings it straight back with no\n // credentials. The sign-in form only returns once the session itself is\n // gone (expired, or ended elsewhere). Explicit \"false\" rather than deleting\n // the cookie: with a live session, deleting it would leave edit mode on —\n // see resolveEditMode. router.replace drops ?edit=true from the URL (a bare\n // refresh would leave it there), and refresh re-renders without chrome.\n //\n // Also what the sign-in bar's dismiss button does, for someone who was never\n // authenticated and doesn't want to be here at all.\n const exitEditMode = () => {\n document.cookie = `${GR_COOKIES.EDIT_MODE}=false; path=/; max-age=${GR_EDIT_MODE_OFF_TTL_SECONDS}; SameSite=Lax`;\n router.replace(pathname);\n router.refresh();\n };\n\n // A real sign-out, kept only for the \"no access\" bar below: someone on the\n // wrong account has no other way to switch to the right one. logoutAction()\n // clears the edit-mode cookie itself — a client-side navigation right after\n // it used to race Next's own post-action route refresh and lose.\n const signOut = async () => {\n await logoutAction();\n router.replace(pathname);\n router.refresh();\n };\n\n // No intent signalled — render nothing at all.\n if (!isEditMode) return null;\n\n if (!isAuth) {\n // The adapter cannot take a password here, so hand sign-in off rather than\n // showing a form that could never succeed.\n if (!auth.password) {\n return (\n <div data-guardrail style={S.banner}>\n <div style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {!auth.signInUrl && (\n <span style={{ fontSize: 12, color: v(\"muted\") }}>\n No sign-in method is configured for this site.\n </span>\n )}\n </div>\n {auth.signInUrl && (\n <a href={auth.signInUrl} style={{ ...S.primaryBtn, textDecoration: \"none\" }}>\n Sign in\n </a>\n )}\n <button\n type=\"button\"\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={exitEditMode}\n title=\"Not editing? Dismiss\"\n aria-label=\"Dismiss\"\n >\n <CloseIcon />\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div data-guardrail style={S.banner}>\n <form action={formAction} style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {formState.error && <span style={S.error}>{formState.error}</span>}\n </div>\n <input\n name=\"email\"\n type=\"email\"\n autoComplete=\"email\"\n placeholder=\"Email\"\n defaultValue={formState.email}\n required\n style={S.input}\n />\n <input\n name=\"password\"\n type=\"password\"\n autoComplete=\"current-password\"\n placeholder=\"Password\"\n required\n style={S.input}\n />\n <button type=\"submit\" disabled={isPending} style={S.primaryBtn}>\n {isPending ? \"Authenticating…\" : \"Sign In\"}\n </button>\n <button\n type=\"button\"\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={exitEditMode}\n title=\"Not editing? Dismiss\"\n aria-label=\"Dismiss\"\n >\n <CloseIcon />\n </button>\n </form>\n </div>\n );\n }\n\n // A redirect-carried session (an invite or password-recovery link) just\n // completed — they're genuinely signed in, but have no password yet.\n // Ahead of the role check below: getCachedViewer() already claims a\n // pending invite as a side effect of resolving them, so role is normally\n // already set by the time this renders — this is purely about the\n // password, which is orthogonal to that. Skippable rather than a hard\n // gate: magic-link sign-in already works with no password at all, so\n // someone can dismiss this and keep using invite/magic links indefinitely.\n if (needsPassword) {\n return (\n <div data-guardrail style={S.banner}>\n <form action={passwordFormAction} style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {passwordState.error ? (\n <span style={S.error}>{passwordState.error}</span>\n ) : (\n <span style={{ fontSize: 12, color: v(\"muted\") }}>\n Set a password to sign in next time\n </span>\n )}\n </div>\n <input\n name=\"password\"\n type=\"password\"\n placeholder=\"New password\"\n required\n minLength={6}\n style={S.input}\n />\n <button type=\"submit\" disabled={isSettingPassword} style={S.primaryBtn}>\n {isSettingPassword ? \"Saving…\" : \"Set Password\"}\n </button>\n <button\n type=\"button\"\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={skipSetPassword}\n title=\"Skip for now\"\n aria-label=\"Skip\"\n >\n <CloseIcon />\n </button>\n </form>\n </div>\n );\n }\n\n // Signed in, but holds no role on this site.\n if (!role) {\n return (\n <div data-guardrail style={S.toolbar}>\n <span style={S.toolbarLabel}>No access to this site</span>\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={signOut}\n title=\"Sign out\"\n aria-label=\"Sign out\"\n >\n <LogOutIcon />\n </button>\n </div>\n );\n }\n\n return (\n <>\n {teamOpen && <TeamDrawer onClose={() => setTeamOpen(false)} />}\n <div data-guardrail style={S.toolbar}>\n <span style={S.dot} />\n <span style={S.toolbarLabel}>Edit Mode</span>\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\"), width: 24, height: 24 }}\n onClick={() => setTeamOpen(true)}\n title=\"Team\"\n aria-label=\"Team\"\n >\n <PeopleIcon />\n </button>\n\n {toolbarExtra && (\n <>\n <span style={S.divider} />\n {toolbarExtra}\n </>\n )}\n\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={exitEditMode}\n title=\"Exit edit mode\"\n aria-label=\"Exit edit mode\"\n >\n <LogOutIcon />\n </button>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import React, { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
5
6
|
import { saveOverride } from "../../server/actions";
|
|
6
7
|
import { GRailRender } from "./render";
|
|
7
8
|
import { useGRail } from "../Provider";
|
|
8
9
|
import { useAnchorRect } from "../useAnchorRect";
|
|
9
|
-
import { CheckIcon, CloseIcon, PencilIcon, SpinnerIcon } from "../Icons";
|
|
10
|
+
import { CheckIcon, CloseIcon, OpenIcon, PencilIcon, SpinnerIcon } from "../Icons";
|
|
11
|
+
import { openHref } from "../openHref";
|
|
10
12
|
import { S } from "../styles";
|
|
11
13
|
import { MediaPicker } from "../MediaPicker";
|
|
12
14
|
function GRailClient({
|
|
@@ -209,12 +211,15 @@ function Chrome({
|
|
|
209
211
|
onCancel
|
|
210
212
|
}) {
|
|
211
213
|
const { storage } = useGRail();
|
|
214
|
+
const router = useRouter();
|
|
212
215
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
213
216
|
const GAP = 8;
|
|
214
217
|
const below = rect.bottom + GAP;
|
|
215
218
|
const flip = typeof window !== "undefined" && below > window.innerHeight - 120;
|
|
219
|
+
const PANEL_MIN_WIDTH = 260;
|
|
220
|
+
const maxLeft = typeof window !== "undefined" ? window.innerWidth - (isInline ? 0 : PANEL_MIN_WIDTH) - 8 : Infinity;
|
|
216
221
|
const position = {
|
|
217
|
-
left: Math.max(8, rect.left),
|
|
222
|
+
left: Math.max(8, Math.min(rect.left, maxLeft)),
|
|
218
223
|
...flip ? { top: Math.max(8, rect.top - GAP - 44) } : { top: below }
|
|
219
224
|
};
|
|
220
225
|
const actions = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -317,7 +322,42 @@ function Chrome({
|
|
|
317
322
|
)
|
|
318
323
|
] }),
|
|
319
324
|
error && /* @__PURE__ */ jsx("div", { style: { ...S.error, marginBottom: 8 }, children: error }),
|
|
320
|
-
/* @__PURE__ */
|
|
325
|
+
/* @__PURE__ */ jsxs(
|
|
326
|
+
"div",
|
|
327
|
+
{
|
|
328
|
+
style: {
|
|
329
|
+
display: "flex",
|
|
330
|
+
gap: 4,
|
|
331
|
+
alignItems: "center",
|
|
332
|
+
justifyContent: row.value.type === "link" ? "space-between" : "flex-end"
|
|
333
|
+
},
|
|
334
|
+
children: [
|
|
335
|
+
row.value.type === "link" && /* @__PURE__ */ jsxs(
|
|
336
|
+
"button",
|
|
337
|
+
{
|
|
338
|
+
type: "button",
|
|
339
|
+
style: {
|
|
340
|
+
...S.secondaryBtn,
|
|
341
|
+
width: "auto",
|
|
342
|
+
marginBottom: 0,
|
|
343
|
+
display: "inline-flex",
|
|
344
|
+
alignItems: "center",
|
|
345
|
+
gap: 6
|
|
346
|
+
},
|
|
347
|
+
disabled: !row.value.href?.trim(),
|
|
348
|
+
onClick: () => openHref(row.value.href ?? "", (path) => router.push(path)),
|
|
349
|
+
title: "Open this link",
|
|
350
|
+
"aria-label": "Open link",
|
|
351
|
+
children: [
|
|
352
|
+
/* @__PURE__ */ jsx(OpenIcon, {}),
|
|
353
|
+
" Open"
|
|
354
|
+
]
|
|
355
|
+
}
|
|
356
|
+
),
|
|
357
|
+
/* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 4 }, children: actions })
|
|
358
|
+
]
|
|
359
|
+
}
|
|
360
|
+
)
|
|
321
361
|
]
|
|
322
362
|
}
|
|
323
363
|
);
|