@guard-rail/core 0.2.0 → 0.3.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 +86 -6
- package/dist/adapters/supabase/db.js +11 -0
- package/dist/adapters/supabase/db.js.map +1 -1
- package/dist/core/adapter-types.d.ts +35 -1
- package/dist/core/config.d.ts +7 -1
- package/dist/core/config.js +11 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/scaffold.d.ts +11 -0
- package/dist/core/scaffold.js +19 -0
- package/dist/core/scaffold.js.map +1 -0
- package/dist/core/types.d.ts +14 -1
- package/dist/index.client.d.ts +18 -0
- package/dist/index.client.js +66 -0
- package/dist/index.client.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react/Admin.d.ts +8 -3
- package/dist/react/Admin.js +14 -6
- package/dist/react/Admin.js.map +1 -1
- package/dist/react/GRail/client.js +86 -27
- package/dist/react/GRail/client.js.map +1 -1
- package/dist/react/GRail/editor.d.ts +24 -0
- package/dist/react/GRail/editor.js +9 -0
- package/dist/react/GRail/editor.js.map +1 -0
- package/dist/react/GRail/index.js +2 -2
- package/dist/react/GRail/index.js.map +1 -1
- package/dist/react/GRail/render.d.ts +3 -3
- package/dist/react/GRail/universal.d.ts +22 -0
- package/dist/react/GRail/universal.js +29 -0
- package/dist/react/GRail/universal.js.map +1 -0
- package/dist/react/Icons.d.ts +2 -1
- package/dist/react/Icons.js +5 -0
- package/dist/react/Icons.js.map +1 -1
- package/dist/react/MediaPicker.js +3 -3
- package/dist/react/MediaPicker.js.map +1 -1
- package/dist/react/Provider.d.ts +9 -5
- package/dist/react/Provider.js +10 -4
- package/dist/react/Provider.js.map +1 -1
- package/dist/react/Repeater/client.d.ts +3 -3
- package/dist/react/Repeater/client.js +2 -2
- package/dist/react/Repeater/client.js.map +1 -1
- package/dist/react/Repeater/index.d.ts +3 -3
- package/dist/react/Team.js +7 -7
- package/dist/react/Team.js.map +1 -1
- package/dist/react/styles.d.ts +104 -30
- package/dist/react/styles.js +154 -65
- package/dist/react/styles.js.map +1 -1
- package/dist/react/useAnchorRect.js +1 -0
- package/dist/react/useAnchorRect.js.map +1 -1
- package/dist/server/data.js +1 -15
- package/dist/server/data.js.map +1 -1
- package/dist/server/loader.d.ts +9 -2
- package/dist/server/loader.js +74 -11
- package/dist/server/loader.js.map +1 -1
- package/dist/server/wrapper.d.ts +2 -2
- package/dist/server/wrapper.js +6 -2
- package/dist/server/wrapper.js.map +1 -1
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -107,9 +107,65 @@ import { GRail } from "@guard-rail/core";
|
|
|
107
107
|
`id` is the content key. `className` and every other prop pass straight through
|
|
108
108
|
to the element — Guardrail never styles your content.
|
|
109
109
|
|
|
110
|
+
Links take `as="a"` (so `href` type-checks) and edit both the text and the URL:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
<GRail id="nav.cta" as="a" type="link" href="/contact">Get in touch</GRail>
|
|
114
|
+
```
|
|
115
|
+
|
|
110
116
|
To edit, visit any page with `?edit=true`, sign in with a Supabase user, and
|
|
111
117
|
click a field.
|
|
112
118
|
|
|
119
|
+
### In Client Components
|
|
120
|
+
|
|
121
|
+
`<GRail>` works the same inside a `"use client"` component — a mobile-nav header,
|
|
122
|
+
a carousel, an accordion — with the same import and the same props:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
"use client";
|
|
126
|
+
import { GRail } from "@guard-rail/core";
|
|
127
|
+
|
|
128
|
+
export function Header() {
|
|
129
|
+
const [open, setOpen] = useState(false);
|
|
130
|
+
return (
|
|
131
|
+
<header>
|
|
132
|
+
<GRail id="header.tagline" as="p">Built by hand in Oklahoma</GRail>
|
|
133
|
+
<button onClick={() => setOpen(!open)}>Menu</button>
|
|
134
|
+
</header>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The package resolves `@guard-rail/core` to an async Server Component in server
|
|
140
|
+
code and to a client-safe version in client code (via the `"react-server"` export
|
|
141
|
+
condition), so moving a component between the two needs no changes. Edited
|
|
142
|
+
content is still in the server-rendered HTML — a Client Component is rendered on
|
|
143
|
+
the server for its first paint too — so SEO is unaffected.
|
|
144
|
+
|
|
145
|
+
A Client Component can't await a query, so instead the layout **preloads** the
|
|
146
|
+
site's content once per request and hands it down. That preload is the same
|
|
147
|
+
single query the server-rendered fields read from, so there's no extra cost: one
|
|
148
|
+
query per request, whether a page uses server fields, client fields, or both.
|
|
149
|
+
|
|
150
|
+
By default it preloads up to 500 rows with no configuration. If a site outgrows
|
|
151
|
+
that, a dev-mode warning says so — scope it, or turn it off:
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
withGuardrail(RootLayout, {
|
|
155
|
+
...supabase,
|
|
156
|
+
prefetch: { prefix: ["home.", "header."] }, // only preload these; optional
|
|
157
|
+
// prefetch: { limit: 2000 }, // or raise the ceiling
|
|
158
|
+
// prefetch: false, // off: client fields show their JSX
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
What still needs a Server Component: `<GRepeater>`, `getFieldValue()`, and
|
|
163
|
+
`listRepeaterItems()` throw a clear error if used from client code. Render them in
|
|
164
|
+
a Server Component and pass the result into the client one as `children` or a
|
|
165
|
+
prop. If your tooling doesn't set the `"react-server"` condition (a test runner,
|
|
166
|
+
a plain Node script), import from `@guard-rail/core/server` or
|
|
167
|
+
`@guard-rail/core/client` explicitly.
|
|
168
|
+
|
|
113
169
|
## Team and roles
|
|
114
170
|
|
|
115
171
|
Two roles, deliberately not a permission matrix:
|
|
@@ -164,11 +220,12 @@ as letting anyone make themselves an admin. Seed the first admin with the
|
|
|
164
220
|
bundled command instead of hand-writing SQL:
|
|
165
221
|
|
|
166
222
|
```bash
|
|
167
|
-
SERVICE_KEY=... npx guardrail-seed-user
|
|
168
|
-
--email=you@example.com --password=... --site-id=my-site --role=admin
|
|
223
|
+
SERVICE_KEY=... npx guardrail-seed-user --email=you@example.com --password=...
|
|
169
224
|
```
|
|
170
225
|
|
|
171
|
-
|
|
226
|
+
`--site-id` defaults to `GUARDRAIL_SITE_ID` from your `.env.local` (and
|
|
227
|
+
`SUPABASE_URL` is read from there too); `--role` defaults to `admin`. Pass
|
|
228
|
+
either explicitly to override. Creates the account if it doesn't exist (or reuses it if it does — the same
|
|
172
229
|
person can hold different roles on different sites) via Supabase's own Admin
|
|
173
230
|
API, then writes the membership row. Needs the **service_role** key, not the
|
|
174
231
|
anon key, since it bypasses RLS by design — Settings > API in the dashboard.
|
|
@@ -302,6 +359,21 @@ scratch. A visual page builder (Puck, say) is a better fit for that and can
|
|
|
302
359
|
sit alongside `<GRepeater>` later — it would decide composition, while
|
|
303
360
|
`<GRail>` still owns each block's leaf content.
|
|
304
361
|
|
|
362
|
+
## Appearance
|
|
363
|
+
|
|
364
|
+
Guardrail's own chrome (sign-in bar, toolbar, edit popups, image picker, team
|
|
365
|
+
drawer) follows the visitor's OS light/dark preference via `prefers-color-scheme`
|
|
366
|
+
— a dark-blue surface with light text in dark mode, white with dark text in light
|
|
367
|
+
mode. Popups render in a portal on `<body>`, so a field nested inside a z-indexed
|
|
368
|
+
or transformed container can't trap its own popup underneath other page content.
|
|
369
|
+
Your own markup is never restyled — including image fields. Instead of
|
|
370
|
+
outlining the `<img>` itself, an image field gets a dashed outline and a small
|
|
371
|
+
**Edit** handle drawn as separate elements over its measured position. That's what
|
|
372
|
+
makes it work for a `next/image` `fill` background with content layered on top:
|
|
373
|
+
the outline ignores the pointer so your overlaid content stays fully clickable,
|
|
374
|
+
and the handle is the only thing that opens the editor (it also won't trigger a
|
|
375
|
+
surrounding `<Link>`).
|
|
376
|
+
|
|
305
377
|
## Extending the toolbar
|
|
306
378
|
|
|
307
379
|
The bundled toolbar (edit indicator, Team, Log out) has one extension point:
|
|
@@ -365,15 +437,23 @@ const clerkAuth: GRAuthAdapter = {
|
|
|
365
437
|
`getSession()` is the sole authorization check for writes. It must validate the
|
|
366
438
|
session against the provider — never trust the presence of a cookie.
|
|
367
439
|
|
|
440
|
+
`listOverrides(siteId, { prefix, limit })` is optional. It's what lets `<GRail>`
|
|
441
|
+
work inside Client Components (see above): one round trip returning the site's rows
|
|
442
|
+
and whether the list was cut off at `limit`. An adapter without it still works
|
|
443
|
+
fully for Server Components — client-rendered fields just show their JSX fallback.
|
|
444
|
+
|
|
368
445
|
`getOverrides(pathKeys, siteId)` is batched by design — it receives every key a
|
|
369
446
|
render pass needs, already deduplicated. Adapters map their own storage naming
|
|
370
447
|
at this boundary; core speaks `pathKey` / `siteId` only.
|
|
371
448
|
|
|
372
449
|
## Performance
|
|
373
450
|
|
|
374
|
-
Fields are resolved through a request-scoped
|
|
375
|
-
|
|
376
|
-
|
|
451
|
+
Fields are resolved through a request-scoped loader. By default it preloads the
|
|
452
|
+
site's content in one query and every `<GRail>` — server or client — reads from
|
|
453
|
+
that, so a request issues a single content query however many fields it renders.
|
|
454
|
+
Anything outside the preload (a `prefetch.prefix` scope, or a site past the row
|
|
455
|
+
ceiling) falls back to a batched lookup by key. Set `GUARDRAIL_DEBUG=1` to log
|
|
456
|
+
what each query resolved.
|
|
377
457
|
|
|
378
458
|
## Known limitations
|
|
379
459
|
|
|
@@ -20,6 +20,17 @@ function supabaseDB(creds) {
|
|
|
20
20
|
}
|
|
21
21
|
return (data ?? []).map(toOverrideRow);
|
|
22
22
|
},
|
|
23
|
+
async listOverrides(siteId, { prefix, limit }) {
|
|
24
|
+
let query = anonClient(creds).schema(GR_SCHEMA).from(GR_TABLE).select("id, path_key, value").eq("site_id", siteId).order("path_key").limit(limit + 1);
|
|
25
|
+
if (prefix?.length) {
|
|
26
|
+
const quote = (p) => `"${p.replace(/["\\]/g, "\\$&")}"`;
|
|
27
|
+
query = query.or(prefix.map((p) => `path_key.like.${quote(`${p}*`)}`).join(","));
|
|
28
|
+
}
|
|
29
|
+
const { data, error } = await query;
|
|
30
|
+
if (error) throw new Error(error.message);
|
|
31
|
+
const rows = (data ?? []).map(toOverrideRow);
|
|
32
|
+
return { rows: rows.slice(0, limit), truncated: rows.length > limit };
|
|
33
|
+
},
|
|
23
34
|
async setOverride(row) {
|
|
24
35
|
const client = await sessionClient(creds);
|
|
25
36
|
const { error } = await client.schema(GR_SCHEMA).from(GR_TABLE).upsert(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/adapters/supabase/db.ts"],"sourcesContent":["import { anonClient, assertCredentials, sessionClient } from \"./client\";\nimport type { SupabaseCredentials } from \"./client\";\nimport { GR_SCHEMA, GR_TABLE } from \"../../core/defs\";\nimport type { GRDBAdapter, GRStorageRow } from \"../../core/adapter-types\";\nimport type { OverrideRow, OverrideValue } from \"../../core/types\";\n\n/** Shape as stored in Postgres. Column naming stops at this boundary. */\ninterface ContentRow {\n id: string;\n path_key: string;\n value: OverrideValue | string;\n}\n\nfunction toOverrideRow(row: ContentRow): OverrideRow {\n return {\n id: row.id,\n pathKey: row.path_key,\n // Some drivers hand back jsonb columns as strings.\n value:\n typeof row.value === \"string\"\n ? (JSON.parse(row.value) as OverrideValue)\n : row.value,\n };\n}\n\nexport function supabaseDB(creds: SupabaseCredentials): GRDBAdapter {\n assertCredentials(creds);\n\n return {\n async getOverrides(pathKeys, siteId) {\n if (pathKeys.length === 0) return [];\n\n // Anon client keeps the read path cookie-free so routes stay static.\n const { data, error } = await anonClient(creds)\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .select(\"id, path_key, value\")\n .eq(\"site_id\", siteId)\n .in(\"path_key\", pathKeys);\n\n if (error) {\n console.error(\"Guardrail (Supabase): read failed\", error.message);\n return [];\n }\n return ((data ?? []) as ContentRow[]).map(toOverrideRow);\n },\n\n async setOverride(row: GRStorageRow) {\n const client = await sessionClient(creds);\n const { error } = await client\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .upsert(\n {\n path_key: row.pathKey,\n site_id: row.siteId,\n type: row.type,\n value: row.value,\n updated_at: new Date().toISOString(),\n },\n { onConflict: \"path_key,site_id\" },\n );\n return error\n ? { success: false, error: error.message }\n : { success: true };\n },\n\n async deleteOverrides(pathKeyPrefix: string, siteId: string) {\n const client = await sessionClient(creds);\n const { error } = await client\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .delete()\n .eq(\"site_id\", siteId)\n .like(\"path_key\", `${pathKeyPrefix}%`);\n if (error) {\n console.error(\"Guardrail (Supabase): deleteOverrides failed\", error.message);\n }\n },\n };\n}\n"],"mappings":"AAAA,SAAS,YAAY,mBAAmB,qBAAqB;AAE7D,SAAS,WAAW,gBAAgB;AAWpC,SAAS,cAAc,KAA8B;AACnD,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,SAAS,IAAI;AAAA;AAAA,IAEb,OACE,OAAO,IAAI,UAAU,WAChB,KAAK,MAAM,IAAI,KAAK,IACrB,IAAI;AAAA,EACZ;AACF;AAEO,SAAS,WAAW,OAAyC;AAClE,oBAAkB,KAAK;AAEvB,SAAO;AAAA,IACL,MAAM,aAAa,UAAU,QAAQ;AACnC,UAAI,SAAS,WAAW,EAAG,QAAO,CAAC;AAGnC,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,WAAW,KAAK,EAC3C,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb,OAAO,qBAAqB,EAC5B,GAAG,WAAW,MAAM,EACpB,GAAG,YAAY,QAAQ;AAE1B,UAAI,OAAO;AACT,gBAAQ,MAAM,qCAAqC,MAAM,OAAO;AAChE,eAAO,CAAC;AAAA,MACV;AACA,cAAS,QAAQ,CAAC,GAAoB,IAAI,aAAa;AAAA,IACzD;AAAA,IAEA,MAAM,YAAY,KAAmB;AACnC,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,IAAI,MAAM,OACrB,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb;AAAA,QACC;AAAA,UACE,UAAU,IAAI;AAAA,UACd,SAAS,IAAI;AAAA,UACb,MAAM,IAAI;AAAA,UACV,OAAO,IAAI;AAAA,UACX,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,QACrC;AAAA,QACA,EAAE,YAAY,mBAAmB;AAAA,MACnC;AACF,aAAO,QACH,EAAE,SAAS,OAAO,OAAO,MAAM,QAAQ,IACvC,EAAE,SAAS,KAAK;AAAA,IACtB;AAAA,IAEA,MAAM,gBAAgB,eAAuB,QAAgB;AAC3D,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,IAAI,MAAM,OACrB,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb,OAAO,EACP,GAAG,WAAW,MAAM,EACpB,KAAK,YAAY,GAAG,aAAa,GAAG;AACvC,UAAI,OAAO;AACT,gBAAQ,MAAM,gDAAgD,MAAM,OAAO;AAAA,MAC7E;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/adapters/supabase/db.ts"],"sourcesContent":["import { anonClient, assertCredentials, sessionClient } from \"./client\";\nimport type { SupabaseCredentials } from \"./client\";\nimport { GR_SCHEMA, GR_TABLE } from \"../../core/defs\";\nimport type { GRDBAdapter, GRStorageRow } from \"../../core/adapter-types\";\nimport type { OverrideRow, OverrideValue } from \"../../core/types\";\n\n/** Shape as stored in Postgres. Column naming stops at this boundary. */\ninterface ContentRow {\n id: string;\n path_key: string;\n value: OverrideValue | string;\n}\n\nfunction toOverrideRow(row: ContentRow): OverrideRow {\n return {\n id: row.id,\n pathKey: row.path_key,\n // Some drivers hand back jsonb columns as strings.\n value:\n typeof row.value === \"string\"\n ? (JSON.parse(row.value) as OverrideValue)\n : row.value,\n };\n}\n\nexport function supabaseDB(creds: SupabaseCredentials): GRDBAdapter {\n assertCredentials(creds);\n\n return {\n async getOverrides(pathKeys, siteId) {\n if (pathKeys.length === 0) return [];\n\n // Anon client keeps the read path cookie-free so routes stay static.\n const { data, error } = await anonClient(creds)\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .select(\"id, path_key, value\")\n .eq(\"site_id\", siteId)\n .in(\"path_key\", pathKeys);\n\n if (error) {\n console.error(\"Guardrail (Supabase): read failed\", error.message);\n return [];\n }\n return ((data ?? []) as ContentRow[]).map(toOverrideRow);\n },\n\n async listOverrides(siteId, { prefix, limit }) {\n let query = anonClient(creds)\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .select(\"id, path_key, value\")\n .eq(\"site_id\", siteId)\n // Ordered so that truncation, when it happens, is deterministic.\n .order(\"path_key\")\n // One past the ceiling, purely to learn whether anything was cut off.\n .limit(limit + 1);\n\n if (prefix?.length) {\n // Quoted so a prefix containing PostgREST's own separators (comma,\n // parens) can't be read as filter syntax.\n const quote = (p: string) => `\"${p.replace(/[\"\\\\]/g, \"\\\\$&\")}\"`;\n query = query.or(prefix.map((p) => `path_key.like.${quote(`${p}*`)}`).join(\",\"));\n }\n\n const { data, error } = await query;\n // Thrown, not swallowed to []: an empty list here would read as \"this\n // site has no content\" and short-circuit every field's own lookup.\n if (error) throw new Error(error.message);\n\n const rows = ((data ?? []) as ContentRow[]).map(toOverrideRow);\n return { rows: rows.slice(0, limit), truncated: rows.length > limit };\n },\n\n async setOverride(row: GRStorageRow) {\n const client = await sessionClient(creds);\n const { error } = await client\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .upsert(\n {\n path_key: row.pathKey,\n site_id: row.siteId,\n type: row.type,\n value: row.value,\n updated_at: new Date().toISOString(),\n },\n { onConflict: \"path_key,site_id\" },\n );\n return error\n ? { success: false, error: error.message }\n : { success: true };\n },\n\n async deleteOverrides(pathKeyPrefix: string, siteId: string) {\n const client = await sessionClient(creds);\n const { error } = await client\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .delete()\n .eq(\"site_id\", siteId)\n .like(\"path_key\", `${pathKeyPrefix}%`);\n if (error) {\n console.error(\"Guardrail (Supabase): deleteOverrides failed\", error.message);\n }\n },\n };\n}\n"],"mappings":"AAAA,SAAS,YAAY,mBAAmB,qBAAqB;AAE7D,SAAS,WAAW,gBAAgB;AAWpC,SAAS,cAAc,KAA8B;AACnD,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,SAAS,IAAI;AAAA;AAAA,IAEb,OACE,OAAO,IAAI,UAAU,WAChB,KAAK,MAAM,IAAI,KAAK,IACrB,IAAI;AAAA,EACZ;AACF;AAEO,SAAS,WAAW,OAAyC;AAClE,oBAAkB,KAAK;AAEvB,SAAO;AAAA,IACL,MAAM,aAAa,UAAU,QAAQ;AACnC,UAAI,SAAS,WAAW,EAAG,QAAO,CAAC;AAGnC,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,WAAW,KAAK,EAC3C,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb,OAAO,qBAAqB,EAC5B,GAAG,WAAW,MAAM,EACpB,GAAG,YAAY,QAAQ;AAE1B,UAAI,OAAO;AACT,gBAAQ,MAAM,qCAAqC,MAAM,OAAO;AAChE,eAAO,CAAC;AAAA,MACV;AACA,cAAS,QAAQ,CAAC,GAAoB,IAAI,aAAa;AAAA,IACzD;AAAA,IAEA,MAAM,cAAc,QAAQ,EAAE,QAAQ,MAAM,GAAG;AAC7C,UAAI,QAAQ,WAAW,KAAK,EACzB,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb,OAAO,qBAAqB,EAC5B,GAAG,WAAW,MAAM,EAEpB,MAAM,UAAU,EAEhB,MAAM,QAAQ,CAAC;AAElB,UAAI,QAAQ,QAAQ;AAGlB,cAAM,QAAQ,CAAC,MAAc,IAAI,EAAE,QAAQ,UAAU,MAAM,CAAC;AAC5D,gBAAQ,MAAM,GAAG,OAAO,IAAI,CAAC,MAAM,iBAAiB,MAAM,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,GAAG,CAAC;AAAA,MACjF;AAEA,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM;AAG9B,UAAI,MAAO,OAAM,IAAI,MAAM,MAAM,OAAO;AAExC,YAAM,QAAS,QAAQ,CAAC,GAAoB,IAAI,aAAa;AAC7D,aAAO,EAAE,MAAM,KAAK,MAAM,GAAG,KAAK,GAAG,WAAW,KAAK,SAAS,MAAM;AAAA,IACtE;AAAA,IAEA,MAAM,YAAY,KAAmB;AACnC,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,IAAI,MAAM,OACrB,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb;AAAA,QACC;AAAA,UACE,UAAU,IAAI;AAAA,UACd,SAAS,IAAI;AAAA,UACb,MAAM,IAAI;AAAA,UACV,OAAO,IAAI;AAAA,UACX,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,QACrC;AAAA,QACA,EAAE,YAAY,mBAAmB;AAAA,MACnC;AACF,aAAO,QACH,EAAE,SAAS,OAAO,OAAO,MAAM,QAAQ,IACvC,EAAE,SAAS,KAAK;AAAA,IACtB;AAAA,IAEA,MAAM,gBAAgB,eAAuB,QAAgB;AAC3D,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,IAAI,MAAM,OACrB,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb,OAAO,EACP,GAAG,WAAW,MAAM,EACpB,KAAK,YAAY,GAAG,aAAa,GAAG;AACvC,UAAI,OAAO;AACT,gBAAQ,MAAM,gDAAgD,MAAM,OAAO;AAAA,MAC7E;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
@@ -30,6 +30,30 @@ interface GRDBAdapter {
|
|
|
30
30
|
* row is still removed, just not its old field values.
|
|
31
31
|
*/
|
|
32
32
|
deleteOverrides?: (pathKeyPrefix: string, siteId: string) => Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* List a site's rows without naming keys — what makes <GRail> usable inside
|
|
35
|
+
* Client Components, which cannot await a per-field query. One round trip.
|
|
36
|
+
*
|
|
37
|
+
* Optional: an adapter without it still works fully for Server Components;
|
|
38
|
+
* client-rendered fields just show their JSX fallback. `limit` is a ceiling,
|
|
39
|
+
* not a page size — return `truncated: true` if rows beyond it existed, so
|
|
40
|
+
* the caller knows the list is partial. `prefix`, when given, restricts to
|
|
41
|
+
* keys starting with any of those strings.
|
|
42
|
+
*/
|
|
43
|
+
listOverrides?: (siteId: string, opts: {
|
|
44
|
+
prefix?: string[];
|
|
45
|
+
limit: number;
|
|
46
|
+
}) => Promise<{
|
|
47
|
+
rows: OverrideRow[];
|
|
48
|
+
truncated: boolean;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
51
|
+
/** How much content to preload for <GRail>s inside Client Components. */
|
|
52
|
+
interface GRPrefetch {
|
|
53
|
+
/** Only preload keys starting with one of these (e.g. `["home.", "header."]`). */
|
|
54
|
+
prefix?: string[];
|
|
55
|
+
/** Ceiling on preloaded rows. Default 500. */
|
|
56
|
+
limit?: number;
|
|
33
57
|
}
|
|
34
58
|
/** Identity of a signed-in editor, as verified by the auth adapter. */
|
|
35
59
|
interface GRSession {
|
|
@@ -207,6 +231,16 @@ interface GRConfig {
|
|
|
207
231
|
* an already-resolved element, not a function.
|
|
208
232
|
*/
|
|
209
233
|
toolbarExtra?: ReactNode;
|
|
234
|
+
/**
|
|
235
|
+
* Content preloaded for <GRail>s inside Client Components. On by default
|
|
236
|
+
* with a row ceiling and no scoping: zero config, and one query per request
|
|
237
|
+
* total, since server-rendered fields read from the same preload instead of
|
|
238
|
+
* issuing their own. Scope it with `prefix` if a site outgrows the ceiling
|
|
239
|
+
* (a dev-mode warning says when), or pass `false` to turn it off — client
|
|
240
|
+
* fields then render their JSX fallback and server fields query per batch as
|
|
241
|
+
* before.
|
|
242
|
+
*/
|
|
243
|
+
prefetch?: false | GRPrefetch;
|
|
210
244
|
}
|
|
211
245
|
|
|
212
|
-
export type { AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget };
|
|
246
|
+
export type { AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget };
|
package/dist/core/config.d.ts
CHANGED
|
@@ -21,7 +21,13 @@ declare function getMembers(): GRMembershipAdapter;
|
|
|
21
21
|
declare function getStorage(): GRStorageAdapter | undefined;
|
|
22
22
|
/** Throws with a clear message if no page on this site uses <GRepeater>. */
|
|
23
23
|
declare function getRepeaters(): GRRepeaterAdapter;
|
|
24
|
+
declare const DEFAULT_PREFETCH_LIMIT = 500;
|
|
25
|
+
/** Normalized preload settings, or null when preloading is off. */
|
|
26
|
+
declare function getPrefetch(): {
|
|
27
|
+
prefix?: string[];
|
|
28
|
+
limit: number;
|
|
29
|
+
} | null;
|
|
24
30
|
declare function getInviteHandler(): GRConfig["onInvite"];
|
|
25
31
|
declare function resolveSiteId(): Promise<string>;
|
|
26
32
|
|
|
27
|
-
export { configureGuardrail, getAuth, getDB, getInviteHandler, getMembers, getRepeaters, getStorage, resolveSiteId };
|
|
33
|
+
export { DEFAULT_PREFETCH_LIMIT, configureGuardrail, getAuth, getDB, getInviteHandler, getMembers, getPrefetch, getRepeaters, getStorage, resolveSiteId };
|
package/dist/core/config.js
CHANGED
|
@@ -26,6 +26,15 @@ function getStorage() {
|
|
|
26
26
|
function getRepeaters() {
|
|
27
27
|
return required("repeaters");
|
|
28
28
|
}
|
|
29
|
+
const DEFAULT_PREFETCH_LIMIT = 500;
|
|
30
|
+
function getPrefetch() {
|
|
31
|
+
const p = _config?.prefetch;
|
|
32
|
+
if (p === false) return null;
|
|
33
|
+
return {
|
|
34
|
+
...p?.prefix?.length ? { prefix: p.prefix } : {},
|
|
35
|
+
limit: p?.limit ?? DEFAULT_PREFETCH_LIMIT
|
|
36
|
+
};
|
|
37
|
+
}
|
|
29
38
|
function getInviteHandler() {
|
|
30
39
|
return _config?.onInvite;
|
|
31
40
|
}
|
|
@@ -34,11 +43,13 @@ async function resolveSiteId() {
|
|
|
34
43
|
return typeof sid === "function" ? await sid() : sid || "default";
|
|
35
44
|
}
|
|
36
45
|
export {
|
|
46
|
+
DEFAULT_PREFETCH_LIMIT,
|
|
37
47
|
configureGuardrail,
|
|
38
48
|
getAuth,
|
|
39
49
|
getDB,
|
|
40
50
|
getInviteHandler,
|
|
41
51
|
getMembers,
|
|
52
|
+
getPrefetch,
|
|
42
53
|
getRepeaters,
|
|
43
54
|
getStorage,
|
|
44
55
|
resolveSiteId
|
package/dist/core/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/config.ts"],"sourcesContent":["import type {\n GRConfig,\n GRAuthAdapter,\n GRDBAdapter,\n GRMembershipAdapter,\n GRRepeaterAdapter,\n GRStorageAdapter,\n} from \"./adapter-types\";\n\nlet _config: GRConfig | null = null;\n\n/**\n * Registers the adapters Guardrail should use.\n *\n * Idempotent, and re-applied on every render by withGuardrail: Next.js does not\n * guarantee one shared module instance across the SSR graph, the server-action\n * graph, and the workers used for static generation, so config registered once\n * at startup is simply absent in some of them.\n *\n * Core has no knowledge of any particular backend — there is no default and no\n * fallback. An app names its adapters explicitly or Guardrail refuses to run.\n */\nexport function configureGuardrail(config: GRConfig): void {\n _config = config;\n}\n\nfunction required<K extends keyof GRConfig>(key: K): GRConfig[K] {\n const value = _config?.[key];\n if (!value) {\n throw new Error(\n `Guardrail: no ${String(key)} adapter configured. Pass one to ` +\n `withGuardrail(Layout, { db, auth, members }).`,\n );\n }\n return value;\n}\n\nexport function getDB(): GRDBAdapter {\n return required(\"db\") as GRDBAdapter;\n}\n\nexport function getAuth(): GRAuthAdapter {\n return required(\"auth\") as GRAuthAdapter;\n}\n\nexport function getMembers(): GRMembershipAdapter {\n return required(\"members\") as GRMembershipAdapter;\n}\n\n/** Optional — sites without editable images need no storage. */\nexport function getStorage(): GRStorageAdapter | undefined {\n return _config?.storage;\n}\n\n/** Throws with a clear message if no page on this site uses <GRepeater>. */\nexport function getRepeaters(): GRRepeaterAdapter {\n return required(\"repeaters\") as GRRepeaterAdapter;\n}\n\nexport function getInviteHandler(): GRConfig[\"onInvite\"] {\n return _config?.onInvite;\n}\n\nexport async function resolveSiteId(): Promise<string> {\n const sid = _config?.siteId ?? \"default\";\n return typeof sid === \"function\" ? await sid() : sid || \"default\";\n}\n"],"mappings":"AASA,IAAI,UAA2B;AAaxB,SAAS,mBAAmB,QAAwB;AACzD,YAAU;AACZ;AAEA,SAAS,SAAmC,KAAqB;AAC/D,QAAM,QAAQ,UAAU,GAAG;AAC3B,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR,iBAAiB,OAAO,GAAG,CAAC;AAAA,IAE9B;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,QAAqB;AACnC,SAAO,SAAS,IAAI;AACtB;AAEO,SAAS,UAAyB;AACvC,SAAO,SAAS,MAAM;AACxB;AAEO,SAAS,aAAkC;AAChD,SAAO,SAAS,SAAS;AAC3B;AAGO,SAAS,aAA2C;AACzD,SAAO,SAAS;AAClB;AAGO,SAAS,eAAkC;AAChD,SAAO,SAAS,WAAW;AAC7B;AAEO,SAAS,mBAAyC;AACvD,SAAO,SAAS;AAClB;AAEA,eAAsB,gBAAiC;AACrD,QAAM,MAAM,SAAS,UAAU;AAC/B,SAAO,OAAO,QAAQ,aAAa,MAAM,IAAI,IAAI,OAAO;AAC1D;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/config.ts"],"sourcesContent":["import type {\n GRConfig,\n GRAuthAdapter,\n GRDBAdapter,\n GRMembershipAdapter,\n GRRepeaterAdapter,\n GRStorageAdapter,\n} from \"./adapter-types\";\n\nlet _config: GRConfig | null = null;\n\n/**\n * Registers the adapters Guardrail should use.\n *\n * Idempotent, and re-applied on every render by withGuardrail: Next.js does not\n * guarantee one shared module instance across the SSR graph, the server-action\n * graph, and the workers used for static generation, so config registered once\n * at startup is simply absent in some of them.\n *\n * Core has no knowledge of any particular backend — there is no default and no\n * fallback. An app names its adapters explicitly or Guardrail refuses to run.\n */\nexport function configureGuardrail(config: GRConfig): void {\n _config = config;\n}\n\nfunction required<K extends keyof GRConfig>(key: K): GRConfig[K] {\n const value = _config?.[key];\n if (!value) {\n throw new Error(\n `Guardrail: no ${String(key)} adapter configured. Pass one to ` +\n `withGuardrail(Layout, { db, auth, members }).`,\n );\n }\n return value;\n}\n\nexport function getDB(): GRDBAdapter {\n return required(\"db\") as GRDBAdapter;\n}\n\nexport function getAuth(): GRAuthAdapter {\n return required(\"auth\") as GRAuthAdapter;\n}\n\nexport function getMembers(): GRMembershipAdapter {\n return required(\"members\") as GRMembershipAdapter;\n}\n\n/** Optional — sites without editable images need no storage. */\nexport function getStorage(): GRStorageAdapter | undefined {\n return _config?.storage;\n}\n\n/** Throws with a clear message if no page on this site uses <GRepeater>. */\nexport function getRepeaters(): GRRepeaterAdapter {\n return required(\"repeaters\") as GRRepeaterAdapter;\n}\n\nexport const DEFAULT_PREFETCH_LIMIT = 500;\n\n/** Normalized preload settings, or null when preloading is off. */\nexport function getPrefetch(): { prefix?: string[]; limit: number } | null {\n const p = _config?.prefetch;\n if (p === false) return null;\n return {\n ...(p?.prefix?.length ? { prefix: p.prefix } : {}),\n limit: p?.limit ?? DEFAULT_PREFETCH_LIMIT,\n };\n}\n\nexport function getInviteHandler(): GRConfig[\"onInvite\"] {\n return _config?.onInvite;\n}\n\nexport async function resolveSiteId(): Promise<string> {\n const sid = _config?.siteId ?? \"default\";\n return typeof sid === \"function\" ? await sid() : sid || \"default\";\n}\n"],"mappings":"AASA,IAAI,UAA2B;AAaxB,SAAS,mBAAmB,QAAwB;AACzD,YAAU;AACZ;AAEA,SAAS,SAAmC,KAAqB;AAC/D,QAAM,QAAQ,UAAU,GAAG;AAC3B,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR,iBAAiB,OAAO,GAAG,CAAC;AAAA,IAE9B;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,QAAqB;AACnC,SAAO,SAAS,IAAI;AACtB;AAEO,SAAS,UAAyB;AACvC,SAAO,SAAS,MAAM;AACxB;AAEO,SAAS,aAAkC;AAChD,SAAO,SAAS,SAAS;AAC3B;AAGO,SAAS,aAA2C;AACzD,SAAO,SAAS;AAClB;AAGO,SAAS,eAAkC;AAChD,SAAO,SAAS,WAAW;AAC7B;AAEO,MAAM,yBAAyB;AAG/B,SAAS,cAA2D;AACzE,QAAM,IAAI,SAAS;AACnB,MAAI,MAAM,MAAO,QAAO;AACxB,SAAO;AAAA,IACL,GAAI,GAAG,QAAQ,SAAS,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,IAChD,OAAO,GAAG,SAAS;AAAA,EACrB;AACF;AAEO,SAAS,mBAAyC;AACvD,SAAO,SAAS;AAClB;AAEA,eAAsB,gBAAiC;AACrD,QAAM,MAAM,SAAS,UAAU;AAC/B,SAAO,OAAO,QAAQ,aAAa,MAAM,IAAI,IAAI,OAAO;AAC1D;","names":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { OverrideRow } from './types.js';
|
|
2
|
+
import 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds a default row from the JSX the developer already wrote, so an
|
|
6
|
+
* un-edited field renders its hardcoded content. Deliberately does not write
|
|
7
|
+
* to the database — nothing is persisted until an editor saves.
|
|
8
|
+
*/
|
|
9
|
+
declare function generateScaffold(pathKey: string, props: Record<string, unknown>): OverrideRow;
|
|
10
|
+
|
|
11
|
+
export { generateScaffold };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function generateScaffold(pathKey, props) {
|
|
2
|
+
const { children, type, src, alt, href } = props;
|
|
3
|
+
const defaultText = typeof children === "string" || typeof children === "number" ? String(children) : "";
|
|
4
|
+
let value;
|
|
5
|
+
if (type === "image" || src) {
|
|
6
|
+
value = { type: "image", src: src ?? "", alt: alt ?? "" };
|
|
7
|
+
} else if (type === "link" || href) {
|
|
8
|
+
value = { type: "link", href: href ?? "", text: defaultText };
|
|
9
|
+
} else if (type === "button") {
|
|
10
|
+
value = { type: "button", text: defaultText };
|
|
11
|
+
} else {
|
|
12
|
+
value = { type: "text", text: defaultText };
|
|
13
|
+
}
|
|
14
|
+
return { id: `scaffold-${pathKey}`, pathKey, value };
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
generateScaffold
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=scaffold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/scaffold.ts"],"sourcesContent":["import type { OverrideRow, OverrideValue } from \"./types\";\n\n/**\n * Builds a default row from the JSX the developer already wrote, so an\n * un-edited field renders its hardcoded content. Deliberately does not write\n * to the database — nothing is persisted until an editor saves.\n */\nexport function generateScaffold(\n pathKey: string,\n props: Record<string, unknown>,\n): OverrideRow {\n const { children, type, src, alt, href } = props as {\n children?: unknown;\n type?: string;\n src?: string;\n alt?: string;\n href?: string;\n };\n\n const defaultText =\n typeof children === \"string\" || typeof children === \"number\"\n ? String(children)\n : \"\";\n\n let value: OverrideValue;\n\n if (type === \"image\" || src) {\n value = { type: \"image\", src: src ?? \"\", alt: alt ?? \"\" };\n } else if (type === \"link\" || href) {\n value = { type: \"link\", href: href ?? \"\", text: defaultText };\n } else if (type === \"button\") {\n value = { type: \"button\", text: defaultText };\n } else {\n value = { type: \"text\", text: defaultText };\n }\n\n return { id: `scaffold-${pathKey}`, pathKey, value };\n}\n"],"mappings":"AAOO,SAAS,iBACd,SACA,OACa;AACb,QAAM,EAAE,UAAU,MAAM,KAAK,KAAK,KAAK,IAAI;AAQ3C,QAAM,cACJ,OAAO,aAAa,YAAY,OAAO,aAAa,WAChD,OAAO,QAAQ,IACf;AAEN,MAAI;AAEJ,MAAI,SAAS,WAAW,KAAK;AAC3B,YAAQ,EAAE,MAAM,SAAS,KAAK,OAAO,IAAI,KAAK,OAAO,GAAG;AAAA,EAC1D,WAAW,SAAS,UAAU,MAAM;AAClC,YAAQ,EAAE,MAAM,QAAQ,MAAM,QAAQ,IAAI,MAAM,YAAY;AAAA,EAC9D,WAAW,SAAS,UAAU;AAC5B,YAAQ,EAAE,MAAM,UAAU,MAAM,YAAY;AAAA,EAC9C,OAAO;AACL,YAAQ,EAAE,MAAM,QAAQ,MAAM,YAAY;AAAA,EAC5C;AAEA,SAAO,EAAE,IAAI,YAAY,OAAO,IAAI,SAAS,MAAM;AACrD;","names":[]}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -33,6 +33,19 @@ interface OverrideRow {
|
|
|
33
33
|
pathKey: string;
|
|
34
34
|
value: OverrideValue;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Field values preloaded for <GRail>s rendered inside Client Components, which
|
|
38
|
+
* cannot await a query themselves. Lean on purpose — it is serialized into the
|
|
39
|
+
* page — so it carries values only, not rows.
|
|
40
|
+
*/
|
|
41
|
+
interface GRContent {
|
|
42
|
+
values: Record<string, OverrideValue>;
|
|
43
|
+
/**
|
|
44
|
+
* True when `values` is every row this site has. A key absent from a
|
|
45
|
+
* complete map is known to be unedited, with no further lookup needed.
|
|
46
|
+
*/
|
|
47
|
+
complete: boolean;
|
|
48
|
+
}
|
|
36
49
|
/**
|
|
37
50
|
* next/image props that have no intrinsic-element equivalent. A field rendered
|
|
38
51
|
* with type="image" goes through next/image, so these have to be accepted even
|
|
@@ -133,4 +146,4 @@ type ActionState = {
|
|
|
133
146
|
password?: string;
|
|
134
147
|
};
|
|
135
148
|
|
|
136
|
-
export type { ActionState, GRAsset, GRAuthCapabilities, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, NextImageExtras, OverrideRow, OverrideType, OverrideValue, PolymorphicProps };
|
|
149
|
+
export type { ActionState, GRAsset, GRAuthCapabilities, GRContent, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, NextImageExtras, OverrideRow, OverrideType, OverrideValue, PolymorphicProps };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { GRail } from './react/GRail/universal.js';
|
|
2
|
+
export { useGRail } from './react/Provider.js';
|
|
3
|
+
export { createAssetUpload, inviteMember, listAssets, listTeam, removeMember, updateMemberRole } from './server/actions.js';
|
|
4
|
+
export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from './server/repeaterActions.js';
|
|
5
|
+
export { GRepeaterProps, RepeaterItemProps } from './react/Repeater/index.js';
|
|
6
|
+
export { GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps } from './core/types.js';
|
|
7
|
+
export { AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
|
|
8
|
+
import 'react/jsx-runtime';
|
|
9
|
+
import 'react';
|
|
10
|
+
|
|
11
|
+
declare const withGuardrail: (..._args: unknown[]) => never;
|
|
12
|
+
declare const configureGuardrail: (..._args: unknown[]) => never;
|
|
13
|
+
declare const GRepeater: (..._args: unknown[]) => never;
|
|
14
|
+
declare const listRepeaterItems: (..._args: unknown[]) => never;
|
|
15
|
+
declare const getRepeaterItemBySlug: (..._args: unknown[]) => never;
|
|
16
|
+
declare const getFieldValue: (..._args: unknown[]) => never;
|
|
17
|
+
|
|
18
|
+
export { GRepeater, configureGuardrail, getFieldValue, getRepeaterItemBySlug, listRepeaterItems, withGuardrail };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { GRail } from "./react/GRail/universal";
|
|
2
|
+
import { useGRail } from "./react/Provider";
|
|
3
|
+
import {
|
|
4
|
+
listTeam,
|
|
5
|
+
inviteMember,
|
|
6
|
+
updateMemberRole,
|
|
7
|
+
removeMember,
|
|
8
|
+
createAssetUpload,
|
|
9
|
+
listAssets
|
|
10
|
+
} from "./server/actions";
|
|
11
|
+
import {
|
|
12
|
+
addRepeaterItem,
|
|
13
|
+
removeRepeaterItem,
|
|
14
|
+
reorderRepeaterItems
|
|
15
|
+
} from "./server/repeaterActions";
|
|
16
|
+
function serverOnly(name, hint) {
|
|
17
|
+
return (..._args) => {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`Guardrail: ${name} only works in a Server Component. ${hint} (If this isn't a Client Component, your tooling may not set the "react-server" condition \u2014 import from "@guard-rail/core/server" instead.)`
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const withGuardrail = serverOnly(
|
|
24
|
+
"withGuardrail()",
|
|
25
|
+
"Call it in your root layout, which is a Server Component."
|
|
26
|
+
);
|
|
27
|
+
const configureGuardrail = serverOnly(
|
|
28
|
+
"configureGuardrail()",
|
|
29
|
+
"Configuration is applied by withGuardrail() in your root layout."
|
|
30
|
+
);
|
|
31
|
+
const GRepeater = serverOnly(
|
|
32
|
+
"<GRepeater>",
|
|
33
|
+
"Render it in a Server Component and pass it into this one as children or a prop."
|
|
34
|
+
);
|
|
35
|
+
const listRepeaterItems = serverOnly(
|
|
36
|
+
"listRepeaterItems()",
|
|
37
|
+
"Read repeater items in a Server Component and pass them down."
|
|
38
|
+
);
|
|
39
|
+
const getRepeaterItemBySlug = serverOnly(
|
|
40
|
+
"getRepeaterItemBySlug()",
|
|
41
|
+
"Resolve the item in a Server Component (your [slug]/page.tsx) and pass it down."
|
|
42
|
+
);
|
|
43
|
+
const getFieldValue = serverOnly(
|
|
44
|
+
"getFieldValue()",
|
|
45
|
+
"Read the value in a Server Component and pass it down as a prop."
|
|
46
|
+
);
|
|
47
|
+
export {
|
|
48
|
+
GRail,
|
|
49
|
+
GRepeater,
|
|
50
|
+
addRepeaterItem,
|
|
51
|
+
configureGuardrail,
|
|
52
|
+
createAssetUpload,
|
|
53
|
+
getFieldValue,
|
|
54
|
+
getRepeaterItemBySlug,
|
|
55
|
+
inviteMember,
|
|
56
|
+
listAssets,
|
|
57
|
+
listRepeaterItems,
|
|
58
|
+
listTeam,
|
|
59
|
+
removeMember,
|
|
60
|
+
removeRepeaterItem,
|
|
61
|
+
reorderRepeaterItems,
|
|
62
|
+
updateMemberRole,
|
|
63
|
+
useGRail,
|
|
64
|
+
withGuardrail
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=index.client.js.map
|
|
@@ -0,0 +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} 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 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} 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,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":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,6 @@ export { getFieldValue } from './server/fieldData.js';
|
|
|
8
8
|
export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from './server/repeaterActions.js';
|
|
9
9
|
export { useGRail } from './react/Provider.js';
|
|
10
10
|
export { GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps } from './core/types.js';
|
|
11
|
-
export { AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
|
|
11
|
+
export { AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
|
|
12
12
|
import 'react/jsx-runtime';
|
|
13
13
|
import 'react';
|
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, exported so an app can build its own UI instead of using\n// the bundled drawer.\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\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 OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n} from \"./core/adapter-types\";\n"],"mappings":"AAGA,SAAS,qBAAqB;AAI9B;AAAA,EACE;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, exported so an app can build its own UI instead of using\n// the bundled drawer.\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\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 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} from \"./core/adapter-types\";\n"],"mappings":"AAGA,SAAS,qBAAqB;AAI9B;AAAA,EACE;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":[]}
|
package/dist/react/Admin.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import
|
|
2
|
+
import React__default from 'react';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Owns the theme stylesheet so every piece of chrome — the sign-in bar, the
|
|
6
|
+
* toolbar, and the field popups portaled to <body> — picks up the same
|
|
7
|
+
* light/dark variables from one place.
|
|
8
|
+
*/
|
|
9
|
+
declare function GRailAdmin(props: {
|
|
10
|
+
toolbarExtra?: React__default.ReactNode;
|
|
6
11
|
}): react_jsx_runtime.JSX.Element | null;
|
|
7
12
|
|
|
8
13
|
export { GRailAdmin };
|
package/dist/react/Admin.js
CHANGED
|
@@ -6,9 +6,17 @@ import { loginAction, logoutAction } from "../server/actions";
|
|
|
6
6
|
import { useGRail } from "./Provider";
|
|
7
7
|
import { TeamDrawer } from "./Team";
|
|
8
8
|
import { LogOutIcon, PeopleIcon } from "./Icons";
|
|
9
|
-
import { S } from "./styles";
|
|
9
|
+
import { S, THEME_CSS, v } from "./styles";
|
|
10
10
|
const initialState = { email: "", password: "" };
|
|
11
|
-
function GRailAdmin(
|
|
11
|
+
function GRailAdmin(props) {
|
|
12
|
+
const { isEditMode } = useGRail();
|
|
13
|
+
if (!isEditMode) return null;
|
|
14
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
15
|
+
/* @__PURE__ */ jsx("style", { href: "guardrail-theme", precedence: "default", children: THEME_CSS }),
|
|
16
|
+
/* @__PURE__ */ jsx(AdminBody, { ...props })
|
|
17
|
+
] });
|
|
18
|
+
}
|
|
19
|
+
function AdminBody({ toolbarExtra }) {
|
|
12
20
|
const { isAuth, isEditMode, role, auth } = useGRail();
|
|
13
21
|
const [teamOpen, setTeamOpen] = useState(false);
|
|
14
22
|
const [formState, formAction, isPending] = useActionState(
|
|
@@ -31,7 +39,7 @@ function GRailAdmin({ toolbarExtra }) {
|
|
|
31
39
|
return /* @__PURE__ */ jsx("div", { "data-guardrail": true, style: S.banner, children: /* @__PURE__ */ jsxs("div", { style: S.bannerForm, children: [
|
|
32
40
|
/* @__PURE__ */ jsxs("div", { style: S.brandStack, children: [
|
|
33
41
|
/* @__PURE__ */ jsx("span", { style: S.brand, children: "Guardrail" }),
|
|
34
|
-
!auth.signInUrl && /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: "
|
|
42
|
+
!auth.signInUrl && /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: v("muted") }, children: "No sign-in method is configured for this site." })
|
|
35
43
|
] }),
|
|
36
44
|
auth.signInUrl && /* @__PURE__ */ jsx("a", { href: auth.signInUrl, style: { ...S.primaryBtn, textDecoration: "none" }, children: "Sign in" })
|
|
37
45
|
] }) });
|
|
@@ -72,7 +80,7 @@ function GRailAdmin({ toolbarExtra }) {
|
|
|
72
80
|
/* @__PURE__ */ jsx(
|
|
73
81
|
"button",
|
|
74
82
|
{
|
|
75
|
-
style: { ...S.iconBtn, color: "
|
|
83
|
+
style: { ...S.iconBtn, color: v("muted") },
|
|
76
84
|
onClick: signOut,
|
|
77
85
|
title: "Log out",
|
|
78
86
|
"aria-label": "Log out",
|
|
@@ -90,7 +98,7 @@ function GRailAdmin({ toolbarExtra }) {
|
|
|
90
98
|
/* @__PURE__ */ jsx(
|
|
91
99
|
"button",
|
|
92
100
|
{
|
|
93
|
-
style: { ...S.iconBtn, color: "
|
|
101
|
+
style: { ...S.iconBtn, color: v("muted"), width: 24, height: 24 },
|
|
94
102
|
onClick: () => setTeamOpen(true),
|
|
95
103
|
title: "Team",
|
|
96
104
|
"aria-label": "Team",
|
|
@@ -105,7 +113,7 @@ function GRailAdmin({ toolbarExtra }) {
|
|
|
105
113
|
/* @__PURE__ */ jsx(
|
|
106
114
|
"button",
|
|
107
115
|
{
|
|
108
|
-
style: { ...S.iconBtn, color: "
|
|
116
|
+
style: { ...S.iconBtn, color: v("muted") },
|
|
109
117
|
onClick: signOut,
|
|
110
118
|
title: "Log out",
|
|
111
119
|
"aria-label": "Log out",
|
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 { loginAction, logoutAction } from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { TeamDrawer } from \"./Team\";\nimport { LogOutIcon, PeopleIcon } from \"./Icons\";\nimport { S } from \"./styles\";\nimport type { ActionState } from \"../core/types\";\n\nconst initialState: ActionState = { email: \"\", password: \"\" };\n\nexport function GRailAdmin({ toolbarExtra }: { toolbarExtra?: React.ReactNode }) {\n const { isAuth, isEditMode, role, auth } = useGRail();\n const [teamOpen, setTeamOpen] = useState(false);\n const [formState, formAction, isPending] = useActionState(\n loginAction,\n initialState,\n );\n const router = useRouter();\n const pathname = usePathname();\n\n useEffect(() => {\n if (formState.success) router.refresh();\n }, [formState.success, router]);\n\n const signOut = async () => {\n await logoutAction();\n document.cookie =\n \"gr_edit_mode=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT\";\n window.location.href = pathname;\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: \"
|
|
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 { loginAction, logoutAction } from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { TeamDrawer } from \"./Team\";\nimport { LogOutIcon, PeopleIcon } from \"./Icons\";\nimport { S, THEME_CSS, v } from \"./styles\";\nimport type { ActionState } from \"../core/types\";\n\nconst initialState: ActionState = { email: \"\", password: \"\" };\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, role, auth } = useGRail();\n const [teamOpen, setTeamOpen] = useState(false);\n const [formState, formAction, isPending] = useActionState(\n loginAction,\n initialState,\n );\n const router = useRouter();\n const pathname = usePathname();\n\n useEffect(() => {\n if (formState.success) router.refresh();\n }, [formState.success, router]);\n\n const signOut = async () => {\n await logoutAction();\n document.cookie =\n \"gr_edit_mode=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT\";\n window.location.href = pathname;\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 </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 </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":[]}
|