@guard-rail/core 0.2.1 → 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 (44) hide show
  1. package/README.md +67 -3
  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 +2 -2
  18. package/dist/react/Admin.js +1 -1
  19. package/dist/react/Admin.js.map +1 -1
  20. package/dist/react/GRail/editor.d.ts +24 -0
  21. package/dist/react/GRail/editor.js +9 -0
  22. package/dist/react/GRail/editor.js.map +1 -0
  23. package/dist/react/GRail/index.js +2 -2
  24. package/dist/react/GRail/index.js.map +1 -1
  25. package/dist/react/GRail/render.d.ts +3 -3
  26. package/dist/react/GRail/universal.d.ts +22 -0
  27. package/dist/react/GRail/universal.js +29 -0
  28. package/dist/react/GRail/universal.js.map +1 -0
  29. package/dist/react/Provider.d.ts +9 -5
  30. package/dist/react/Provider.js +10 -4
  31. package/dist/react/Provider.js.map +1 -1
  32. package/dist/react/Repeater/client.d.ts +3 -3
  33. package/dist/react/Repeater/index.d.ts +3 -3
  34. package/dist/react/Team.js +1 -1
  35. package/dist/react/Team.js.map +1 -1
  36. package/dist/server/data.js +1 -15
  37. package/dist/server/data.js.map +1 -1
  38. package/dist/server/loader.d.ts +9 -2
  39. package/dist/server/loader.js +74 -11
  40. package/dist/server/loader.js.map +1 -1
  41. package/dist/server/wrapper.d.ts +2 -2
  42. package/dist/server/wrapper.js +6 -2
  43. package/dist/server/wrapper.js.map +1 -1
  44. package/package.json +10 -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:
@@ -381,15 +437,23 @@ const clerkAuth: GRAuthAdapter = {
381
437
  `getSession()` is the sole authorization check for writes. It must validate the
382
438
  session against the provider — never trust the presence of a cookie.
383
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
+
384
445
  `getOverrides(pathKeys, siteId)` is batched by design — it receives every key a
385
446
  render pass needs, already deduplicated. Adapters map their own storage naming
386
447
  at this boundary; core speaks `pathKey` / `siteId` only.
387
448
 
388
449
  ## Performance
389
450
 
390
- Fields are resolved through a request-scoped batching loader: every `<GRail>`
391
- rendered in the same pass is collected and fetched in a single query. Set
392
- `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.
393
457
 
394
458
  ## Known limitations
395
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,5 +1,5 @@
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
4
  /**
5
5
  * Owns the theme stylesheet so every piece of chrome — the sign-in bar, the
@@ -7,7 +7,7 @@ import React from 'react';
7
7
  * light/dark variables from one place.
8
8
  */
9
9
  declare function GRailAdmin(props: {
10
- toolbarExtra?: React.ReactNode;
10
+ toolbarExtra?: React__default.ReactNode;
11
11
  }): react_jsx_runtime.JSX.Element | null;
12
12
 
13
13
  export { GRailAdmin };
@@ -12,7 +12,7 @@ function GRailAdmin(props) {
12
12
  const { isEditMode } = useGRail();
13
13
  if (!isEditMode) return null;
14
14
  return /* @__PURE__ */ jsxs(Fragment, { children: [
15
- /* @__PURE__ */ jsx("style", { children: THEME_CSS }),
15
+ /* @__PURE__ */ jsx("style", { href: "guardrail-theme", precedence: "default", children: THEME_CSS }),
16
16
  /* @__PURE__ */ jsx(AdminBody, { ...props })
17
17
  ] });
18
18
  }
@@ -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, 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 <style>{THEME_CSS}</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":[]}
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":[]}
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+ import { HTMLKeys, OverrideRow } from '../../core/types.js';
3
+
4
+ /**
5
+ * The editor, loaded on demand rather than imported statically.
6
+ *
7
+ * A static import puts the editing UI (popups, image picker, save-action
8
+ * wiring) into the JavaScript every visitor downloads: Next preloads every
9
+ * client module that a route's server graph imports, whether or not that page
10
+ * ever renders it. Confirmed by measurement — before this, an anonymous page
11
+ * loaded the editor chunk. Behind a dynamic import it is only fetched when an
12
+ * editor's <GRail> actually renders it, and it is still rendered on the
13
+ * server, so there is no flash.
14
+ *
15
+ * Shared by the server <GRail> and the client-component one, so the promise
16
+ * that anonymous visitors get no editor code holds for both.
17
+ */
18
+ declare const GRailEditor: React.ComponentType<{
19
+ [key: string]: unknown;
20
+ as?: HTMLKeys;
21
+ initialOverride: OverrideRow;
22
+ }>;
23
+
24
+ export { GRailEditor };
@@ -0,0 +1,9 @@
1
+ "use client";
2
+ import dynamic from "next/dynamic";
3
+ const GRailEditor = dynamic(
4
+ () => import("./client").then((m) => m.GRailClient)
5
+ );
6
+ export {
7
+ GRailEditor
8
+ };
9
+ //# sourceMappingURL=editor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/react/GRail/editor.tsx"],"sourcesContent":["\"use client\";\n\nimport dynamic from \"next/dynamic\";\n\n/**\n * The editor, loaded on demand rather than imported statically.\n *\n * A static import puts the editing UI (popups, image picker, save-action\n * wiring) into the JavaScript every visitor downloads: Next preloads every\n * client module that a route's server graph imports, whether or not that page\n * ever renders it. Confirmed by measurement — before this, an anonymous page\n * loaded the editor chunk. Behind a dynamic import it is only fetched when an\n * editor's <GRail> actually renders it, and it is still rendered on the\n * server, so there is no flash.\n *\n * Shared by the server <GRail> and the client-component one, so the promise\n * that anonymous visitors get no editor code holds for both.\n */\nexport const GRailEditor = dynamic(() =>\n import(\"./client\").then((m) => m.GRailClient),\n);\n"],"mappings":";AAEA,OAAO,aAAa;AAgBb,MAAM,cAAc;AAAA,EAAQ,MACjC,OAAO,UAAU,EAAE,KAAK,CAAC,MAAM,EAAE,WAAW;AAC9C;","names":[]}
@@ -1,6 +1,6 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import React from "react";
3
- import { GRailClient } from "./client";
3
+ import { GRailEditor } from "./editor";
4
4
  import { GRailRender } from "./render";
5
5
  import { fetchOrScaffold } from "../../server/data";
6
6
  import { getCachedViewer } from "../../server/session";
@@ -9,7 +9,7 @@ async function GRail(props) {
9
9
  const override = await fetchOrScaffold(id, props);
10
10
  const viewer = await getCachedViewer();
11
11
  if (editable && viewer && viewer.role) {
12
- return /* @__PURE__ */ jsx(GRailClient, { ...rest, initialOverride: override });
12
+ return /* @__PURE__ */ jsx(GRailEditor, { ...rest, initialOverride: override });
13
13
  }
14
14
  return /* @__PURE__ */ jsx(GRailRender, { ...rest, override: override.value });
15
15
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/react/GRail/index.tsx"],"sourcesContent":["import React from \"react\";\nimport { GRailClient } from \"./client\";\nimport { GRailRender } from \"./render\";\nimport { fetchOrScaffold } from \"../../server/data\";\nimport { getCachedViewer } from \"../../server/session\";\nimport type { HTMLKeys, PolymorphicProps } from \"../../core/types\";\n\n/**\n * An editable field. Server Component by default: anonymous visitors get plain\n * server-rendered markup with no client JS. The editor only loads for a\n * verified session.\n */\nexport async function GRail<T extends HTMLKeys = \"span\">(\n props: PolymorphicProps<T>,\n) {\n const { id, editable = true, ...rest } = props;\n const override = await fetchOrScaffold(id, props as Record<string, unknown>);\n const viewer = await getCachedViewer();\n\n // Only members who can actually write get the editor, and only when this\n // field allows it — everyone else gets the same server-rendered markup an\n // anonymous visitor sees.\n if (editable && viewer && viewer.role) {\n return <GRailClient {...(rest as object)} initialOverride={override} />;\n }\n\n return <GRailRender {...(rest as object)} override={override.value} />;\n}\n"],"mappings":"AAuBW;AAvBX,OAAO,WAAW;AAClB,SAAS,mBAAmB;AAC5B,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAChC,SAAS,uBAAuB;AAQhC,eAAsB,MACpB,OACA;AACA,QAAM,EAAE,IAAI,WAAW,MAAM,GAAG,KAAK,IAAI;AACzC,QAAM,WAAW,MAAM,gBAAgB,IAAI,KAAgC;AAC3E,QAAM,SAAS,MAAM,gBAAgB;AAKrC,MAAI,YAAY,UAAU,OAAO,MAAM;AACrC,WAAO,oBAAC,eAAa,GAAI,MAAiB,iBAAiB,UAAU;AAAA,EACvE;AAEA,SAAO,oBAAC,eAAa,GAAI,MAAiB,UAAU,SAAS,OAAO;AACtE;","names":[]}
1
+ {"version":3,"sources":["../../../src/react/GRail/index.tsx"],"sourcesContent":["import React from \"react\";\nimport { GRailEditor } from \"./editor\";\nimport { GRailRender } from \"./render\";\nimport { fetchOrScaffold } from \"../../server/data\";\nimport { getCachedViewer } from \"../../server/session\";\nimport type { HTMLKeys, PolymorphicProps } from \"../../core/types\";\n\n/**\n * An editable field. Server Component by default: anonymous visitors get plain\n * server-rendered markup with no client JS. The editor only loads for a\n * verified session.\n */\nexport async function GRail<T extends HTMLKeys = \"span\">(\n props: PolymorphicProps<T>,\n) {\n const { id, editable = true, ...rest } = props;\n const override = await fetchOrScaffold(id, props as Record<string, unknown>);\n const viewer = await getCachedViewer();\n\n // Only members who can actually write get the editor, and only when this\n // field allows it — everyone else gets the same server-rendered markup an\n // anonymous visitor sees.\n if (editable && viewer && viewer.role) {\n return <GRailEditor {...(rest as object)} initialOverride={override} />;\n }\n\n return <GRailRender {...(rest as object)} override={override.value} />;\n}\n"],"mappings":"AAuBW;AAvBX,OAAO,WAAW;AAClB,SAAS,mBAAmB;AAC5B,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAChC,SAAS,uBAAuB;AAQhC,eAAsB,MACpB,OACA;AACA,QAAM,EAAE,IAAI,WAAW,MAAM,GAAG,KAAK,IAAI;AACzC,QAAM,WAAW,MAAM,gBAAgB,IAAI,KAAgC;AAC3E,QAAM,SAAS,MAAM,gBAAgB;AAKrC,MAAI,YAAY,UAAU,OAAO,MAAM;AACrC,WAAO,oBAAC,eAAa,GAAI,MAAiB,iBAAiB,UAAU;AAAA,EACvE;AAEA,SAAO,oBAAC,eAAa,GAAI,MAAiB,UAAU,SAAS,OAAO;AACtE;","names":[]}
@@ -1,13 +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
  import { HTMLKeys, OverrideValue } from '../../core/types.js';
4
4
 
5
5
  interface RenderProps {
6
6
  as?: HTMLKeys;
7
7
  override: OverrideValue;
8
- children?: React.ReactNode;
8
+ children?: React__default.ReactNode;
9
9
  /** Forwarded to the rendered element so the editor can measure it. */
10
- ref?: React.Ref<HTMLElement>;
10
+ ref?: React__default.Ref<HTMLElement>;
11
11
  [key: string]: unknown;
12
12
  }
13
13
  /**
@@ -0,0 +1,22 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { HTMLKeys, PolymorphicProps } from '../../core/types.js';
3
+ import 'react';
4
+
5
+ /**
6
+ * <GRail> as seen from a Client Component.
7
+ *
8
+ * `import { GRail } from "@guard-rail/core"` resolves here (via the
9
+ * "react-server" export condition) whenever the importing file is a Client
10
+ * Component, and to the async server version everywhere else — same name, same
11
+ * props, so a component moving between the two needs no code change.
12
+ *
13
+ * A Client Component can't await a query, so instead of fetching this reads its
14
+ * value from content the layout preloaded into context. That context is filled
15
+ * during the server render, so anonymous visitors still get real content in the
16
+ * initial HTML (this component is itself server-rendered for the first paint) —
17
+ * SEO is unaffected. An unedited field, or one outside the preload, renders the
18
+ * JSX that was written, exactly like the server version.
19
+ */
20
+ declare function GRail<T extends HTMLKeys = "span">(props: PolymorphicProps<T>): react_jsx_runtime.JSX.Element;
21
+
22
+ export { GRail };
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import { GRailEditor } from "./editor";
5
+ import { GRailRender } from "./render";
6
+ import { useGRail, useGRailContent } from "../Provider";
7
+ import { generateScaffold } from "../../core/scaffold";
8
+ let warned = false;
9
+ function GRail(props) {
10
+ const { id, editable = true, ...rest } = props;
11
+ const { role } = useGRail();
12
+ const content = useGRailContent();
13
+ if (content === void 0 && process.env.NODE_ENV !== "production" && !warned) {
14
+ warned = true;
15
+ console.warn(
16
+ `Guardrail: <GRail id="${id}"> is inside a Client Component, but no content was preloaded for it, so it shows its JSX fallback. Preloading is off (prefetch: false) or this database adapter has no listOverrides().`
17
+ );
18
+ }
19
+ const stored = content?.values[id];
20
+ const override = stored ? { id: `content-${id}`, pathKey: id, value: stored } : generateScaffold(id, props);
21
+ if (editable && role) {
22
+ return /* @__PURE__ */ jsx(GRailEditor, { ...rest, initialOverride: override });
23
+ }
24
+ return /* @__PURE__ */ jsx(GRailRender, { ...rest, override: override.value });
25
+ }
26
+ export {
27
+ GRail
28
+ };
29
+ //# sourceMappingURL=universal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/react/GRail/universal.tsx"],"sourcesContent":["\"use client\";\n\nimport React from \"react\";\nimport { GRailEditor } from \"./editor\";\nimport { GRailRender } from \"./render\";\nimport { useGRail, useGRailContent } from \"../Provider\";\nimport { generateScaffold } from \"../../core/scaffold\";\nimport type { HTMLKeys, OverrideRow, PolymorphicProps } from \"../../core/types\";\n\nlet warned = false;\n\n/**\n * <GRail> as seen from a Client Component.\n *\n * `import { GRail } from \"@guard-rail/core\"` resolves here (via the\n * \"react-server\" export condition) whenever the importing file is a Client\n * Component, and to the async server version everywhere else — same name, same\n * props, so a component moving between the two needs no code change.\n *\n * A Client Component can't await a query, so instead of fetching this reads its\n * value from content the layout preloaded into context. That context is filled\n * during the server render, so anonymous visitors still get real content in the\n * initial HTML (this component is itself server-rendered for the first paint) —\n * SEO is unaffected. An unedited field, or one outside the preload, renders the\n * JSX that was written, exactly like the server version.\n */\nexport function GRail<T extends HTMLKeys = \"span\">(props: PolymorphicProps<T>) {\n const { id, editable = true, ...rest } = props;\n const { role } = useGRail();\n const content = useGRailContent();\n\n if (content === undefined && process.env.NODE_ENV !== \"production\" && !warned) {\n warned = true;\n console.warn(\n `Guardrail: <GRail id=\"${id}\"> is inside a Client Component, but no content was ` +\n `preloaded for it, so it shows its JSX fallback. Preloading is off ` +\n `(prefetch: false) or this database adapter has no listOverrides().`,\n );\n }\n\n const stored = content?.values[id];\n const override: OverrideRow = stored\n ? { id: `content-${id}`, pathKey: id, value: stored }\n : generateScaffold(id, props as Record<string, unknown>);\n\n // Same gate as the server version: only a member who can write gets the\n // editor, and only when this field allows it.\n if (editable && role) {\n return <GRailEditor {...(rest as object)} initialOverride={override} />;\n }\n return <GRailRender {...(rest as object)} override={override.value} />;\n}\n"],"mappings":";AAgDW;AA9CX,OAAO,WAAW;AAClB,SAAS,mBAAmB;AAC5B,SAAS,mBAAmB;AAC5B,SAAS,UAAU,uBAAuB;AAC1C,SAAS,wBAAwB;AAGjC,IAAI,SAAS;AAiBN,SAAS,MAAmC,OAA4B;AAC7E,QAAM,EAAE,IAAI,WAAW,MAAM,GAAG,KAAK,IAAI;AACzC,QAAM,EAAE,KAAK,IAAI,SAAS;AAC1B,QAAM,UAAU,gBAAgB;AAEhC,MAAI,YAAY,UAAa,QAAQ,IAAI,aAAa,gBAAgB,CAAC,QAAQ;AAC7E,aAAS;AACT,YAAQ;AAAA,MACN,yBAAyB,EAAE;AAAA,IAG7B;AAAA,EACF;AAEA,QAAM,SAAS,SAAS,OAAO,EAAE;AACjC,QAAM,WAAwB,SAC1B,EAAE,IAAI,WAAW,EAAE,IAAI,SAAS,IAAI,OAAO,OAAO,IAClD,iBAAiB,IAAI,KAAgC;AAIzD,MAAI,YAAY,MAAM;AACpB,WAAO,oBAAC,eAAa,GAAI,MAAiB,iBAAiB,UAAU;AAAA,EACvE;AACA,SAAO,oBAAC,eAAa,GAAI,MAAiB,UAAU,SAAS,OAAO;AACtE;","names":[]}
@@ -1,11 +1,15 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import React from 'react';
3
- import { GRState } from '../core/types.js';
2
+ import React__default from 'react';
3
+ import { GRState, GRContent } from '../core/types.js';
4
4
 
5
- declare function GRailProvider({ children, state, }: {
6
- children: React.ReactNode;
5
+ declare function GRailProvider({ children, state, content, }: {
6
+ children: React__default.ReactNode;
7
7
  state: GRState;
8
+ /** Preloaded field values for <GRail>s inside Client Components. */
9
+ content?: GRContent;
8
10
  }): react_jsx_runtime.JSX.Element;
11
+ /** Internal — the client-component <GRail> reads its preloaded value from here. */
12
+ declare function useGRailContent(): GRContent | undefined;
9
13
  declare function useGRail(): GRState;
10
14
 
11
- export { GRailProvider, useGRail };
15
+ export { GRailProvider, useGRail, useGRailContent };
@@ -3,14 +3,19 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import React, { createContext, useContext, Suspense } from "react";
4
4
  import { EditSyncer } from "./Syncer";
5
5
  const GRContext = createContext(void 0);
6
+ const GRContentContext = createContext(void 0);
6
7
  function GRailProvider({
7
8
  children,
8
- state
9
+ state,
10
+ content
9
11
  }) {
10
- return /* @__PURE__ */ jsxs(GRContext.Provider, { value: state, children: [
12
+ return /* @__PURE__ */ jsx(GRContext.Provider, { value: state, children: /* @__PURE__ */ jsxs(GRContentContext.Provider, { value: content, children: [
11
13
  /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(EditSyncer, {}) }),
12
14
  children
13
- ] });
15
+ ] }) });
16
+ }
17
+ function useGRailContent() {
18
+ return useContext(GRContentContext);
14
19
  }
15
20
  function useGRail() {
16
21
  const context = useContext(GRContext);
@@ -21,6 +26,7 @@ function useGRail() {
21
26
  }
22
27
  export {
23
28
  GRailProvider,
24
- useGRail
29
+ useGRail,
30
+ useGRailContent
25
31
  };
26
32
  //# sourceMappingURL=Provider.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/Provider.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { createContext, useContext, Suspense } from \"react\";\nimport type { GRState } from \"../core/types\";\nimport { EditSyncer } from \"./Syncer\";\n\nconst GRContext = createContext<GRState | undefined>(undefined);\n\nexport function GRailProvider({\n children,\n state,\n}: {\n children: React.ReactNode;\n state: GRState;\n}) {\n // Deliberately not held in useState. The server is the source of truth here,\n // and useState ignores prop changes after mount — so after signing in,\n // router.refresh() would deliver a fresh state that the provider discarded,\n // leaving the login bar on screen until a full page reload.\n return (\n <GRContext.Provider value={state}>\n {/* EditSyncer reads searchParams, which suspends during static render. */}\n <Suspense fallback={null}>\n <EditSyncer />\n </Suspense>\n {children}\n </GRContext.Provider>\n );\n}\n\nexport function useGRail(): GRState {\n const context = useContext(GRContext);\n if (!context) {\n throw new Error(\"useGRail must be used inside withGuardrail()\");\n }\n return context;\n}\n"],"mappings":";AAoBI,SAGI,KAHJ;AAlBJ,OAAO,SAAS,eAAe,YAAY,gBAAgB;AAE3D,SAAS,kBAAkB;AAE3B,MAAM,YAAY,cAAmC,MAAS;AAEvD,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AACF,GAGG;AAKD,SACE,qBAAC,UAAU,UAAV,EAAmB,OAAO,OAEzB;AAAA,wBAAC,YAAS,UAAU,MAClB,8BAAC,cAAW,GACd;AAAA,IACC;AAAA,KACH;AAEJ;AAEO,SAAS,WAAoB;AAClC,QAAM,UAAU,WAAW,SAAS;AACpC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,8CAA8C;AAAA,EAChE;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/react/Provider.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { createContext, useContext, Suspense } from \"react\";\nimport type { GRContent, GRState } from \"../core/types\";\nimport { EditSyncer } from \"./Syncer\";\n\nconst GRContext = createContext<GRState | undefined>(undefined);\nconst GRContentContext = createContext<GRContent | undefined>(undefined);\n\nexport function GRailProvider({\n children,\n state,\n content,\n}: {\n children: React.ReactNode;\n state: GRState;\n /** Preloaded field values for <GRail>s inside Client Components. */\n content?: GRContent;\n}) {\n // Deliberately not held in useState. The server is the source of truth here,\n // and useState ignores prop changes after mount — so after signing in,\n // router.refresh() would deliver a fresh state that the provider discarded,\n // leaving the login bar on screen until a full page reload.\n return (\n <GRContext.Provider value={state}>\n <GRContentContext.Provider value={content}>\n {/* EditSyncer reads searchParams, which suspends during static render. */}\n <Suspense fallback={null}>\n <EditSyncer />\n </Suspense>\n {children}\n </GRContentContext.Provider>\n </GRContext.Provider>\n );\n}\n\n/** Internal — the client-component <GRail> reads its preloaded value from here. */\nexport function useGRailContent(): GRContent | undefined {\n return useContext(GRContentContext);\n}\n\nexport function useGRail(): GRState {\n const context = useContext(GRContext);\n if (!context) {\n throw new Error(\"useGRail must be used inside withGuardrail()\");\n }\n return context;\n}\n"],"mappings":";AAyBM,SAGI,KAHJ;AAvBN,OAAO,SAAS,eAAe,YAAY,gBAAgB;AAE3D,SAAS,kBAAkB;AAE3B,MAAM,YAAY,cAAmC,MAAS;AAC9D,MAAM,mBAAmB,cAAqC,MAAS;AAEhE,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AACF,GAKG;AAKD,SACE,oBAAC,UAAU,UAAV,EAAmB,OAAO,OACzB,+BAAC,iBAAiB,UAAjB,EAA0B,OAAO,SAEhC;AAAA,wBAAC,YAAS,UAAU,MAClB,8BAAC,cAAW,GACd;AAAA,IACC;AAAA,KACH,GACF;AAEJ;AAGO,SAAS,kBAAyC;AACvD,SAAO,WAAW,gBAAgB;AACpC;AAEO,SAAS,WAAoB;AAClC,QAAM,UAAU,WAAW,SAAS;AACpC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,8CAA8C;AAAA,EAChE;AACA,SAAO;AACT;","names":[]}
@@ -1,16 +1,16 @@
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
4
  interface RenderedItem {
5
5
  id: string;
6
6
  /** Already rendered server-side — this component only wraps it in chrome. */
7
- node: React.ReactNode;
7
+ node: React__default.ReactNode;
8
8
  }
9
9
  declare function RepeaterClient({ repeaterId, routed, items, children, }: {
10
10
  repeaterId: string;
11
11
  routed?: boolean;
12
12
  items: RenderedItem[];
13
- children?: React.ReactNode;
13
+ children?: React__default.ReactNode;
14
14
  }): react_jsx_runtime.JSX.Element;
15
15
 
16
16
  export { RepeaterClient };
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import React from 'react';
2
+ import React__default from 'react';
3
3
  import { GRRepeaterItem } from '../../core/types.js';
4
4
 
5
5
  interface RepeaterItemProps {
@@ -9,7 +9,7 @@ interface GRepeaterProps {
9
9
  /** The repeater's own key. Item fields are namespaced under it: `${id}.${item.id}.*`. */
10
10
  id: string;
11
11
  /** The developer's own component — the one place the item's shape is defined. */
12
- as: React.ComponentType<RepeaterItemProps>;
12
+ as: React__default.ComponentType<RepeaterItemProps>;
13
13
  /**
14
14
  * Marks this repeater as routed: each item gets its own URL (a product, a
15
15
  * post), so adding one prompts for a slug. Omit for a plain row of items on
@@ -17,7 +17,7 @@ interface GRepeaterProps {
17
17
  */
18
18
  routed?: boolean;
19
19
  /** Shown in place of the list when it has no items yet. */
20
- children?: React.ReactNode;
20
+ children?: React__default.ReactNode;
21
21
  }
22
22
  /**
23
23
  * Renders N instances of one developer-authored template component, with the
@@ -72,7 +72,7 @@ function TeamDrawer({ onClose }) {
72
72
  return () => window.removeEventListener("keydown", onKey);
73
73
  }, [requestClose]);
74
74
  return /* @__PURE__ */ jsxs(Fragment, { children: [
75
- /* @__PURE__ */ jsx("style", { children: ANIMATION_CSS }),
75
+ /* @__PURE__ */ jsx("style", { href: "guardrail-team-animation", precedence: "default", children: ANIMATION_CSS }),
76
76
  /* @__PURE__ */ jsx(
77
77
  "div",
78
78
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/Team.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useCallback, useEffect, useState, useTransition } from \"react\";\nimport {\n inviteMember,\n listTeam,\n removeMember,\n updateMemberRole,\n} from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { CloseIcon } from \"./Icons\";\nimport { S, v } from \"./styles\";\nimport type { GRMember, GRRole } from \"../core/types\";\n\nconst ROLES: GRRole[] = [\"admin\", \"editor\"];\n\n/**\n * Keyframes cannot be expressed as inline styles, so the drawer ships the few\n * it needs. Scoped to [data-guardrail-team] to stay clear of app styles, and\n * disabled under prefers-reduced-motion.\n */\nconst ANIMATION_CSS = `\n@keyframes gr-fade-in { from { opacity: 0 } to { opacity: 1 } }\n@keyframes gr-slide-in {\n from { transform: translateX(100%) }\n to { transform: translateX(0) }\n}\n[data-guardrail-scrim] { animation: gr-fade-in 160ms ease-out both }\n[data-guardrail-team] { animation: gr-slide-in 220ms cubic-bezier(0.32, 0.72, 0, 1) both }\n[data-guardrail-team] [data-gr-row] { animation: gr-fade-in 240ms ease-out both }\n@keyframes gr-fade-out { from { opacity: 1 } to { opacity: 0 } }\n@keyframes gr-slide-out {\n from { transform: translateX(0) }\n to { transform: translateX(100%) }\n}\n[data-guardrail-scrim][data-closing] { animation: gr-fade-out 180ms ease-in both }\n[data-guardrail-team][data-closing] { animation: gr-slide-out 180ms cubic-bezier(0.4, 0, 1, 1) both }\n@media (prefers-reduced-motion: reduce) {\n [data-guardrail-scrim], [data-guardrail-team], [data-guardrail-team] [data-gr-row] {\n animation: none\n }\n}\n`;\n\nexport function TeamDrawer({ onClose }: { onClose: () => void }) {\n const { role, email: currentEmail } = useGRail();\n const isAdmin = role === \"admin\";\n\n const [members, setMembers] = useState<GRMember[]>([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [inviteEmail, setInviteEmail] = useState(\"\");\n const [inviteRole, setInviteRole] = useState<GRRole>(\"editor\");\n const [pending, startTransition] = useTransition();\n\n const refresh = () =>\n listTeam().then((rows) => {\n setMembers(rows);\n setLoading(false);\n });\n\n useEffect(() => {\n void refresh();\n }, []);\n\n // Server actions re-check the role; this only decides what to render.\n // Always refresh: a failure can still have changed the roster (an invite\n // that saved but whose email failed, for instance).\n const run = (fn: () => Promise<{ success: boolean; error?: string }>) =>\n startTransition(async () => {\n setError(null);\n const res = await fn();\n if (!res.success) setError(res.error ?? \"Something went wrong.\");\n await refresh();\n });\n\n /**\n * Unmounting on close skips the exit animation entirely. Stay mounted, play\n * it, and let the parent unmount once it finishes.\n */\n const [closing, setClosing] = useState(false);\n const requestClose = useCallback(() => {\n const reduced =\n typeof window !== \"undefined\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n if (reduced) return onClose(); // No animation, so no animationend to wait for.\n setClosing(true);\n }, [onClose]);\n\n useEffect(() => {\n if (!closing) return;\n // Safety net if animationend never arrives (tab hidden, styles stripped).\n const t = window.setTimeout(onClose, 400);\n return () => window.clearTimeout(t);\n }, [closing, onClose]);\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && requestClose();\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [requestClose]);\n\n return (\n <>\n <style>{ANIMATION_CSS}</style>\n <div\n data-guardrail\n data-guardrail-scrim\n {...(closing ? { \"data-closing\": \"\" } : {})}\n style={S.scrim}\n onClick={requestClose}\n />\n <aside\n data-guardrail\n data-guardrail-team\n {...(closing ? { \"data-closing\": \"\" } : {})}\n onAnimationEnd={(e) => {\n // Row fade-ins bubble up here too; only the drawer's own exit counts.\n if (closing && e.target === e.currentTarget) onClose();\n }}\n style={S.drawer}\n role=\"dialog\"\n aria-label=\"Team\"\n >\n <header style={S.drawerHead}>\n <h2 style={S.drawerTitle}>Team</h2>\n <button style={{ ...S.iconBtn, color: v(\"muted\") }} onClick={requestClose} aria-label=\"Close\">\n <CloseIcon />\n </button>\n </header>\n\n <div style={S.drawerBody}>\n {error && <div style={{ ...S.error, marginBottom: 12 }}>{error}</div>}\n\n {loading ? (\n <p style={{ fontSize: 13, color: v(\"muted\") }}>Loading…</p>\n ) : (\n members.map((m) => (\n <div key={m.email} data-gr-row style={S.memberRow}>\n <span style={S.memberEmail} title={m.email}>\n {m.email}\n {m.email === currentEmail && \" (you)\"}\n </span>\n\n {m.status === \"pending\" && <span style={S.badge}>Invited</span>}\n\n {isAdmin ? (\n <select\n style={S.select}\n value={m.role}\n disabled={pending}\n onChange={(e) =>\n run(() =>\n updateMemberRole(m.email, e.target.value as GRRole),\n )\n }\n >\n {ROLES.map((r) => (\n <option key={r} value={r}>\n {r}\n </option>\n ))}\n </select>\n ) : (\n <span style={{ fontSize: 12, color: v(\"muted\") }}>{m.role}</span>\n )}\n\n {isAdmin && (\n <button\n style={{ ...S.iconBtn, color: v(\"muted\"), width: 22, height: 22 }}\n disabled={pending}\n onClick={() => run(() => removeMember(m.email))}\n aria-label={`Remove ${m.email}`}\n title=\"Remove\"\n >\n <CloseIcon />\n </button>\n )}\n </div>\n ))\n )}\n\n {isAdmin && (\n <>\n <p style={S.sectionLabel}>Invite someone</p>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n run(async () => {\n const res = await inviteMember(inviteEmail, inviteRole);\n if (res.success) setInviteEmail(\"\");\n return res;\n });\n }}\n >\n <input\n style={S.field}\n type=\"email\"\n placeholder=\"name@company.com\"\n value={inviteEmail}\n onChange={(e) => setInviteEmail(e.target.value)}\n required\n />\n <div style={{ display: \"flex\", gap: 8 }}>\n <select\n style={{ ...S.select, flex: 1 }}\n value={inviteRole}\n onChange={(e) => setInviteRole(e.target.value as GRRole)}\n >\n {ROLES.map((r) => (\n <option key={r} value={r}>\n {r}\n </option>\n ))}\n </select>\n <button style={S.primaryBtn} type=\"submit\" disabled={pending}>\n {pending ? \"Working…\" : \"Invite\"}\n </button>\n </div>\n </form>\n <p style={{ fontSize: 11, color: v(\"subtle\"), marginTop: 10 }}>\n Guardrail records the invite. Sending the email is your app&apos;s\n job — wire the <code>onInvite</code> handler.\n </p>\n </>\n )}\n </div>\n </aside>\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/react/Team.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useCallback, useEffect, useState, useTransition } from \"react\";\nimport {\n inviteMember,\n listTeam,\n removeMember,\n updateMemberRole,\n} from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { CloseIcon } from \"./Icons\";\nimport { S, v } from \"./styles\";\nimport type { GRMember, GRRole } from \"../core/types\";\n\nconst ROLES: GRRole[] = [\"admin\", \"editor\"];\n\n/**\n * Keyframes cannot be expressed as inline styles, so the drawer ships the few\n * it needs. Scoped to [data-guardrail-team] to stay clear of app styles, and\n * disabled under prefers-reduced-motion.\n */\nconst ANIMATION_CSS = `\n@keyframes gr-fade-in { from { opacity: 0 } to { opacity: 1 } }\n@keyframes gr-slide-in {\n from { transform: translateX(100%) }\n to { transform: translateX(0) }\n}\n[data-guardrail-scrim] { animation: gr-fade-in 160ms ease-out both }\n[data-guardrail-team] { animation: gr-slide-in 220ms cubic-bezier(0.32, 0.72, 0, 1) both }\n[data-guardrail-team] [data-gr-row] { animation: gr-fade-in 240ms ease-out both }\n@keyframes gr-fade-out { from { opacity: 1 } to { opacity: 0 } }\n@keyframes gr-slide-out {\n from { transform: translateX(0) }\n to { transform: translateX(100%) }\n}\n[data-guardrail-scrim][data-closing] { animation: gr-fade-out 180ms ease-in both }\n[data-guardrail-team][data-closing] { animation: gr-slide-out 180ms cubic-bezier(0.4, 0, 1, 1) both }\n@media (prefers-reduced-motion: reduce) {\n [data-guardrail-scrim], [data-guardrail-team], [data-guardrail-team] [data-gr-row] {\n animation: none\n }\n}\n`;\n\nexport function TeamDrawer({ onClose }: { onClose: () => void }) {\n const { role, email: currentEmail } = useGRail();\n const isAdmin = role === \"admin\";\n\n const [members, setMembers] = useState<GRMember[]>([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [inviteEmail, setInviteEmail] = useState(\"\");\n const [inviteRole, setInviteRole] = useState<GRRole>(\"editor\");\n const [pending, startTransition] = useTransition();\n\n const refresh = () =>\n listTeam().then((rows) => {\n setMembers(rows);\n setLoading(false);\n });\n\n useEffect(() => {\n void refresh();\n }, []);\n\n // Server actions re-check the role; this only decides what to render.\n // Always refresh: a failure can still have changed the roster (an invite\n // that saved but whose email failed, for instance).\n const run = (fn: () => Promise<{ success: boolean; error?: string }>) =>\n startTransition(async () => {\n setError(null);\n const res = await fn();\n if (!res.success) setError(res.error ?? \"Something went wrong.\");\n await refresh();\n });\n\n /**\n * Unmounting on close skips the exit animation entirely. Stay mounted, play\n * it, and let the parent unmount once it finishes.\n */\n const [closing, setClosing] = useState(false);\n const requestClose = useCallback(() => {\n const reduced =\n typeof window !== \"undefined\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n if (reduced) return onClose(); // No animation, so no animationend to wait for.\n setClosing(true);\n }, [onClose]);\n\n useEffect(() => {\n if (!closing) return;\n // Safety net if animationend never arrives (tab hidden, styles stripped).\n const t = window.setTimeout(onClose, 400);\n return () => window.clearTimeout(t);\n }, [closing, onClose]);\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && requestClose();\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [requestClose]);\n\n return (\n <>\n {/* Hoisted to <head> — see the note on the theme <style> in Admin.tsx. */}\n <style href=\"guardrail-team-animation\" precedence=\"default\">\n {ANIMATION_CSS}\n </style>\n <div\n data-guardrail\n data-guardrail-scrim\n {...(closing ? { \"data-closing\": \"\" } : {})}\n style={S.scrim}\n onClick={requestClose}\n />\n <aside\n data-guardrail\n data-guardrail-team\n {...(closing ? { \"data-closing\": \"\" } : {})}\n onAnimationEnd={(e) => {\n // Row fade-ins bubble up here too; only the drawer's own exit counts.\n if (closing && e.target === e.currentTarget) onClose();\n }}\n style={S.drawer}\n role=\"dialog\"\n aria-label=\"Team\"\n >\n <header style={S.drawerHead}>\n <h2 style={S.drawerTitle}>Team</h2>\n <button style={{ ...S.iconBtn, color: v(\"muted\") }} onClick={requestClose} aria-label=\"Close\">\n <CloseIcon />\n </button>\n </header>\n\n <div style={S.drawerBody}>\n {error && <div style={{ ...S.error, marginBottom: 12 }}>{error}</div>}\n\n {loading ? (\n <p style={{ fontSize: 13, color: v(\"muted\") }}>Loading…</p>\n ) : (\n members.map((m) => (\n <div key={m.email} data-gr-row style={S.memberRow}>\n <span style={S.memberEmail} title={m.email}>\n {m.email}\n {m.email === currentEmail && \" (you)\"}\n </span>\n\n {m.status === \"pending\" && <span style={S.badge}>Invited</span>}\n\n {isAdmin ? (\n <select\n style={S.select}\n value={m.role}\n disabled={pending}\n onChange={(e) =>\n run(() =>\n updateMemberRole(m.email, e.target.value as GRRole),\n )\n }\n >\n {ROLES.map((r) => (\n <option key={r} value={r}>\n {r}\n </option>\n ))}\n </select>\n ) : (\n <span style={{ fontSize: 12, color: v(\"muted\") }}>{m.role}</span>\n )}\n\n {isAdmin && (\n <button\n style={{ ...S.iconBtn, color: v(\"muted\"), width: 22, height: 22 }}\n disabled={pending}\n onClick={() => run(() => removeMember(m.email))}\n aria-label={`Remove ${m.email}`}\n title=\"Remove\"\n >\n <CloseIcon />\n </button>\n )}\n </div>\n ))\n )}\n\n {isAdmin && (\n <>\n <p style={S.sectionLabel}>Invite someone</p>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n run(async () => {\n const res = await inviteMember(inviteEmail, inviteRole);\n if (res.success) setInviteEmail(\"\");\n return res;\n });\n }}\n >\n <input\n style={S.field}\n type=\"email\"\n placeholder=\"name@company.com\"\n value={inviteEmail}\n onChange={(e) => setInviteEmail(e.target.value)}\n required\n />\n <div style={{ display: \"flex\", gap: 8 }}>\n <select\n style={{ ...S.select, flex: 1 }}\n value={inviteRole}\n onChange={(e) => setInviteRole(e.target.value as GRRole)}\n >\n {ROLES.map((r) => (\n <option key={r} value={r}>\n {r}\n </option>\n ))}\n </select>\n <button style={S.primaryBtn} type=\"submit\" disabled={pending}>\n {pending ? \"Working…\" : \"Invite\"}\n </button>\n </div>\n </form>\n <p style={{ fontSize: 11, color: v(\"subtle\"), marginTop: 10 }}>\n Guardrail records the invite. Sending the email is your app&apos;s\n job — wire the <code>onInvite</code> handler.\n </p>\n </>\n )}\n </div>\n </aside>\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,23 +1,9 @@
1
+ import { generateScaffold } from "../core/scaffold";
1
2
  import { loadOverride } from "./loader";
2
3
  async function fetchOrScaffold(pathKey, props) {
3
4
  const row = await loadOverride(pathKey);
4
5
  return row ?? generateScaffold(pathKey, props);
5
6
  }
6
- function generateScaffold(pathKey, props) {
7
- const { children, type, src, alt, href } = props;
8
- const defaultText = typeof children === "string" || typeof children === "number" ? String(children) : "";
9
- let value;
10
- if (type === "image" || src) {
11
- value = { type: "image", src: src ?? "", alt: alt ?? "" };
12
- } else if (type === "link" || href) {
13
- value = { type: "link", href: href ?? "", text: defaultText };
14
- } else if (type === "button") {
15
- value = { type: "button", text: defaultText };
16
- } else {
17
- value = { type: "text", text: defaultText };
18
- }
19
- return { id: `scaffold-${pathKey}`, pathKey, value };
20
- }
21
7
  export {
22
8
  fetchOrScaffold
23
9
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/data.ts"],"sourcesContent":["import type { OverrideRow, OverrideValue } from \"../core/types\";\nimport { loadOverride } from \"./loader\";\n\n/**\n * Server-side fetch for a single field. Runs during SSR/ISR so anonymous\n * visitors get the real content in the initial HTML.\n */\nexport async function fetchOrScaffold(\n pathKey: string,\n props: Record<string, unknown>,\n): Promise<OverrideRow> {\n // Adapters normalize storage quirks (stringified JSON, column naming), so\n // anything returned here is already a domain-shaped row.\n const row = await loadOverride(pathKey);\n return row ?? generateScaffold(pathKey, props);\n}\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 */\nfunction 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":"AACA,SAAS,oBAAoB;AAM7B,eAAsB,gBACpB,SACA,OACsB;AAGtB,QAAM,MAAM,MAAM,aAAa,OAAO;AACtC,SAAO,OAAO,iBAAiB,SAAS,KAAK;AAC/C;AAOA,SAAS,iBACP,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":[]}
1
+ {"version":3,"sources":["../../src/server/data.ts"],"sourcesContent":["import type { OverrideRow } from \"../core/types\";\nimport { generateScaffold } from \"../core/scaffold\";\nimport { loadOverride } from \"./loader\";\n\n/**\n * Server-side fetch for a single field. Runs during SSR/ISR so anonymous\n * visitors get the real content in the initial HTML.\n */\nexport async function fetchOrScaffold(\n pathKey: string,\n props: Record<string, unknown>,\n): Promise<OverrideRow> {\n // Adapters normalize storage quirks (stringified JSON, column naming), so\n // anything returned here is already a domain-shaped row.\n const row = await loadOverride(pathKey);\n return row ?? generateScaffold(pathKey, props);\n}\n"],"mappings":"AACA,SAAS,wBAAwB;AACjC,SAAS,oBAAoB;AAM7B,eAAsB,gBACpB,SACA,OACsB;AAGtB,QAAM,MAAM,MAAM,aAAa,OAAO;AACtC,SAAO,OAAO,iBAAiB,SAAS,KAAK;AAC/C;","names":[]}
@@ -1,6 +1,13 @@
1
- import { OverrideRow } from '../core/types.js';
1
+ import { OverrideRow, GRContent } from '../core/types.js';
2
2
  import 'react';
3
3
 
4
4
  declare function loadOverride(pathKey: string): Promise<OverrideRow | null>;
5
+ /**
6
+ * The preload in the shape handed to Client Components, or undefined when
7
+ * preloading is off or unavailable (client fields then render their JSX
8
+ * fallback). Also where the two dev-mode nudges live, since this is the one
9
+ * place that knows the size of what is about to be serialized into the page.
10
+ */
11
+ declare function loadPreloadedContent(): Promise<GRContent | undefined>;
5
12
 
6
- export { loadOverride };
13
+ export { loadOverride, loadPreloadedContent };
@@ -1,5 +1,5 @@
1
1
  import { cache } from "react";
2
- import { getDB, resolveSiteId } from "../core/config";
2
+ import { getDB, getPrefetch, resolveSiteId } from "../core/config";
3
3
  import { rethrowIfBailout } from "../core/bailout";
4
4
  const getLoader = cache(() => {
5
5
  let pending = [];
@@ -26,17 +26,52 @@ const getLoader = cache(() => {
26
26
  for (const p of batch) p.reject(e);
27
27
  }
28
28
  };
29
- return {
30
- load(key) {
31
- return new Promise((resolve, reject) => {
32
- pending.push({ key, resolve, reject });
33
- if (!scheduled) {
34
- scheduled = true;
35
- queueMicrotask(flush);
36
- }
37
- });
29
+ const enqueue = (key) => new Promise((resolve, reject) => {
30
+ pending.push({ key, resolve, reject });
31
+ if (!scheduled) {
32
+ scheduled = true;
33
+ queueMicrotask(flush);
34
+ }
35
+ });
36
+ let preload;
37
+ const runPreload = async () => {
38
+ try {
39
+ const prefetch = getPrefetch();
40
+ const db = getDB();
41
+ if (!prefetch || !db.listOverrides) return null;
42
+ const siteId = await resolveSiteId();
43
+ const started = Date.now();
44
+ const { rows, truncated } = await db.listOverrides(siteId, prefetch);
45
+ if (process.env.GUARDRAIL_DEBUG) {
46
+ console.log(
47
+ `[guardrail] preload: ${rows.length} row(s) in 1 query (${Date.now() - started}ms) site=${siteId}${prefetch.prefix ? ` prefix=${prefetch.prefix.join("|")}` : ""}${truncated ? " TRUNCATED" : ""}`
48
+ );
49
+ }
50
+ return {
51
+ byKey: new Map(rows.map((r) => [r.pathKey, r])),
52
+ // Scoping by prefix or hitting the ceiling both mean "not every row."
53
+ complete: !prefetch.prefix && !truncated,
54
+ truncated
55
+ };
56
+ } catch (e) {
57
+ rethrowIfBailout(e);
58
+ console.error("Guardrail: content preload failed", e);
59
+ return null;
38
60
  }
39
61
  };
62
+ const getPreload = () => preload ??= runPreload();
63
+ return {
64
+ async load(key) {
65
+ const pre = await getPreload();
66
+ if (pre) {
67
+ const hit = pre.byKey.get(key);
68
+ if (hit) return hit;
69
+ if (pre.complete) return null;
70
+ }
71
+ return enqueue(key);
72
+ },
73
+ getPreload
74
+ };
40
75
  });
41
76
  async function loadOverride(pathKey) {
42
77
  try {
@@ -47,7 +82,35 @@ async function loadOverride(pathKey) {
47
82
  return null;
48
83
  }
49
84
  }
85
+ let warnedTruncated = false;
86
+ let warnedSize = false;
87
+ const SIZE_WARN_CHARS = 1e5;
88
+ async function loadPreloadedContent() {
89
+ const pre = await getLoader().getPreload();
90
+ if (!pre) return void 0;
91
+ const values = {};
92
+ for (const [key, row] of pre.byKey) values[key] = row.value;
93
+ if (process.env.NODE_ENV !== "production") {
94
+ if (pre.truncated && !warnedTruncated) {
95
+ warnedTruncated = true;
96
+ console.warn(
97
+ `Guardrail: this site has more content rows than the preload ceiling (${pre.byKey.size}). <GRail>s inside Client Components beyond it fall back to their JSX. Scope it with withGuardrail(..., { prefetch: { prefix: ["home.", "header."] } }) or raise prefetch.limit.`
98
+ );
99
+ }
100
+ if (!warnedSize) {
101
+ const size = JSON.stringify(values).length;
102
+ if (size > SIZE_WARN_CHARS) {
103
+ warnedSize = true;
104
+ console.warn(
105
+ `Guardrail: preloaded content for Client Components is ~${Math.round(size / 1e3)}KB and ships in every page. Consider scoping it with prefetch.prefix.`
106
+ );
107
+ }
108
+ }
109
+ }
110
+ return { values, complete: pre.complete };
111
+ }
50
112
  export {
51
- loadOverride
113
+ loadOverride,
114
+ loadPreloadedContent
52
115
  };
53
116
  //# sourceMappingURL=loader.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/loader.ts"],"sourcesContent":["import { cache } from \"react\";\nimport { getDB, resolveSiteId } from \"../core/config\";\nimport { rethrowIfBailout } from \"../core/bailout\";\nimport type { OverrideRow } from \"../core/types\";\n\ninterface Pending {\n key: string;\n resolve: (row: OverrideRow | null) => void;\n reject: (error: unknown) => void;\n}\n\n/**\n * Request-scoped batching loader.\n *\n * Every <GRail> resolves its own field, so a naive implementation issues one\n * query per field. This collects the keys requested within a microtask and\n * flushes them as a single query. React renders siblings concurrently, so a\n * page's fields land in the same batch; a field nested behind another await\n * simply falls into a later batch rather than degrading to one query each.\n *\n * cache() scopes the queue to a single request so keys never leak between\n * concurrent renders.\n */\nconst getLoader = cache(() => {\n let pending: Pending[] = [];\n let scheduled = false;\n\n const flush = async () => {\n const batch = pending;\n pending = [];\n scheduled = false;\n if (batch.length === 0) return;\n\n // One lookup per distinct key, even if several fields share it.\n const keys = [...new Set(batch.map((p) => p.key))];\n\n try {\n const db = getDB();\n const siteId = await resolveSiteId();\n const started = Date.now();\n const rows = await db.getOverrides(keys, siteId);\n\n if (process.env.GUARDRAIL_DEBUG) {\n console.log(\n `[guardrail] batch: ${keys.length} key(s) -> ${rows.length} row(s) ` +\n `in 1 query (${Date.now() - started}ms) ` +\n `site=${siteId} keys=${keys.join(\", \")}`,\n );\n }\n const byKey = new Map(rows.map((r) => [r.pathKey, r]));\n for (const p of batch) p.resolve(byKey.get(p.key) ?? null);\n } catch (e) {\n for (const p of batch) p.reject(e);\n }\n };\n\n return {\n load(key: string): Promise<OverrideRow | null> {\n return new Promise<OverrideRow | null>((resolve, reject) => {\n pending.push({ key, resolve, reject });\n if (!scheduled) {\n scheduled = true;\n queueMicrotask(flush);\n }\n });\n },\n };\n});\n\nexport async function loadOverride(\n pathKey: string,\n): Promise<OverrideRow | null> {\n try {\n return await getLoader().load(pathKey);\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: failed to load override\", e);\n return null;\n }\n}\n"],"mappings":"AAAA,SAAS,aAAa;AACtB,SAAS,OAAO,qBAAqB;AACrC,SAAS,wBAAwB;AAqBjC,MAAM,YAAY,MAAM,MAAM;AAC5B,MAAI,UAAqB,CAAC;AAC1B,MAAI,YAAY;AAEhB,QAAM,QAAQ,YAAY;AACxB,UAAM,QAAQ;AACd,cAAU,CAAC;AACX,gBAAY;AACZ,QAAI,MAAM,WAAW,EAAG;AAGxB,UAAM,OAAO,CAAC,GAAG,IAAI,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAEjD,QAAI;AACF,YAAM,KAAK,MAAM;AACjB,YAAM,SAAS,MAAM,cAAc;AACnC,YAAM,UAAU,KAAK,IAAI;AACzB,YAAM,OAAO,MAAM,GAAG,aAAa,MAAM,MAAM;AAE/C,UAAI,QAAQ,IAAI,iBAAiB;AAC/B,gBAAQ;AAAA,UACN,sBAAsB,KAAK,MAAM,cAAc,KAAK,MAAM,uBACzC,KAAK,IAAI,IAAI,OAAO,YAC3B,MAAM,SAAS,KAAK,KAAK,IAAI,CAAC;AAAA,QAC1C;AAAA,MACF;AACA,YAAM,QAAQ,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;AACrD,iBAAW,KAAK,MAAO,GAAE,QAAQ,MAAM,IAAI,EAAE,GAAG,KAAK,IAAI;AAAA,IAC3D,SAAS,GAAG;AACV,iBAAW,KAAK,MAAO,GAAE,OAAO,CAAC;AAAA,IACnC;AAAA,EACF;AAEA,SAAO;AAAA,IACL,KAAK,KAA0C;AAC7C,aAAO,IAAI,QAA4B,CAAC,SAAS,WAAW;AAC1D,gBAAQ,KAAK,EAAE,KAAK,SAAS,OAAO,CAAC;AACrC,YAAI,CAAC,WAAW;AACd,sBAAY;AACZ,yBAAe,KAAK;AAAA,QACtB;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF,CAAC;AAED,eAAsB,aACpB,SAC6B;AAC7B,MAAI;AACF,WAAO,MAAM,UAAU,EAAE,KAAK,OAAO;AAAA,EACvC,SAAS,GAAG;AACV,qBAAiB,CAAC;AAClB,YAAQ,MAAM,sCAAsC,CAAC;AACrD,WAAO;AAAA,EACT;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/server/loader.ts"],"sourcesContent":["import { cache } from \"react\";\nimport { getDB, getPrefetch, resolveSiteId } from \"../core/config\";\nimport { rethrowIfBailout } from \"../core/bailout\";\nimport type { GRContent, OverrideRow } from \"../core/types\";\n\ninterface Pending {\n key: string;\n resolve: (row: OverrideRow | null) => void;\n reject: (error: unknown) => void;\n}\n\ninterface Preloaded {\n byKey: Map<string, OverrideRow>;\n /** Every row the site has — an absent key is known to be unedited. */\n complete: boolean;\n truncated: boolean;\n}\n\n/**\n * Request-scoped content loader.\n *\n * Two jobs, one source of truth:\n *\n * 1. Batching. Every <GRail> resolves its own field, so a naive implementation\n * issues one query per field. Keys requested within a microtask are flushed\n * as a single query. React renders siblings concurrently, so a page's fields\n * land in the same batch; a field nested behind another await simply falls\n * into a later batch rather than degrading to one query each.\n *\n * 2. Preloading. <GRail>s inside Client Components cannot await anything, so\n * their content has to be handed to them up front. That preload lives *here*,\n * as one lazily-created promise per request, rather than as a separate fetch\n * in the layout — Next renders layouts and pages in parallel, so a page's\n * server fields could otherwise ask for content before the layout had\n * loaded any. Server fields read from the same preload: when it is complete,\n * every key resolves from it and no per-field query runs at all.\n *\n * cache() scopes all of it to a single request so nothing leaks between\n * concurrent renders.\n */\nconst getLoader = cache(() => {\n let pending: Pending[] = [];\n let scheduled = false;\n\n const flush = async () => {\n const batch = pending;\n pending = [];\n scheduled = false;\n if (batch.length === 0) return;\n\n // One lookup per distinct key, even if several fields share it.\n const keys = [...new Set(batch.map((p) => p.key))];\n\n try {\n const db = getDB();\n const siteId = await resolveSiteId();\n const started = Date.now();\n const rows = await db.getOverrides(keys, siteId);\n\n if (process.env.GUARDRAIL_DEBUG) {\n console.log(\n `[guardrail] batch: ${keys.length} key(s) -> ${rows.length} row(s) ` +\n `in 1 query (${Date.now() - started}ms) ` +\n `site=${siteId} keys=${keys.join(\", \")}`,\n );\n }\n const byKey = new Map(rows.map((r) => [r.pathKey, r]));\n for (const p of batch) p.resolve(byKey.get(p.key) ?? null);\n } catch (e) {\n for (const p of batch) p.reject(e);\n }\n };\n\n const enqueue = (key: string) =>\n new Promise<OverrideRow | null>((resolve, reject) => {\n pending.push({ key, resolve, reject });\n if (!scheduled) {\n scheduled = true;\n queueMicrotask(flush);\n }\n });\n\n let preload: Promise<Preloaded | null> | undefined;\n\n const runPreload = async (): Promise<Preloaded | null> => {\n try {\n const prefetch = getPrefetch();\n const db = getDB();\n // Off, or an adapter that can't list: behave exactly as before.\n if (!prefetch || !db.listOverrides) return null;\n\n const siteId = await resolveSiteId();\n const started = Date.now();\n const { rows, truncated } = await db.listOverrides(siteId, prefetch);\n\n if (process.env.GUARDRAIL_DEBUG) {\n console.log(\n `[guardrail] preload: ${rows.length} row(s) in 1 query ` +\n `(${Date.now() - started}ms) site=${siteId}` +\n `${prefetch.prefix ? ` prefix=${prefetch.prefix.join(\"|\")}` : \"\"}` +\n `${truncated ? \" TRUNCATED\" : \"\"}`,\n );\n }\n return {\n byKey: new Map(rows.map((r) => [r.pathKey, r])),\n // Scoping by prefix or hitting the ceiling both mean \"not every row.\"\n complete: !prefetch.prefix && !truncated,\n truncated,\n };\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: content preload failed\", e);\n return null;\n }\n };\n\n const getPreload = () => (preload ??= runPreload());\n\n return {\n async load(key: string): Promise<OverrideRow | null> {\n const pre = await getPreload();\n if (pre) {\n const hit = pre.byKey.get(key);\n if (hit) return hit;\n if (pre.complete) return null;\n }\n return enqueue(key);\n },\n getPreload,\n };\n});\n\nexport async function loadOverride(\n pathKey: string,\n): Promise<OverrideRow | null> {\n try {\n return await getLoader().load(pathKey);\n } catch (e) {\n rethrowIfBailout(e);\n console.error(\"Guardrail: failed to load override\", e);\n return null;\n }\n}\n\nlet warnedTruncated = false;\nlet warnedSize = false;\nconst SIZE_WARN_CHARS = 100_000;\n\n/**\n * The preload in the shape handed to Client Components, or undefined when\n * preloading is off or unavailable (client fields then render their JSX\n * fallback). Also where the two dev-mode nudges live, since this is the one\n * place that knows the size of what is about to be serialized into the page.\n */\nexport async function loadPreloadedContent(): Promise<GRContent | undefined> {\n const pre = await getLoader().getPreload();\n if (!pre) return undefined;\n\n const values: GRContent[\"values\"] = {};\n for (const [key, row] of pre.byKey) values[key] = row.value;\n\n if (process.env.NODE_ENV !== \"production\") {\n if (pre.truncated && !warnedTruncated) {\n warnedTruncated = true;\n console.warn(\n `Guardrail: this site has more content rows than the preload ceiling ` +\n `(${pre.byKey.size}). <GRail>s inside Client Components beyond it fall ` +\n `back to their JSX. Scope it with withGuardrail(..., { prefetch: { prefix: [\"home.\", \"header.\"] } }) ` +\n `or raise prefetch.limit.`,\n );\n }\n if (!warnedSize) {\n const size = JSON.stringify(values).length;\n if (size > SIZE_WARN_CHARS) {\n warnedSize = true;\n console.warn(\n `Guardrail: preloaded content for Client Components is ~${Math.round(size / 1000)}KB ` +\n `and ships in every page. Consider scoping it with prefetch.prefix.`,\n );\n }\n }\n }\n\n return { values, complete: pre.complete };\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import React from 'react';
2
+ import React__default from 'react';
3
3
  import { GRConfig } from '../core/adapter-types.js';
4
4
  import '../core/types.js';
5
5
 
@@ -7,6 +7,6 @@ import '../core/types.js';
7
7
  * Wraps a root layout so every route carries Guardrail's edit state and admin
8
8
  * chrome. Stays a Server Component: the provider below is the client boundary.
9
9
  */
10
- declare function withGuardrail<P extends object>(LayoutComponent: React.ComponentType<P>, config: GRConfig): (props: P) => Promise<react_jsx_runtime.JSX.Element>;
10
+ declare function withGuardrail<P extends object>(LayoutComponent: React__default.ComponentType<P>, config: GRConfig): (props: P) => Promise<react_jsx_runtime.JSX.Element>;
11
11
 
12
12
  export { withGuardrail };
@@ -8,6 +8,7 @@ import {
8
8
  resolveSiteId
9
9
  } from "../core/config";
10
10
  import { getCachedViewer } from "./session";
11
+ import { loadPreloadedContent } from "./loader";
11
12
  import { GR_COOKIES } from "../core/defs";
12
13
  import { GRailProvider } from "../react/Provider";
13
14
  import { GRailAdmin } from "../react/Admin";
@@ -16,7 +17,10 @@ function withGuardrail(LayoutComponent, config) {
16
17
  return async function GuardrailLayout(props) {
17
18
  configureGuardrail(config);
18
19
  const cookieStore = await cookies();
19
- const viewer = await getCachedViewer();
20
+ const [viewer, content] = await Promise.all([
21
+ getCachedViewer(),
22
+ loadPreloadedContent()
23
+ ]);
20
24
  const auth = getAuth();
21
25
  const state = {
22
26
  storage: getStorage() !== void 0,
@@ -30,7 +34,7 @@ function withGuardrail(LayoutComponent, config) {
30
34
  role: viewer?.role ?? null,
31
35
  ...viewer?.email ? { email: viewer.email } : {}
32
36
  };
33
- return /* @__PURE__ */ jsxs(GRailProvider, { state, children: [
37
+ return /* @__PURE__ */ jsxs(GRailProvider, { state, content, children: [
34
38
  /* @__PURE__ */ jsx(LayoutComponent, { ...props }),
35
39
  /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(GRailAdmin, { toolbarExtra: config.toolbarExtra }) })
36
40
  ] });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/wrapper.tsx"],"sourcesContent":["import React, { Suspense } from \"react\";\nimport { cookies } from \"next/headers\";\nimport {\n configureGuardrail,\n getAuth,\n getStorage,\n resolveSiteId,\n} from \"../core/config\";\nimport { getCachedViewer } from \"./session\";\nimport type { GRConfig } from \"../core/adapter-types\";\nimport { GR_COOKIES } from \"../core/defs\";\nimport { GRailProvider } from \"../react/Provider\";\nimport { GRailAdmin } from \"../react/Admin\";\nimport type { GRState } from \"../core/types\";\n\n/**\n * Wraps a root layout so every route carries Guardrail's edit state and admin\n * chrome. Stays a Server Component: the provider below is the client boundary.\n */\nexport function withGuardrail<P extends object>(\n LayoutComponent: React.ComponentType<P>,\n config: GRConfig,\n) {\n // Applied at module evaluation so the config is present as soon as anything\n // imports the layout, and again per render because Next.js does not\n // guarantee one shared module instance across its server contexts.\n configureGuardrail(config);\n\n return async function GuardrailLayout(props: P) {\n configureGuardrail(config);\n\n const cookieStore = await cookies();\n\n // Verified against the auth provider, not inferred from a cookie.\n const viewer = await getCachedViewer();\n\n const auth = getAuth();\n\n const state: GRState = {\n storage: getStorage() !== undefined,\n auth: {\n password: typeof auth.authenticate === \"function\",\n ...(auth.signInUrl ? { signInUrl: auth.signInUrl } : {}),\n },\n isAuth: viewer !== null,\n isEditMode: cookieStore.get(GR_COOKIES.EDIT_MODE)?.value === \"true\",\n siteId: await resolveSiteId(),\n role: viewer?.role ?? null,\n ...(viewer?.email ? { email: viewer.email } : {}),\n };\n\n return (\n <GRailProvider state={state}>\n <LayoutComponent {...props} />\n <Suspense fallback={null}>\n <GRailAdmin toolbarExtra={config.toolbarExtra} />\n </Suspense>\n </GRailProvider>\n );\n };\n}\n"],"mappings":"AAoDM,SACE,KADF;AApDN,OAAO,SAAS,gBAAgB;AAChC,SAAS,eAAe;AACxB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,uBAAuB;AAEhC,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAOpB,SAAS,cACd,iBACA,QACA;AAIA,qBAAmB,MAAM;AAEzB,SAAO,eAAe,gBAAgB,OAAU;AAC9C,uBAAmB,MAAM;AAEzB,UAAM,cAAc,MAAM,QAAQ;AAGlC,UAAM,SAAS,MAAM,gBAAgB;AAErC,UAAM,OAAO,QAAQ;AAErB,UAAM,QAAiB;AAAA,MACrB,SAAS,WAAW,MAAM;AAAA,MAC1B,MAAM;AAAA,QACJ,UAAU,OAAO,KAAK,iBAAiB;AAAA,QACvC,GAAI,KAAK,YAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;AAAA,MACxD;AAAA,MACA,QAAQ,WAAW;AAAA,MACnB,YAAY,YAAY,IAAI,WAAW,SAAS,GAAG,UAAU;AAAA,MAC7D,QAAQ,MAAM,cAAc;AAAA,MAC5B,MAAM,QAAQ,QAAQ;AAAA,MACtB,GAAI,QAAQ,QAAQ,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,IACjD;AAEA,WACE,qBAAC,iBAAc,OACb;AAAA,0BAAC,mBAAiB,GAAG,OAAO;AAAA,MAC5B,oBAAC,YAAS,UAAU,MAClB,8BAAC,cAAW,cAAc,OAAO,cAAc,GACjD;AAAA,OACF;AAAA,EAEJ;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/server/wrapper.tsx"],"sourcesContent":["import React, { Suspense } from \"react\";\nimport { cookies } from \"next/headers\";\nimport {\n configureGuardrail,\n getAuth,\n getStorage,\n resolveSiteId,\n} from \"../core/config\";\nimport { getCachedViewer } from \"./session\";\nimport { loadPreloadedContent } from \"./loader\";\nimport type { GRConfig } from \"../core/adapter-types\";\nimport { GR_COOKIES } from \"../core/defs\";\nimport { GRailProvider } from \"../react/Provider\";\nimport { GRailAdmin } from \"../react/Admin\";\nimport type { GRState } from \"../core/types\";\n\n/**\n * Wraps a root layout so every route carries Guardrail's edit state and admin\n * chrome. Stays a Server Component: the provider below is the client boundary.\n */\nexport function withGuardrail<P extends object>(\n LayoutComponent: React.ComponentType<P>,\n config: GRConfig,\n) {\n // Applied at module evaluation so the config is present as soon as anything\n // imports the layout, and again per render because Next.js does not\n // guarantee one shared module instance across its server contexts.\n configureGuardrail(config);\n\n return async function GuardrailLayout(props: P) {\n configureGuardrail(config);\n\n const cookieStore = await cookies();\n\n // Verified against the auth provider, not inferred from a cookie. Loaded\n // alongside the content preload: independent round trips, and the preload\n // is shared with the page's own fields, so it costs them nothing extra.\n const [viewer, content] = await Promise.all([\n getCachedViewer(),\n loadPreloadedContent(),\n ]);\n\n const auth = getAuth();\n\n const state: GRState = {\n storage: getStorage() !== undefined,\n auth: {\n password: typeof auth.authenticate === \"function\",\n ...(auth.signInUrl ? { signInUrl: auth.signInUrl } : {}),\n },\n isAuth: viewer !== null,\n isEditMode: cookieStore.get(GR_COOKIES.EDIT_MODE)?.value === \"true\",\n siteId: await resolveSiteId(),\n role: viewer?.role ?? null,\n ...(viewer?.email ? { email: viewer.email } : {}),\n };\n\n return (\n <GRailProvider state={state} content={content}>\n <LayoutComponent {...props} />\n <Suspense fallback={null}>\n <GRailAdmin toolbarExtra={config.toolbarExtra} />\n </Suspense>\n </GRailProvider>\n );\n };\n}\n"],"mappings":"AA0DM,SACE,KADF;AA1DN,OAAO,SAAS,gBAAgB;AAChC,SAAS,eAAe;AACxB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,uBAAuB;AAChC,SAAS,4BAA4B;AAErC,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAOpB,SAAS,cACd,iBACA,QACA;AAIA,qBAAmB,MAAM;AAEzB,SAAO,eAAe,gBAAgB,OAAU;AAC9C,uBAAmB,MAAM;AAEzB,UAAM,cAAc,MAAM,QAAQ;AAKlC,UAAM,CAAC,QAAQ,OAAO,IAAI,MAAM,QAAQ,IAAI;AAAA,MAC1C,gBAAgB;AAAA,MAChB,qBAAqB;AAAA,IACvB,CAAC;AAED,UAAM,OAAO,QAAQ;AAErB,UAAM,QAAiB;AAAA,MACrB,SAAS,WAAW,MAAM;AAAA,MAC1B,MAAM;AAAA,QACJ,UAAU,OAAO,KAAK,iBAAiB;AAAA,QACvC,GAAI,KAAK,YAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;AAAA,MACxD;AAAA,MACA,QAAQ,WAAW;AAAA,MACnB,YAAY,YAAY,IAAI,WAAW,SAAS,GAAG,UAAU;AAAA,MAC7D,QAAQ,MAAM,cAAc;AAAA,MAC5B,MAAM,QAAQ,QAAQ;AAAA,MACtB,GAAI,QAAQ,QAAQ,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,IACjD;AAEA,WACE,qBAAC,iBAAc,OAAc,SAC3B;AAAA,0BAAC,mBAAiB,GAAG,OAAO;AAAA,MAC5B,oBAAC,YAAS,UAAU,MAClB,8BAAC,cAAW,cAAc,OAAO,cAAc,GACjD;AAAA,OACF;AAAA,EAEJ;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guard-rail/core",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Lightweight inline content editing for developer-owned Next.js sites.",
5
5
  "license": "ISC",
6
6
  "type": "module",
@@ -8,9 +8,18 @@
8
8
  "types": "./dist/index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "react-server": "./dist/index.js",
13
+ "default": "./dist/index.client.js"
14
+ },
15
+ "./server": {
11
16
  "types": "./dist/index.d.ts",
12
17
  "default": "./dist/index.js"
13
18
  },
19
+ "./client": {
20
+ "types": "./dist/index.client.d.ts",
21
+ "default": "./dist/index.client.js"
22
+ },
14
23
  "./adapters/supabase": {
15
24
  "types": "./dist/adapters/supabase/index.d.ts",
16
25
  "default": "./dist/adapters/supabase/index.js"