@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.
Files changed (58) hide show
  1. package/README.md +86 -6
  2. package/dist/adapters/supabase/db.js +11 -0
  3. package/dist/adapters/supabase/db.js.map +1 -1
  4. package/dist/core/adapter-types.d.ts +35 -1
  5. package/dist/core/config.d.ts +7 -1
  6. package/dist/core/config.js +11 -0
  7. package/dist/core/config.js.map +1 -1
  8. package/dist/core/scaffold.d.ts +11 -0
  9. package/dist/core/scaffold.js +19 -0
  10. package/dist/core/scaffold.js.map +1 -0
  11. package/dist/core/types.d.ts +14 -1
  12. package/dist/index.client.d.ts +18 -0
  13. package/dist/index.client.js +66 -0
  14. package/dist/index.client.js.map +1 -0
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/react/Admin.d.ts +8 -3
  18. package/dist/react/Admin.js +14 -6
  19. package/dist/react/Admin.js.map +1 -1
  20. package/dist/react/GRail/client.js +86 -27
  21. package/dist/react/GRail/client.js.map +1 -1
  22. package/dist/react/GRail/editor.d.ts +24 -0
  23. package/dist/react/GRail/editor.js +9 -0
  24. package/dist/react/GRail/editor.js.map +1 -0
  25. package/dist/react/GRail/index.js +2 -2
  26. package/dist/react/GRail/index.js.map +1 -1
  27. package/dist/react/GRail/render.d.ts +3 -3
  28. package/dist/react/GRail/universal.d.ts +22 -0
  29. package/dist/react/GRail/universal.js +29 -0
  30. package/dist/react/GRail/universal.js.map +1 -0
  31. package/dist/react/Icons.d.ts +2 -1
  32. package/dist/react/Icons.js +5 -0
  33. package/dist/react/Icons.js.map +1 -1
  34. package/dist/react/MediaPicker.js +3 -3
  35. package/dist/react/MediaPicker.js.map +1 -1
  36. package/dist/react/Provider.d.ts +9 -5
  37. package/dist/react/Provider.js +10 -4
  38. package/dist/react/Provider.js.map +1 -1
  39. package/dist/react/Repeater/client.d.ts +3 -3
  40. package/dist/react/Repeater/client.js +2 -2
  41. package/dist/react/Repeater/client.js.map +1 -1
  42. package/dist/react/Repeater/index.d.ts +3 -3
  43. package/dist/react/Team.js +7 -7
  44. package/dist/react/Team.js.map +1 -1
  45. package/dist/react/styles.d.ts +104 -30
  46. package/dist/react/styles.js +154 -65
  47. package/dist/react/styles.js.map +1 -1
  48. package/dist/react/useAnchorRect.js +1 -0
  49. package/dist/react/useAnchorRect.js.map +1 -1
  50. package/dist/server/data.js +1 -15
  51. package/dist/server/data.js.map +1 -1
  52. package/dist/server/loader.d.ts +9 -2
  53. package/dist/server/loader.js +74 -11
  54. package/dist/server/loader.js.map +1 -1
  55. package/dist/server/wrapper.d.ts +2 -2
  56. package/dist/server/wrapper.js +6 -2
  57. package/dist/server/wrapper.js.map +1 -1
  58. 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
- Creates the account if it doesn't exist (or reuses it if it does — the same
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 batching loader: every `<GRail>`
375
- rendered in the same pass is collected and fetched in a single query. Set
376
- `GUARDRAIL_DEBUG=1` to log how many keys each batch resolved.
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 };
@@ -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 };
@@ -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
@@ -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":[]}
@@ -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":[]}
@@ -1,8 +1,13 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import React from 'react';
2
+ import React__default from 'react';
3
3
 
4
- declare function GRailAdmin({ toolbarExtra }: {
5
- toolbarExtra?: React.ReactNode;
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 };
@@ -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({ toolbarExtra }) {
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: "#6b7280" }, children: "No sign-in method is configured for this site." })
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: "#9ca3af" },
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: "#9ca3af", width: 24, height: 24 },
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: "#9ca3af" },
116
+ style: { ...S.iconBtn, color: v("muted") },
109
117
  onClick: signOut,
110
118
  title: "Log out",
111
119
  "aria-label": "Log out",
@@ -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: \"#6b7280\" }}>\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: \"#9ca3af\" }}\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: \"#9ca3af\", 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: \"#9ca3af\" }}\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 { 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":[]}