@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.
- package/README.md +67 -3
- package/dist/adapters/supabase/db.js +11 -0
- package/dist/adapters/supabase/db.js.map +1 -1
- package/dist/core/adapter-types.d.ts +35 -1
- package/dist/core/config.d.ts +7 -1
- package/dist/core/config.js +11 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/scaffold.d.ts +11 -0
- package/dist/core/scaffold.js +19 -0
- package/dist/core/scaffold.js.map +1 -0
- package/dist/core/types.d.ts +14 -1
- package/dist/index.client.d.ts +18 -0
- package/dist/index.client.js +66 -0
- package/dist/index.client.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react/Admin.d.ts +2 -2
- package/dist/react/Admin.js +1 -1
- package/dist/react/Admin.js.map +1 -1
- package/dist/react/GRail/editor.d.ts +24 -0
- package/dist/react/GRail/editor.js +9 -0
- package/dist/react/GRail/editor.js.map +1 -0
- package/dist/react/GRail/index.js +2 -2
- package/dist/react/GRail/index.js.map +1 -1
- package/dist/react/GRail/render.d.ts +3 -3
- package/dist/react/GRail/universal.d.ts +22 -0
- package/dist/react/GRail/universal.js +29 -0
- package/dist/react/GRail/universal.js.map +1 -0
- package/dist/react/Provider.d.ts +9 -5
- package/dist/react/Provider.js +10 -4
- package/dist/react/Provider.js.map +1 -1
- package/dist/react/Repeater/client.d.ts +3 -3
- package/dist/react/Repeater/index.d.ts +3 -3
- package/dist/react/Team.js +1 -1
- package/dist/react/Team.js.map +1 -1
- package/dist/server/data.js +1 -15
- package/dist/server/data.js.map +1 -1
- package/dist/server/loader.d.ts +9 -2
- package/dist/server/loader.js +74 -11
- package/dist/server/loader.js.map +1 -1
- package/dist/server/wrapper.d.ts +2 -2
- package/dist/server/wrapper.js +6 -2
- package/dist/server/wrapper.js.map +1 -1
- package/package.json +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
|
|
391
|
-
|
|
392
|
-
|
|
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 };
|
package/dist/core/config.d.ts
CHANGED
|
@@ -21,7 +21,13 @@ declare function getMembers(): GRMembershipAdapter;
|
|
|
21
21
|
declare function getStorage(): GRStorageAdapter | undefined;
|
|
22
22
|
/** Throws with a clear message if no page on this site uses <GRepeater>. */
|
|
23
23
|
declare function getRepeaters(): GRRepeaterAdapter;
|
|
24
|
+
declare const DEFAULT_PREFETCH_LIMIT = 500;
|
|
25
|
+
/** Normalized preload settings, or null when preloading is off. */
|
|
26
|
+
declare function getPrefetch(): {
|
|
27
|
+
prefix?: string[];
|
|
28
|
+
limit: number;
|
|
29
|
+
} | null;
|
|
24
30
|
declare function getInviteHandler(): GRConfig["onInvite"];
|
|
25
31
|
declare function resolveSiteId(): Promise<string>;
|
|
26
32
|
|
|
27
|
-
export { configureGuardrail, getAuth, getDB, getInviteHandler, getMembers, getRepeaters, getStorage, resolveSiteId };
|
|
33
|
+
export { DEFAULT_PREFETCH_LIMIT, configureGuardrail, getAuth, getDB, getInviteHandler, getMembers, getPrefetch, getRepeaters, getStorage, resolveSiteId };
|
package/dist/core/config.js
CHANGED
|
@@ -26,6 +26,15 @@ function getStorage() {
|
|
|
26
26
|
function getRepeaters() {
|
|
27
27
|
return required("repeaters");
|
|
28
28
|
}
|
|
29
|
+
const DEFAULT_PREFETCH_LIMIT = 500;
|
|
30
|
+
function getPrefetch() {
|
|
31
|
+
const p = _config?.prefetch;
|
|
32
|
+
if (p === false) return null;
|
|
33
|
+
return {
|
|
34
|
+
...p?.prefix?.length ? { prefix: p.prefix } : {},
|
|
35
|
+
limit: p?.limit ?? DEFAULT_PREFETCH_LIMIT
|
|
36
|
+
};
|
|
37
|
+
}
|
|
29
38
|
function getInviteHandler() {
|
|
30
39
|
return _config?.onInvite;
|
|
31
40
|
}
|
|
@@ -34,11 +43,13 @@ async function resolveSiteId() {
|
|
|
34
43
|
return typeof sid === "function" ? await sid() : sid || "default";
|
|
35
44
|
}
|
|
36
45
|
export {
|
|
46
|
+
DEFAULT_PREFETCH_LIMIT,
|
|
37
47
|
configureGuardrail,
|
|
38
48
|
getAuth,
|
|
39
49
|
getDB,
|
|
40
50
|
getInviteHandler,
|
|
41
51
|
getMembers,
|
|
52
|
+
getPrefetch,
|
|
42
53
|
getRepeaters,
|
|
43
54
|
getStorage,
|
|
44
55
|
resolveSiteId
|
package/dist/core/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/config.ts"],"sourcesContent":["import type {\n GRConfig,\n GRAuthAdapter,\n GRDBAdapter,\n GRMembershipAdapter,\n GRRepeaterAdapter,\n GRStorageAdapter,\n} from \"./adapter-types\";\n\nlet _config: GRConfig | null = null;\n\n/**\n * Registers the adapters Guardrail should use.\n *\n * Idempotent, and re-applied on every render by withGuardrail: Next.js does not\n * guarantee one shared module instance across the SSR graph, the server-action\n * graph, and the workers used for static generation, so config registered once\n * at startup is simply absent in some of them.\n *\n * Core has no knowledge of any particular backend — there is no default and no\n * fallback. An app names its adapters explicitly or Guardrail refuses to run.\n */\nexport function configureGuardrail(config: GRConfig): void {\n _config = config;\n}\n\nfunction required<K extends keyof GRConfig>(key: K): GRConfig[K] {\n const value = _config?.[key];\n if (!value) {\n throw new Error(\n `Guardrail: no ${String(key)} adapter configured. Pass one to ` +\n `withGuardrail(Layout, { db, auth, members }).`,\n );\n }\n return value;\n}\n\nexport function getDB(): GRDBAdapter {\n return required(\"db\") as GRDBAdapter;\n}\n\nexport function getAuth(): GRAuthAdapter {\n return required(\"auth\") as GRAuthAdapter;\n}\n\nexport function getMembers(): GRMembershipAdapter {\n return required(\"members\") as GRMembershipAdapter;\n}\n\n/** Optional — sites without editable images need no storage. */\nexport function getStorage(): GRStorageAdapter | undefined {\n return _config?.storage;\n}\n\n/** Throws with a clear message if no page on this site uses <GRepeater>. */\nexport function getRepeaters(): GRRepeaterAdapter {\n return required(\"repeaters\") as GRRepeaterAdapter;\n}\n\nexport function getInviteHandler(): GRConfig[\"onInvite\"] {\n return _config?.onInvite;\n}\n\nexport async function resolveSiteId(): Promise<string> {\n const sid = _config?.siteId ?? \"default\";\n return typeof sid === \"function\" ? await sid() : sid || \"default\";\n}\n"],"mappings":"AASA,IAAI,UAA2B;AAaxB,SAAS,mBAAmB,QAAwB;AACzD,YAAU;AACZ;AAEA,SAAS,SAAmC,KAAqB;AAC/D,QAAM,QAAQ,UAAU,GAAG;AAC3B,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR,iBAAiB,OAAO,GAAG,CAAC;AAAA,IAE9B;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,QAAqB;AACnC,SAAO,SAAS,IAAI;AACtB;AAEO,SAAS,UAAyB;AACvC,SAAO,SAAS,MAAM;AACxB;AAEO,SAAS,aAAkC;AAChD,SAAO,SAAS,SAAS;AAC3B;AAGO,SAAS,aAA2C;AACzD,SAAO,SAAS;AAClB;AAGO,SAAS,eAAkC;AAChD,SAAO,SAAS,WAAW;AAC7B;AAEO,SAAS,mBAAyC;AACvD,SAAO,SAAS;AAClB;AAEA,eAAsB,gBAAiC;AACrD,QAAM,MAAM,SAAS,UAAU;AAC/B,SAAO,OAAO,QAAQ,aAAa,MAAM,IAAI,IAAI,OAAO;AAC1D;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/config.ts"],"sourcesContent":["import type {\n GRConfig,\n GRAuthAdapter,\n GRDBAdapter,\n GRMembershipAdapter,\n GRRepeaterAdapter,\n GRStorageAdapter,\n} from \"./adapter-types\";\n\nlet _config: GRConfig | null = null;\n\n/**\n * Registers the adapters Guardrail should use.\n *\n * Idempotent, and re-applied on every render by withGuardrail: Next.js does not\n * guarantee one shared module instance across the SSR graph, the server-action\n * graph, and the workers used for static generation, so config registered once\n * at startup is simply absent in some of them.\n *\n * Core has no knowledge of any particular backend — there is no default and no\n * fallback. An app names its adapters explicitly or Guardrail refuses to run.\n */\nexport function configureGuardrail(config: GRConfig): void {\n _config = config;\n}\n\nfunction required<K extends keyof GRConfig>(key: K): GRConfig[K] {\n const value = _config?.[key];\n if (!value) {\n throw new Error(\n `Guardrail: no ${String(key)} adapter configured. Pass one to ` +\n `withGuardrail(Layout, { db, auth, members }).`,\n );\n }\n return value;\n}\n\nexport function getDB(): GRDBAdapter {\n return required(\"db\") as GRDBAdapter;\n}\n\nexport function getAuth(): GRAuthAdapter {\n return required(\"auth\") as GRAuthAdapter;\n}\n\nexport function getMembers(): GRMembershipAdapter {\n return required(\"members\") as GRMembershipAdapter;\n}\n\n/** Optional — sites without editable images need no storage. */\nexport function getStorage(): GRStorageAdapter | undefined {\n return _config?.storage;\n}\n\n/** Throws with a clear message if no page on this site uses <GRepeater>. */\nexport function getRepeaters(): GRRepeaterAdapter {\n return required(\"repeaters\") as GRRepeaterAdapter;\n}\n\nexport const DEFAULT_PREFETCH_LIMIT = 500;\n\n/** Normalized preload settings, or null when preloading is off. */\nexport function getPrefetch(): { prefix?: string[]; limit: number } | null {\n const p = _config?.prefetch;\n if (p === false) return null;\n return {\n ...(p?.prefix?.length ? { prefix: p.prefix } : {}),\n limit: p?.limit ?? DEFAULT_PREFETCH_LIMIT,\n };\n}\n\nexport function getInviteHandler(): GRConfig[\"onInvite\"] {\n return _config?.onInvite;\n}\n\nexport async function resolveSiteId(): Promise<string> {\n const sid = _config?.siteId ?? \"default\";\n return typeof sid === \"function\" ? await sid() : sid || \"default\";\n}\n"],"mappings":"AASA,IAAI,UAA2B;AAaxB,SAAS,mBAAmB,QAAwB;AACzD,YAAU;AACZ;AAEA,SAAS,SAAmC,KAAqB;AAC/D,QAAM,QAAQ,UAAU,GAAG;AAC3B,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR,iBAAiB,OAAO,GAAG,CAAC;AAAA,IAE9B;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,QAAqB;AACnC,SAAO,SAAS,IAAI;AACtB;AAEO,SAAS,UAAyB;AACvC,SAAO,SAAS,MAAM;AACxB;AAEO,SAAS,aAAkC;AAChD,SAAO,SAAS,SAAS;AAC3B;AAGO,SAAS,aAA2C;AACzD,SAAO,SAAS;AAClB;AAGO,SAAS,eAAkC;AAChD,SAAO,SAAS,WAAW;AAC7B;AAEO,MAAM,yBAAyB;AAG/B,SAAS,cAA2D;AACzE,QAAM,IAAI,SAAS;AACnB,MAAI,MAAM,MAAO,QAAO;AACxB,SAAO;AAAA,IACL,GAAI,GAAG,QAAQ,SAAS,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,IAChD,OAAO,GAAG,SAAS;AAAA,EACrB;AACF;AAEO,SAAS,mBAAyC;AACvD,SAAO,SAAS;AAClB;AAEA,eAAsB,gBAAiC;AACrD,QAAM,MAAM,SAAS,UAAU;AAC/B,SAAO,OAAO,QAAQ,aAAa,MAAM,IAAI,IAAI,OAAO;AAC1D;","names":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { OverrideRow } from './types.js';
|
|
2
|
+
import 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds a default row from the JSX the developer already wrote, so an
|
|
6
|
+
* un-edited field renders its hardcoded content. Deliberately does not write
|
|
7
|
+
* to the database — nothing is persisted until an editor saves.
|
|
8
|
+
*/
|
|
9
|
+
declare function generateScaffold(pathKey: string, props: Record<string, unknown>): OverrideRow;
|
|
10
|
+
|
|
11
|
+
export { generateScaffold };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function generateScaffold(pathKey, props) {
|
|
2
|
+
const { children, type, src, alt, href } = props;
|
|
3
|
+
const defaultText = typeof children === "string" || typeof children === "number" ? String(children) : "";
|
|
4
|
+
let value;
|
|
5
|
+
if (type === "image" || src) {
|
|
6
|
+
value = { type: "image", src: src ?? "", alt: alt ?? "" };
|
|
7
|
+
} else if (type === "link" || href) {
|
|
8
|
+
value = { type: "link", href: href ?? "", text: defaultText };
|
|
9
|
+
} else if (type === "button") {
|
|
10
|
+
value = { type: "button", text: defaultText };
|
|
11
|
+
} else {
|
|
12
|
+
value = { type: "text", text: defaultText };
|
|
13
|
+
}
|
|
14
|
+
return { id: `scaffold-${pathKey}`, pathKey, value };
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
generateScaffold
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=scaffold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/scaffold.ts"],"sourcesContent":["import type { OverrideRow, OverrideValue } from \"./types\";\n\n/**\n * Builds a default row from the JSX the developer already wrote, so an\n * un-edited field renders its hardcoded content. Deliberately does not write\n * to the database — nothing is persisted until an editor saves.\n */\nexport function generateScaffold(\n pathKey: string,\n props: Record<string, unknown>,\n): OverrideRow {\n const { children, type, src, alt, href } = props as {\n children?: unknown;\n type?: string;\n src?: string;\n alt?: string;\n href?: string;\n };\n\n const defaultText =\n typeof children === \"string\" || typeof children === \"number\"\n ? String(children)\n : \"\";\n\n let value: OverrideValue;\n\n if (type === \"image\" || src) {\n value = { type: \"image\", src: src ?? \"\", alt: alt ?? \"\" };\n } else if (type === \"link\" || href) {\n value = { type: \"link\", href: href ?? \"\", text: defaultText };\n } else if (type === \"button\") {\n value = { type: \"button\", text: defaultText };\n } else {\n value = { type: \"text\", text: defaultText };\n }\n\n return { id: `scaffold-${pathKey}`, pathKey, value };\n}\n"],"mappings":"AAOO,SAAS,iBACd,SACA,OACa;AACb,QAAM,EAAE,UAAU,MAAM,KAAK,KAAK,KAAK,IAAI;AAQ3C,QAAM,cACJ,OAAO,aAAa,YAAY,OAAO,aAAa,WAChD,OAAO,QAAQ,IACf;AAEN,MAAI;AAEJ,MAAI,SAAS,WAAW,KAAK;AAC3B,YAAQ,EAAE,MAAM,SAAS,KAAK,OAAO,IAAI,KAAK,OAAO,GAAG;AAAA,EAC1D,WAAW,SAAS,UAAU,MAAM;AAClC,YAAQ,EAAE,MAAM,QAAQ,MAAM,QAAQ,IAAI,MAAM,YAAY;AAAA,EAC9D,WAAW,SAAS,UAAU;AAC5B,YAAQ,EAAE,MAAM,UAAU,MAAM,YAAY;AAAA,EAC9C,OAAO;AACL,YAAQ,EAAE,MAAM,QAAQ,MAAM,YAAY;AAAA,EAC5C;AAEA,SAAO,EAAE,IAAI,YAAY,OAAO,IAAI,SAAS,MAAM;AACrD;","names":[]}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -33,6 +33,19 @@ interface OverrideRow {
|
|
|
33
33
|
pathKey: string;
|
|
34
34
|
value: OverrideValue;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Field values preloaded for <GRail>s rendered inside Client Components, which
|
|
38
|
+
* cannot await a query themselves. Lean on purpose — it is serialized into the
|
|
39
|
+
* page — so it carries values only, not rows.
|
|
40
|
+
*/
|
|
41
|
+
interface GRContent {
|
|
42
|
+
values: Record<string, OverrideValue>;
|
|
43
|
+
/**
|
|
44
|
+
* True when `values` is every row this site has. A key absent from a
|
|
45
|
+
* complete map is known to be unedited, with no further lookup needed.
|
|
46
|
+
*/
|
|
47
|
+
complete: boolean;
|
|
48
|
+
}
|
|
36
49
|
/**
|
|
37
50
|
* next/image props that have no intrinsic-element equivalent. A field rendered
|
|
38
51
|
* with type="image" goes through next/image, so these have to be accepted even
|
|
@@ -133,4 +146,4 @@ type ActionState = {
|
|
|
133
146
|
password?: string;
|
|
134
147
|
};
|
|
135
148
|
|
|
136
|
-
export type { ActionState, GRAsset, GRAuthCapabilities, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, NextImageExtras, OverrideRow, OverrideType, OverrideValue, PolymorphicProps };
|
|
149
|
+
export type { ActionState, GRAsset, GRAuthCapabilities, GRContent, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, NextImageExtras, OverrideRow, OverrideType, OverrideValue, PolymorphicProps };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { GRail } from './react/GRail/universal.js';
|
|
2
|
+
export { useGRail } from './react/Provider.js';
|
|
3
|
+
export { createAssetUpload, inviteMember, listAssets, listTeam, removeMember, updateMemberRole } from './server/actions.js';
|
|
4
|
+
export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from './server/repeaterActions.js';
|
|
5
|
+
export { GRepeaterProps, RepeaterItemProps } from './react/Repeater/index.js';
|
|
6
|
+
export { GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps } from './core/types.js';
|
|
7
|
+
export { AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
|
|
8
|
+
import 'react/jsx-runtime';
|
|
9
|
+
import 'react';
|
|
10
|
+
|
|
11
|
+
declare const withGuardrail: (..._args: unknown[]) => never;
|
|
12
|
+
declare const configureGuardrail: (..._args: unknown[]) => never;
|
|
13
|
+
declare const GRepeater: (..._args: unknown[]) => never;
|
|
14
|
+
declare const listRepeaterItems: (..._args: unknown[]) => never;
|
|
15
|
+
declare const getRepeaterItemBySlug: (..._args: unknown[]) => never;
|
|
16
|
+
declare const getFieldValue: (..._args: unknown[]) => never;
|
|
17
|
+
|
|
18
|
+
export { GRepeater, configureGuardrail, getFieldValue, getRepeaterItemBySlug, listRepeaterItems, withGuardrail };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { GRail } from "./react/GRail/universal";
|
|
2
|
+
import { useGRail } from "./react/Provider";
|
|
3
|
+
import {
|
|
4
|
+
listTeam,
|
|
5
|
+
inviteMember,
|
|
6
|
+
updateMemberRole,
|
|
7
|
+
removeMember,
|
|
8
|
+
createAssetUpload,
|
|
9
|
+
listAssets
|
|
10
|
+
} from "./server/actions";
|
|
11
|
+
import {
|
|
12
|
+
addRepeaterItem,
|
|
13
|
+
removeRepeaterItem,
|
|
14
|
+
reorderRepeaterItems
|
|
15
|
+
} from "./server/repeaterActions";
|
|
16
|
+
function serverOnly(name, hint) {
|
|
17
|
+
return (..._args) => {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`Guardrail: ${name} only works in a Server Component. ${hint} (If this isn't a Client Component, your tooling may not set the "react-server" condition \u2014 import from "@guard-rail/core/server" instead.)`
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const withGuardrail = serverOnly(
|
|
24
|
+
"withGuardrail()",
|
|
25
|
+
"Call it in your root layout, which is a Server Component."
|
|
26
|
+
);
|
|
27
|
+
const configureGuardrail = serverOnly(
|
|
28
|
+
"configureGuardrail()",
|
|
29
|
+
"Configuration is applied by withGuardrail() in your root layout."
|
|
30
|
+
);
|
|
31
|
+
const GRepeater = serverOnly(
|
|
32
|
+
"<GRepeater>",
|
|
33
|
+
"Render it in a Server Component and pass it into this one as children or a prop."
|
|
34
|
+
);
|
|
35
|
+
const listRepeaterItems = serverOnly(
|
|
36
|
+
"listRepeaterItems()",
|
|
37
|
+
"Read repeater items in a Server Component and pass them down."
|
|
38
|
+
);
|
|
39
|
+
const getRepeaterItemBySlug = serverOnly(
|
|
40
|
+
"getRepeaterItemBySlug()",
|
|
41
|
+
"Resolve the item in a Server Component (your [slug]/page.tsx) and pass it down."
|
|
42
|
+
);
|
|
43
|
+
const getFieldValue = serverOnly(
|
|
44
|
+
"getFieldValue()",
|
|
45
|
+
"Read the value in a Server Component and pass it down as a prop."
|
|
46
|
+
);
|
|
47
|
+
export {
|
|
48
|
+
GRail,
|
|
49
|
+
GRepeater,
|
|
50
|
+
addRepeaterItem,
|
|
51
|
+
configureGuardrail,
|
|
52
|
+
createAssetUpload,
|
|
53
|
+
getFieldValue,
|
|
54
|
+
getRepeaterItemBySlug,
|
|
55
|
+
inviteMember,
|
|
56
|
+
listAssets,
|
|
57
|
+
listRepeaterItems,
|
|
58
|
+
listTeam,
|
|
59
|
+
removeMember,
|
|
60
|
+
removeRepeaterItem,
|
|
61
|
+
reorderRepeaterItems,
|
|
62
|
+
updateMemberRole,
|
|
63
|
+
useGRail,
|
|
64
|
+
withGuardrail
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=index.client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.client.ts"],"sourcesContent":["// Client-graph entry. `import ... from \"@guard-rail/core\"` resolves here from a\n// Client Component (see \"exports\" in package.json) and to ./index from a\n// Server Component, so an app uses one import path everywhere.\n//\n// Nothing server-only may be imported here — this is bundled for the browser.\n// Server Actions are fine: a \"use server\" module is replaced by references in\n// the client bundle, which is what lets an app build its own team/repeater UI\n// in a Client Component.\n\nexport { GRail } from \"./react/GRail/universal\";\nexport { useGRail } from \"./react/Provider\";\n\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n} from \"./server/actions\";\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\n\n/**\n * Stand-ins for the server-only exports, so importing one from a Client\n * Component fails with an explanation instead of an undefined-is-not-a-function\n * deep in a render. Also what a tool that doesn't set the \"react-server\"\n * condition (a test runner, a plain Node script) gets from the package root —\n * `@guard-rail/core/server` is the explicit path to the real ones there.\n */\nfunction serverOnly(name: string, hint: string) {\n return (..._args: unknown[]): never => {\n throw new Error(\n `Guardrail: ${name} only works in a Server Component. ${hint} ` +\n `(If this isn't a Client Component, your tooling may not set the ` +\n `\"react-server\" condition — import from \"@guard-rail/core/server\" instead.)`,\n );\n };\n}\n\nexport const withGuardrail = serverOnly(\n \"withGuardrail()\",\n \"Call it in your root layout, which is a Server Component.\",\n);\nexport const configureGuardrail = serverOnly(\n \"configureGuardrail()\",\n \"Configuration is applied by withGuardrail() in your root layout.\",\n);\nexport const GRepeater = serverOnly(\n \"<GRepeater>\",\n \"Render it in a Server Component and pass it into this one as children or a prop.\",\n);\nexport const listRepeaterItems = serverOnly(\n \"listRepeaterItems()\",\n \"Read repeater items in a Server Component and pass them down.\",\n);\nexport const getRepeaterItemBySlug = serverOnly(\n \"getRepeaterItemBySlug()\",\n \"Resolve the item in a Server Component (your [slug]/page.tsx) and pass it down.\",\n);\nexport const getFieldValue = serverOnly(\n \"getFieldValue()\",\n \"Read the value in a Server Component and pass it down as a prop.\",\n);\n\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n} from \"./core/adapter-types\";\n"],"mappings":"AASA,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAEzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,WAAW,MAAc,MAAc;AAC9C,SAAO,IAAI,UAA4B;AACrC,UAAM,IAAI;AAAA,MACR,cAAc,IAAI,sCAAsC,IAAI;AAAA,IAG9D;AAAA,EACF;AACF;AAEO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AACF;AACO,MAAM,YAAY;AAAA,EACvB;AAAA,EACA;AACF;AACO,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AACF;AACO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AACF;AACO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,6 @@ export { getFieldValue } from './server/fieldData.js';
|
|
|
8
8
|
export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from './server/repeaterActions.js';
|
|
9
9
|
export { useGRail } from './react/Provider.js';
|
|
10
10
|
export { GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps } from './core/types.js';
|
|
11
|
-
export { AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
|
|
11
|
+
export { AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
|
|
12
12
|
import 'react/jsx-runtime';
|
|
13
13
|
import 'react';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Server-safe entry. Client components are reached through the files that\n// carry their own \"use client\" directive, so importing this from a Server\n// Component does not pull the editor into the client bundle.\nexport { withGuardrail } from \"./server/wrapper\";\n\n// Team management, exported so an app can build its own UI instead of using\n// the bundled drawer.\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n} from \"./server/actions\";\nexport { GRail } from \"./react/GRail\";\nexport { GRepeater } from \"./react/Repeater\";\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport { configureGuardrail } from \"./core/config\";\nexport {\n listRepeaterItems,\n getRepeaterItemBySlug,\n} from \"./server/repeaterData\";\nexport { getFieldValue } from \"./server/fieldData\";\n// Mutations, exported so an app can build its own add/remove/reorder UI\n// instead of the bundled chrome — same rationale as the team actions above.\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\nexport { useGRail } from \"./react/Provider\";\n\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n} from \"./core/adapter-types\";\n"],"mappings":"AAGA,SAAS,qBAAqB;AAI9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAE1B,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAG9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Server-safe entry. Client components are reached through the files that\n// carry their own \"use client\" directive, so importing this from a Server\n// Component does not pull the editor into the client bundle.\nexport { withGuardrail } from \"./server/wrapper\";\n\n// Team management, exported so an app can build its own UI instead of using\n// the bundled drawer.\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n} from \"./server/actions\";\nexport { GRail } from \"./react/GRail\";\nexport { GRepeater } from \"./react/Repeater\";\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport { configureGuardrail } from \"./core/config\";\nexport {\n listRepeaterItems,\n getRepeaterItemBySlug,\n} from \"./server/repeaterData\";\nexport { getFieldValue } from \"./server/fieldData\";\n// Mutations, exported so an app can build its own add/remove/reorder UI\n// instead of the bundled chrome — same rationale as the team actions above.\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\nexport { useGRail } from \"./react/Provider\";\n\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n} from \"./core/adapter-types\";\n"],"mappings":"AAGA,SAAS,qBAAqB;AAI9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAE1B,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAG9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;","names":[]}
|
package/dist/react/Admin.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import
|
|
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?:
|
|
10
|
+
toolbarExtra?: React__default.ReactNode;
|
|
11
11
|
}): react_jsx_runtime.JSX.Element | null;
|
|
12
12
|
|
|
13
13
|
export { GRailAdmin };
|
package/dist/react/Admin.js
CHANGED
|
@@ -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
|
}
|
package/dist/react/Admin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/Admin.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useActionState, useEffect, useState } from \"react\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport { loginAction, logoutAction } from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { TeamDrawer } from \"./Team\";\nimport { LogOutIcon, PeopleIcon } from \"./Icons\";\nimport { S, 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":";AAsBI,
|
|
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 @@
|
|
|
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 {
|
|
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(
|
|
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 {
|
|
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
|
|
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?:
|
|
8
|
+
children?: React__default.ReactNode;
|
|
9
9
|
/** Forwarded to the rendered element so the editor can measure it. */
|
|
10
|
-
ref?:
|
|
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":[]}
|
package/dist/react/Provider.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import
|
|
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:
|
|
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 };
|
package/dist/react/Provider.js
CHANGED
|
@@ -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__ */
|
|
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
|
|
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
|
|
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:
|
|
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?:
|
|
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
|
|
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:
|
|
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?:
|
|
20
|
+
children?: React__default.ReactNode;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* Renders N instances of one developer-authored template component, with the
|
package/dist/react/Team.js
CHANGED
|
@@ -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
|
{
|
package/dist/react/Team.js.map
CHANGED
|
@@ -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'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'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":[]}
|
package/dist/server/data.js
CHANGED
|
@@ -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
|
};
|
package/dist/server/data.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/data.ts"],"sourcesContent":["import type { OverrideRow
|
|
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":[]}
|
package/dist/server/loader.d.ts
CHANGED
|
@@ -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 };
|
package/dist/server/loader.js
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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":[]}
|
package/dist/server/wrapper.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import
|
|
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:
|
|
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 };
|
package/dist/server/wrapper.js
CHANGED
|
@@ -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
|
|
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":"
|
|
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.
|
|
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"
|