@guard-rail/core 0.12.0 → 0.13.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 +53 -4
- package/dist/adapters/supabase/db.js +4 -1
- package/dist/adapters/supabase/db.js.map +1 -1
- package/dist/core/adapter-types.d.ts +4 -1
- package/dist/core/seo.d.ts +69 -0
- package/dist/core/seo.js +23 -0
- package/dist/core/seo.js.map +1 -0
- package/dist/index.client.d.ts +3 -1
- package/dist/index.client.js +5 -0
- package/dist/index.client.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/react/Admin.js +14 -1
- package/dist/react/Admin.js.map +1 -1
- package/dist/react/GRail/client.js +1 -10
- package/dist/react/GRail/client.js.map +1 -1
- package/dist/react/Icons.d.ts +3 -1
- package/dist/react/Icons.js +5 -0
- package/dist/react/Icons.js.map +1 -1
- package/dist/react/PageSettings.d.ts +12 -0
- package/dist/react/PageSettings.js +275 -0
- package/dist/react/PageSettings.js.map +1 -0
- package/dist/react/guardrail.css +8 -0
- package/dist/react/revert.d.ts +8 -0
- package/dist/react/revert.js +14 -0
- package/dist/react/revert.js.map +1 -0
- package/dist/server/actions.d.ts +19 -1
- package/dist/server/actions.js +77 -0
- package/dist/server/actions.js.map +1 -1
- package/dist/server/loader.js +3 -1
- package/dist/server/loader.js.map +1 -1
- package/dist/server/seo.d.ts +33 -0
- package/dist/server/seo.js +51 -0
- package/dist/server/seo.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -472,12 +472,61 @@ Building your own editing UI? `getRevertInfo(id)` reports what a revert would do
|
|
|
472
472
|
(`{ kind: "previous" | "default" | "none", at }`) and `revertField(id)` does it. Both
|
|
473
473
|
check the caller is an editor or admin.
|
|
474
474
|
|
|
475
|
+
## Page settings and metadata
|
|
476
|
+
|
|
477
|
+
Editors can set each page's search and sharing metadata without touching code. The
|
|
478
|
+
**gear** in the toolbar, next to the people icon, opens a **Page settings** drawer for
|
|
479
|
+
the page they're on, with a **title**, a **description**, a **social image** (with
|
|
480
|
+
alt text) and a **hide this page from search engines** switch. It looks like the
|
|
481
|
+
Team drawer, with a revert arrow on each field.
|
|
482
|
+
|
|
483
|
+
In the page, give `guardrailMetadata()` the page's path and your own defaults:
|
|
484
|
+
|
|
485
|
+
```tsx
|
|
486
|
+
// app/about/page.tsx
|
|
487
|
+
import { guardrailMetadata } from "@guard-rail/core";
|
|
488
|
+
|
|
489
|
+
export async function generateMetadata() {
|
|
490
|
+
return guardrailMetadata("/about", {
|
|
491
|
+
title: "About us",
|
|
492
|
+
description: "Who we are.",
|
|
493
|
+
image: "https://example.com/about.png", // optional
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Whatever an editor saved wins; your values are the fallback, and clearing a field in
|
|
499
|
+
the drawer goes back to them. For a dynamic route, pass the real path:
|
|
500
|
+
`` guardrailMetadata(`/products/${slug}`, { … }) ``. It returns the title, description,
|
|
501
|
+
Open Graph and Twitter card tags, plus a robots tag when the page is hidden from search
|
|
502
|
+
engines. With nothing saved and no defaults given, it returns nothing, so the page
|
|
503
|
+
inherits your layout's metadata. All four settings are read in one query.
|
|
504
|
+
|
|
505
|
+
Things to know:
|
|
506
|
+
|
|
507
|
+
- **The path has to match.** The drawer keys values by the path the browser shows, and
|
|
508
|
+
`guardrailMetadata()` by the path you pass. Query strings, hashes and trailing slashes
|
|
509
|
+
are ignored; a rewrite, locale prefix or `basePath` that changes the visible path will
|
|
510
|
+
need the same path passed in.
|
|
511
|
+
- **Only pages that call `guardrailMetadata()` pick the settings up.** The drawer says so.
|
|
512
|
+
- **Next replaces a layout's `openGraph` and `twitter` objects rather than merging
|
|
513
|
+
them**, so if your layout sets something there (a `siteName`, say) and you want to keep
|
|
514
|
+
it, merge it into what you return.
|
|
515
|
+
- **Social images need an absolute URL.** Uploaded images already have one. A relative
|
|
516
|
+
path needs `metadataBase` set in your layout.
|
|
517
|
+
- **Versioned like everything else.** Settings are ordinary content rows (keyed
|
|
518
|
+
`seo./about.title` and so on), so each one gets the history and one-step revert
|
|
519
|
+
described above. Clearing a field is saved as a version too, so it can be reverted.
|
|
520
|
+
- **No new migration.** They're kept out of the content preloaded into Client Components,
|
|
521
|
+
so a site with many pages doesn't crowd out its real content.
|
|
522
|
+
|
|
475
523
|
## Custom editing UI
|
|
476
524
|
|
|
477
|
-
`<GRail>` edits what's on the page. Some fields have nothing to click —
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
authorization in
|
|
525
|
+
`<GRail>` edits what's on the page. Some fields have nothing to click — and
|
|
526
|
+
some data isn't copy at all, like stock or price. (A page's own SEO title,
|
|
527
|
+
description and og:image have a built-in drawer: see "Page settings and metadata".)
|
|
528
|
+
For anything else, build a small drawer of your own and keep the authorization in
|
|
529
|
+
Guardrail's hands:
|
|
481
530
|
|
|
482
531
|
- **`getFieldValue(id)`** reads any field (server-side).
|
|
483
532
|
- **`setFieldValue(id, value)`** writes one. A string is shorthand for a text
|
|
@@ -23,12 +23,15 @@ function supabaseDB(creds) {
|
|
|
23
23
|
}
|
|
24
24
|
return (data ?? []).map(toOverrideRow);
|
|
25
25
|
},
|
|
26
|
-
async listOverrides(siteId, { prefix, limit }) {
|
|
26
|
+
async listOverrides(siteId, { prefix, limit, exclude }) {
|
|
27
27
|
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);
|
|
28
28
|
if (prefix?.length) {
|
|
29
29
|
const quote = (p) => `"${p.replace(/["\\]/g, "\\$&")}"`;
|
|
30
30
|
query = query.or(prefix.map((p) => `path_key.like.${quote(`${p}*`)}`).join(","));
|
|
31
31
|
}
|
|
32
|
+
for (const p of exclude ?? []) {
|
|
33
|
+
query = query.not("path_key", "like", `${p.replace(/[%_\\]/g, "\\$&")}%`);
|
|
34
|
+
}
|
|
32
35
|
const { data, error } = await query;
|
|
33
36
|
if (error) throw new Error(error.message);
|
|
34
37
|
const rows = (data ?? []).map(toOverrideRow);
|
|
@@ -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, GRRevertInfo, GRRevertResult, 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\n/** PostgREST's \"no such function\" (PGRST202) or Postgres' own (42883): the migration isn't applied. */\nfunction isMissingFunction(error: { code?: string; message?: string }): boolean {\n return error.code === \"PGRST202\" || error.code === \"42883\" || /could not find the function/i.test(error.message ?? \"\");\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 // Reverting runs in the database (gr_revert_content / gr_revert_info) so a\n // step back is one atomic move, and the editor's own access rules apply.\n async getRevertInfo(pathKey: string, siteId: string): Promise<GRRevertInfo> {\n const client = await sessionClient(creds);\n const { data, error } = await client\n .schema(GR_SCHEMA)\n .rpc(\"gr_revert_info\", { p_site: siteId, p_key: pathKey });\n if (error) {\n // Not migrated yet: no history to offer, so just don't offer one.\n if (!isMissingFunction(error)) {\n console.error(\"Guardrail (Supabase): gr_revert_info failed\", error.message);\n }\n return { kind: \"none\" };\n }\n const info = data as GRRevertInfo | null;\n return info && [\"previous\", \"default\", \"none\"].includes(info.kind) ? info : { kind: \"none\" };\n },\n\n async revertOverride(pathKey: string, siteId: string): Promise<GRRevertResult> {\n const client = await sessionClient(creds);\n const { data, error } = await client\n .schema(GR_SCHEMA)\n .rpc(\"gr_revert_content\", { p_site: siteId, p_key: pathKey });\n if (error) {\n return {\n success: false,\n error: isMissingFunction(error)\n ? \"Reverting needs the latest database migration — run `pnpm exec guardrail-init-db --push`.\"\n : error.code === \"42501\"\n ? \"You don't have permission to revert this.\"\n : error.message,\n };\n }\n const result = data as { action?: string; value?: OverrideValue | string } | null;\n if (result?.action === \"restored\" && result.value) {\n const value = typeof result.value === \"string\" ? (JSON.parse(result.value) as OverrideValue) : result.value;\n return { success: true, action: \"restored\", value };\n }\n return { success: true, action: result?.action === \"reset\" ? \"reset\" : \"none\" };\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;AAGA,SAAS,kBAAkB,OAAqD;AAC9E,SAAO,MAAM,SAAS,cAAc,MAAM,SAAS,WAAW,+BAA+B,KAAK,MAAM,WAAW,EAAE;AACvH;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,
|
|
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, GRRevertInfo, GRRevertResult, 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\n/** PostgREST's \"no such function\" (PGRST202) or Postgres' own (42883): the migration isn't applied. */\nfunction isMissingFunction(error: { code?: string; message?: string }): boolean {\n return error.code === \"PGRST202\" || error.code === \"42883\" || /could not find the function/i.test(error.message ?? \"\");\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, exclude }) {\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 for (const p of exclude ?? []) {\n query = query.not(\"path_key\", \"like\", `${p.replace(/[%_\\\\]/g, \"\\\\$&\")}%`);\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 // Reverting runs in the database (gr_revert_content / gr_revert_info) so a\n // step back is one atomic move, and the editor's own access rules apply.\n async getRevertInfo(pathKey: string, siteId: string): Promise<GRRevertInfo> {\n const client = await sessionClient(creds);\n const { data, error } = await client\n .schema(GR_SCHEMA)\n .rpc(\"gr_revert_info\", { p_site: siteId, p_key: pathKey });\n if (error) {\n // Not migrated yet: no history to offer, so just don't offer one.\n if (!isMissingFunction(error)) {\n console.error(\"Guardrail (Supabase): gr_revert_info failed\", error.message);\n }\n return { kind: \"none\" };\n }\n const info = data as GRRevertInfo | null;\n return info && [\"previous\", \"default\", \"none\"].includes(info.kind) ? info : { kind: \"none\" };\n },\n\n async revertOverride(pathKey: string, siteId: string): Promise<GRRevertResult> {\n const client = await sessionClient(creds);\n const { data, error } = await client\n .schema(GR_SCHEMA)\n .rpc(\"gr_revert_content\", { p_site: siteId, p_key: pathKey });\n if (error) {\n return {\n success: false,\n error: isMissingFunction(error)\n ? \"Reverting needs the latest database migration — run `pnpm exec guardrail-init-db --push`.\"\n : error.code === \"42501\"\n ? \"You don't have permission to revert this.\"\n : error.message,\n };\n }\n const result = data as { action?: string; value?: OverrideValue | string } | null;\n if (result?.action === \"restored\" && result.value) {\n const value = typeof result.value === \"string\" ? (JSON.parse(result.value) as OverrideValue) : result.value;\n return { success: true, action: \"restored\", value };\n }\n return { success: true, action: result?.action === \"reset\" ? \"reset\" : \"none\" };\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":"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","names":[]}
|
|
@@ -50,11 +50,14 @@ interface GRDBAdapter {
|
|
|
50
50
|
* client-rendered fields just show their JSX fallback. `limit` is a ceiling,
|
|
51
51
|
* not a page size — return `truncated: true` if rows beyond it existed, so
|
|
52
52
|
* the caller knows the list is partial. `prefix`, when given, restricts to
|
|
53
|
-
* keys starting with any of those strings
|
|
53
|
+
* keys starting with any of those strings; `exclude` leaves out keys starting
|
|
54
|
+
* with any of them (page settings, which would otherwise crowd the preload).
|
|
55
|
+
* An adapter that ignores `exclude` still works — it just preloads more.
|
|
54
56
|
*/
|
|
55
57
|
listOverrides?: (siteId: string, opts: {
|
|
56
58
|
prefix?: string[];
|
|
57
59
|
limit: number;
|
|
60
|
+
exclude?: string[];
|
|
58
61
|
}) => Promise<{
|
|
59
62
|
rows: OverrideRow[];
|
|
60
63
|
truncated: boolean;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { GRRevertInfo } from './adapter-types.js';
|
|
2
|
+
import 'react';
|
|
3
|
+
import './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Page settings — the title, description, social image and "hide from search
|
|
7
|
+
* engines" flag an editor can set for a page, kept as ordinary content rows so
|
|
8
|
+
* they get the same storage, permissions and revert history as everything else.
|
|
9
|
+
*
|
|
10
|
+
* Pure and client-safe: the path/key rules here are shared by the drawer (which
|
|
11
|
+
* keys off the browser's current path) and guardrailMetadata() (which is handed
|
|
12
|
+
* the path by the page), and the two must agree exactly.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Reserved namespace for page settings. Kept out of the client preload — one row per page would crowd it. */
|
|
16
|
+
declare const SEO_PREFIX = "seo.";
|
|
17
|
+
type SeoField = "title" | "description" | "image" | "noindex";
|
|
18
|
+
declare const SEO_FIELDS: readonly SeoField[];
|
|
19
|
+
/** Server-side caps. The drawer's counters are guidance; these are the limit. */
|
|
20
|
+
declare const SEO_LIMITS: {
|
|
21
|
+
readonly title: 300;
|
|
22
|
+
readonly description: 1000;
|
|
23
|
+
readonly path: 150;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* One canonical form per page: a leading slash, no query string or hash, no
|
|
27
|
+
* doubled or trailing slashes (except the root). Null if it isn't a usable path.
|
|
28
|
+
*/
|
|
29
|
+
declare function normalizePagePath(raw: unknown): string | null;
|
|
30
|
+
/** The content key for one setting of one page, e.g. `seo./about.title`. */
|
|
31
|
+
declare function seoKey(path: string, field: SeoField): string;
|
|
32
|
+
interface GRPageImage {
|
|
33
|
+
src: string;
|
|
34
|
+
alt: string;
|
|
35
|
+
width?: number;
|
|
36
|
+
height?: number;
|
|
37
|
+
}
|
|
38
|
+
/** What the drawer loads: the saved values, which of them are saved, and what a revert would do to each. */
|
|
39
|
+
interface GRPageSettings {
|
|
40
|
+
path: string;
|
|
41
|
+
title: string;
|
|
42
|
+
description: string;
|
|
43
|
+
image: GRPageImage;
|
|
44
|
+
noindex: boolean;
|
|
45
|
+
/** Which settings have a saved value (as opposed to using the page's own default). */
|
|
46
|
+
saved: Record<SeoField, boolean>;
|
|
47
|
+
revert: Record<SeoField, GRRevertInfo>;
|
|
48
|
+
}
|
|
49
|
+
/** Only the settings that changed. `image: null` clears the saved image. */
|
|
50
|
+
interface GRPageSettingsPatch {
|
|
51
|
+
title?: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
image?: GRPageImage | null;
|
|
54
|
+
noindex?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** The page's own defaults, used wherever an editor hasn't saved a value. */
|
|
57
|
+
interface GRMetadataFallback {
|
|
58
|
+
title?: string;
|
|
59
|
+
description?: string;
|
|
60
|
+
image?: string | {
|
|
61
|
+
url: string;
|
|
62
|
+
width?: number;
|
|
63
|
+
height?: number;
|
|
64
|
+
alt?: string;
|
|
65
|
+
};
|
|
66
|
+
noindex?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { type GRMetadataFallback, type GRPageImage, type GRPageSettings, type GRPageSettingsPatch, SEO_FIELDS, SEO_LIMITS, SEO_PREFIX, type SeoField, normalizePagePath, seoKey };
|
package/dist/core/seo.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const SEO_PREFIX = "seo.";
|
|
2
|
+
const SEO_FIELDS = ["title", "description", "image", "noindex"];
|
|
3
|
+
const SEO_LIMITS = { title: 300, description: 1e3, path: 150 };
|
|
4
|
+
function normalizePagePath(raw) {
|
|
5
|
+
if (typeof raw !== "string") return null;
|
|
6
|
+
let path = raw.trim().split("#")[0].split("?")[0];
|
|
7
|
+
if (!path.startsWith("/")) path = `/${path}`;
|
|
8
|
+
path = path.replace(/\/{2,}/g, "/");
|
|
9
|
+
if (path.length > 1) path = path.replace(/\/+$/, "");
|
|
10
|
+
if (path.length > SEO_LIMITS.path || /[\u0000-\u001f\u007f]/.test(path)) return null;
|
|
11
|
+
return path;
|
|
12
|
+
}
|
|
13
|
+
function seoKey(path, field) {
|
|
14
|
+
return `${SEO_PREFIX}${path}.${field}`;
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
SEO_FIELDS,
|
|
18
|
+
SEO_LIMITS,
|
|
19
|
+
SEO_PREFIX,
|
|
20
|
+
normalizePagePath,
|
|
21
|
+
seoKey
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=seo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/seo.ts"],"sourcesContent":["/**\n * Page settings — the title, description, social image and \"hide from search\n * engines\" flag an editor can set for a page, kept as ordinary content rows so\n * they get the same storage, permissions and revert history as everything else.\n *\n * Pure and client-safe: the path/key rules here are shared by the drawer (which\n * keys off the browser's current path) and guardrailMetadata() (which is handed\n * the path by the page), and the two must agree exactly.\n */\nimport type { GRRevertInfo } from \"./adapter-types\";\n\n/** Reserved namespace for page settings. Kept out of the client preload — one row per page would crowd it. */\nexport const SEO_PREFIX = \"seo.\";\n\nexport type SeoField = \"title\" | \"description\" | \"image\" | \"noindex\";\nexport const SEO_FIELDS: readonly SeoField[] = [\"title\", \"description\", \"image\", \"noindex\"];\n\n/** Server-side caps. The drawer's counters are guidance; these are the limit. */\nexport const SEO_LIMITS = { title: 300, description: 1000, path: 150 } as const;\n\n/**\n * One canonical form per page: a leading slash, no query string or hash, no\n * doubled or trailing slashes (except the root). Null if it isn't a usable path.\n */\nexport function normalizePagePath(raw: unknown): string | null {\n if (typeof raw !== \"string\") return null;\n let path = raw.trim().split(\"#\")[0]!.split(\"?\")[0]!;\n if (!path.startsWith(\"/\")) path = `/${path}`;\n path = path.replace(/\\/{2,}/g, \"/\");\n if (path.length > 1) path = path.replace(/\\/+$/, \"\");\n if (path.length > SEO_LIMITS.path || /[\\u0000-\\u001f\\u007f]/.test(path)) return null;\n return path;\n}\n\n/** The content key for one setting of one page, e.g. `seo./about.title`. */\nexport function seoKey(path: string, field: SeoField): string {\n return `${SEO_PREFIX}${path}.${field}`;\n}\n\nexport interface GRPageImage {\n src: string;\n alt: string;\n width?: number;\n height?: number;\n}\n\n/** What the drawer loads: the saved values, which of them are saved, and what a revert would do to each. */\nexport interface GRPageSettings {\n path: string;\n title: string;\n description: string;\n image: GRPageImage;\n noindex: boolean;\n /** Which settings have a saved value (as opposed to using the page's own default). */\n saved: Record<SeoField, boolean>;\n revert: Record<SeoField, GRRevertInfo>;\n}\n\n/** Only the settings that changed. `image: null` clears the saved image. */\nexport interface GRPageSettingsPatch {\n title?: string;\n description?: string;\n image?: GRPageImage | null;\n noindex?: boolean;\n}\n\n/** The page's own defaults, used wherever an editor hasn't saved a value. */\nexport interface GRMetadataFallback {\n title?: string;\n description?: string;\n image?: string | { url: string; width?: number; height?: number; alt?: string };\n noindex?: boolean;\n}\n"],"mappings":"AAYO,MAAM,aAAa;AAGnB,MAAM,aAAkC,CAAC,SAAS,eAAe,SAAS,SAAS;AAGnF,MAAM,aAAa,EAAE,OAAO,KAAK,aAAa,KAAM,MAAM,IAAI;AAM9D,SAAS,kBAAkB,KAA6B;AAC7D,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,OAAO,IAAI,KAAK,EAAE,MAAM,GAAG,EAAE,CAAC,EAAG,MAAM,GAAG,EAAE,CAAC;AACjD,MAAI,CAAC,KAAK,WAAW,GAAG,EAAG,QAAO,IAAI,IAAI;AAC1C,SAAO,KAAK,QAAQ,WAAW,GAAG;AAClC,MAAI,KAAK,SAAS,EAAG,QAAO,KAAK,QAAQ,QAAQ,EAAE;AACnD,MAAI,KAAK,SAAS,WAAW,QAAQ,wBAAwB,KAAK,IAAI,EAAG,QAAO;AAChF,SAAO;AACT;AAGO,SAAS,OAAO,MAAc,OAAyB;AAC5D,SAAO,GAAG,UAAU,GAAG,IAAI,IAAI,KAAK;AACtC;","names":[]}
|
package/dist/index.client.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { GRCspDirectives, GRCspOptions, cspString, guardrailCspDirectives } from
|
|
|
4
4
|
export { createAssetUpload, forgotPasswordAction, getRevertInfo, inviteMember, listAssets, listTeam, removeMember, revertField, sendPasswordResetAction, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole } from './server/actions.js';
|
|
5
5
|
export { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from './server/repeaterActions.js';
|
|
6
6
|
export { GRepeaterProps, RepeaterItemProps } from './react/Repeater/index.js';
|
|
7
|
+
export { GRMetadataFallback } from './core/seo.js';
|
|
7
8
|
export { ResendEmailOptions, ResendInviteContent, ResendInviteOptions, ResendInviteTemplateEvent, ResendResetTemplateEvent } from './email/resend.js';
|
|
8
9
|
export { ActionState, GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRViewer, HTMLKeys, OverrideRow, OverrideType, OverrideValue, PolymorphicProps, SetPasswordState } from './core/types.js';
|
|
9
10
|
export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPasswordResetEvent, GRPrefetch, GRRateLimitHit, GRRateLimitName, GRRateLimiter, GRRepeaterAdapter, GRRevertInfo, GRRevertResult, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
|
|
@@ -16,6 +17,7 @@ declare const GRepeater: (..._args: unknown[]) => never;
|
|
|
16
17
|
declare const listRepeaterItems: (..._args: unknown[]) => never;
|
|
17
18
|
declare const getRepeaterItemBySlug: (..._args: unknown[]) => never;
|
|
18
19
|
declare const getFieldValue: (..._args: unknown[]) => never;
|
|
20
|
+
declare const guardrailMetadata: (..._args: unknown[]) => never;
|
|
19
21
|
declare const isAdmin: (..._args: unknown[]) => never;
|
|
20
22
|
declare const isEditor: (..._args: unknown[]) => never;
|
|
21
23
|
declare const canEdit: (..._args: unknown[]) => never;
|
|
@@ -23,4 +25,4 @@ declare const resendInvite: (..._args: unknown[]) => never;
|
|
|
23
25
|
declare const resendEmails: (..._args: unknown[]) => never;
|
|
24
26
|
declare const consoleEmails: (..._args: unknown[]) => never;
|
|
25
27
|
|
|
26
|
-
export { GRepeater, canEdit, configureGuardrail, consoleEmails, getFieldValue, getRepeaterItemBySlug, isAdmin, isEditor, listRepeaterItems, resendEmails, resendInvite, withGuardrail };
|
|
28
|
+
export { GRepeater, canEdit, configureGuardrail, consoleEmails, getFieldValue, getRepeaterItemBySlug, guardrailMetadata, isAdmin, isEditor, listRepeaterItems, resendEmails, resendInvite, withGuardrail };
|
package/dist/index.client.js
CHANGED
|
@@ -52,6 +52,10 @@ const getFieldValue = serverOnly(
|
|
|
52
52
|
"getFieldValue()",
|
|
53
53
|
"Read the value in a Server Component and pass it down as a prop."
|
|
54
54
|
);
|
|
55
|
+
const guardrailMetadata = serverOnly(
|
|
56
|
+
"guardrailMetadata()",
|
|
57
|
+
"Call it from generateMetadata() in a page or layout, which runs on the server."
|
|
58
|
+
);
|
|
55
59
|
const isAdmin = serverOnly(
|
|
56
60
|
"isAdmin()",
|
|
57
61
|
"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show."
|
|
@@ -90,6 +94,7 @@ export {
|
|
|
90
94
|
getRepeaterItemBySlug,
|
|
91
95
|
getRevertInfo,
|
|
92
96
|
guardrailCspDirectives,
|
|
97
|
+
guardrailMetadata,
|
|
93
98
|
inviteMember,
|
|
94
99
|
isAdmin,
|
|
95
100
|
isEditor,
|
package/dist/index.client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.client.ts"],"sourcesContent":["// Client-graph entry. `import ... from \"@guard-rail/core\"` resolves here from a\n// Client Component (see \"exports\" in package.json) and to ./index from a\n// Server Component, so an app uses one import path everywhere.\n//\n// Nothing server-only may be imported here — this is bundled for the browser.\n// Server Actions are fine: a \"use server\" module is replaced by references in\n// the client bundle, which is what lets an app build its own team/repeater UI\n// in a Client Component.\n\nexport { GRail } from \"./react/GRail/universal\";\nexport { useGRail } from \"./react/Provider\";\n// What a strict Content-Security-Policy has to allow for the editing chrome.\nexport { guardrailCspDirectives, cspString } from \"./core/csp\";\nexport type { GRCspDirectives, GRCspOptions } from \"./core/csp\";\n\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n forgotPasswordAction,\n sendPasswordResetAction,\n setFieldValue,\n getRevertInfo,\n revertField,\n} from \"./server/actions\";\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\n\n/**\n * Stand-ins for the server-only exports, so importing one from a Client\n * Component fails with an explanation instead of an undefined-is-not-a-function\n * deep in a render. Also what a tool that doesn't set the \"react-server\"\n * condition (a test runner, a plain Node script) gets from the package root —\n * `@guard-rail/core/server` is the explicit path to the real ones there.\n */\nfunction serverOnly(name: string, hint: string) {\n return (..._args: unknown[]): never => {\n throw new Error(\n `Guardrail: ${name} only works in a Server Component. ${hint} ` +\n `(If this isn't a Client Component, your tooling may not set the ` +\n `\"react-server\" condition — import from \"@guard-rail/core/server\" instead.)`,\n );\n };\n}\n\nexport const withGuardrail = serverOnly(\n \"withGuardrail()\",\n \"Call it in your root layout, which is a Server Component.\",\n);\nexport const configureGuardrail = serverOnly(\n \"configureGuardrail()\",\n \"Configuration is applied by withGuardrail() in your root layout.\",\n);\nexport const GRepeater = serverOnly(\n \"<GRepeater>\",\n \"Render it in a Server Component and pass it into this one as children or a prop.\",\n);\nexport const listRepeaterItems = serverOnly(\n \"listRepeaterItems()\",\n \"Read repeater items in a Server Component and pass them down.\",\n);\nexport const getRepeaterItemBySlug = serverOnly(\n \"getRepeaterItemBySlug()\",\n \"Resolve the item in a Server Component (your [slug]/page.tsx) and pass it down.\",\n);\nexport const getFieldValue = serverOnly(\n \"getFieldValue()\",\n \"Read the value in a Server Component and pass it down as a prop.\",\n);\nexport const isAdmin = serverOnly(\n \"isAdmin()\",\n \"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show.\",\n);\nexport const isEditor = serverOnly(\n \"isEditor()\",\n \"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show.\",\n);\nexport const canEdit = serverOnly(\n \"canEdit()\",\n \"Call it in a Server Component; in a Client Component, read `canEdit` from useGRail().\",\n);\nexport const resendInvite = serverOnly(\n \"resendInvite()\",\n \"Pass it as onInvite in withGuardrail(), which runs in your root layout.\",\n);\nexport const resendEmails = serverOnly(\n \"resendEmails()\",\n \"Spread it into withGuardrail(), which runs in your root layout.\",\n);\nexport const consoleEmails = serverOnly(\n \"consoleEmails()\",\n \"Spread it into withGuardrail(), which runs in your root layout.\",\n);\n\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport type {\n ResendInviteOptions,\n ResendEmailOptions,\n ResendInviteContent,\n ResendInviteTemplateEvent,\n ResendResetTemplateEvent,\n} from \"./email/resend\";\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n GRPasswordResetEvent,\n GRRevertInfo,\n GRRevertResult,\n GRRateLimiter,\n GRRateLimitHit,\n GRRateLimitName,\n} from \"./core/adapter-types\";\n"],"mappings":"AASA,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAEzB,SAAS,wBAAwB,iBAAiB;AAGlD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,WAAW,MAAc,MAAc;AAC9C,SAAO,IAAI,UAA4B;AACrC,UAAM,IAAI;AAAA,MACR,cAAc,IAAI,sCAAsC,IAAI;AAAA,IAG9D;AAAA,EACF;AACF;AAEO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AACF;AACO,MAAM,YAAY;AAAA,EACvB;AAAA,EACA;AACF;AACO,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AACF;AACO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AACF;AACO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,UAAU;AAAA,EACrB;AAAA,EACA;AACF;AACO,MAAM,WAAW;AAAA,EACtB;AAAA,EACA;AACF;AACO,MAAM,UAAU;AAAA,EACrB;AAAA,EACA;AACF;AACO,MAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AACF;AACO,MAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AACF;AACO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.client.ts"],"sourcesContent":["// Client-graph entry. `import ... from \"@guard-rail/core\"` resolves here from a\n// Client Component (see \"exports\" in package.json) and to ./index from a\n// Server Component, so an app uses one import path everywhere.\n//\n// Nothing server-only may be imported here — this is bundled for the browser.\n// Server Actions are fine: a \"use server\" module is replaced by references in\n// the client bundle, which is what lets an app build its own team/repeater UI\n// in a Client Component.\n\nexport { GRail } from \"./react/GRail/universal\";\nexport { useGRail } from \"./react/Provider\";\n// What a strict Content-Security-Policy has to allow for the editing chrome.\nexport { guardrailCspDirectives, cspString } from \"./core/csp\";\nexport type { GRCspDirectives, GRCspOptions } from \"./core/csp\";\n\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n forgotPasswordAction,\n sendPasswordResetAction,\n setFieldValue,\n getRevertInfo,\n revertField,\n} from \"./server/actions\";\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\n\n/**\n * Stand-ins for the server-only exports, so importing one from a Client\n * Component fails with an explanation instead of an undefined-is-not-a-function\n * deep in a render. Also what a tool that doesn't set the \"react-server\"\n * condition (a test runner, a plain Node script) gets from the package root —\n * `@guard-rail/core/server` is the explicit path to the real ones there.\n */\nfunction serverOnly(name: string, hint: string) {\n return (..._args: unknown[]): never => {\n throw new Error(\n `Guardrail: ${name} only works in a Server Component. ${hint} ` +\n `(If this isn't a Client Component, your tooling may not set the ` +\n `\"react-server\" condition — import from \"@guard-rail/core/server\" instead.)`,\n );\n };\n}\n\nexport const withGuardrail = serverOnly(\n \"withGuardrail()\",\n \"Call it in your root layout, which is a Server Component.\",\n);\nexport const configureGuardrail = serverOnly(\n \"configureGuardrail()\",\n \"Configuration is applied by withGuardrail() in your root layout.\",\n);\nexport const GRepeater = serverOnly(\n \"<GRepeater>\",\n \"Render it in a Server Component and pass it into this one as children or a prop.\",\n);\nexport const listRepeaterItems = serverOnly(\n \"listRepeaterItems()\",\n \"Read repeater items in a Server Component and pass them down.\",\n);\nexport const getRepeaterItemBySlug = serverOnly(\n \"getRepeaterItemBySlug()\",\n \"Resolve the item in a Server Component (your [slug]/page.tsx) and pass it down.\",\n);\nexport const getFieldValue = serverOnly(\n \"getFieldValue()\",\n \"Read the value in a Server Component and pass it down as a prop.\",\n);\nexport const guardrailMetadata = serverOnly(\n \"guardrailMetadata()\",\n \"Call it from generateMetadata() in a page or layout, which runs on the server.\",\n);\nexport const isAdmin = serverOnly(\n \"isAdmin()\",\n \"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show.\",\n);\nexport const isEditor = serverOnly(\n \"isEditor()\",\n \"Call it inside your own Server Action to authorize it; in a Client Component, read useGRail().role to decide what to show.\",\n);\nexport const canEdit = serverOnly(\n \"canEdit()\",\n \"Call it in a Server Component; in a Client Component, read `canEdit` from useGRail().\",\n);\nexport const resendInvite = serverOnly(\n \"resendInvite()\",\n \"Pass it as onInvite in withGuardrail(), which runs in your root layout.\",\n);\nexport const resendEmails = serverOnly(\n \"resendEmails()\",\n \"Spread it into withGuardrail(), which runs in your root layout.\",\n);\nexport const consoleEmails = serverOnly(\n \"consoleEmails()\",\n \"Spread it into withGuardrail(), which runs in your root layout.\",\n);\n\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport type { GRMetadataFallback } from \"./core/seo\";\nexport type {\n ResendInviteOptions,\n ResendEmailOptions,\n ResendInviteContent,\n ResendInviteTemplateEvent,\n ResendResetTemplateEvent,\n} from \"./email/resend\";\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n GRPasswordResetEvent,\n GRRevertInfo,\n GRRevertResult,\n GRRateLimiter,\n GRRateLimitHit,\n GRRateLimitName,\n} from \"./core/adapter-types\";\n"],"mappings":"AASA,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAEzB,SAAS,wBAAwB,iBAAiB;AAGlD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,WAAW,MAAc,MAAc;AAC9C,SAAO,IAAI,UAA4B;AACrC,UAAM,IAAI;AAAA,MACR,cAAc,IAAI,sCAAsC,IAAI;AAAA,IAG9D;AAAA,EACF;AACF;AAEO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AACF;AACO,MAAM,YAAY;AAAA,EACvB;AAAA,EACA;AACF;AACO,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AACF;AACO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AACF;AACO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;AACO,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AACF;AACO,MAAM,UAAU;AAAA,EACrB;AAAA,EACA;AACF;AACO,MAAM,WAAW;AAAA,EACtB;AAAA,EACA;AACF;AACO,MAAM,UAAU;AAAA,EACrB;AAAA,EACA;AACF;AACO,MAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AACF;AACO,MAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AACF;AACO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AACF;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { GRepeater, GRepeaterProps, RepeaterItemProps } from './react/Repeater/i
|
|
|
5
5
|
export { configureGuardrail } from './core/config.js';
|
|
6
6
|
export { getRepeaterItemBySlug, listRepeaterItems } from './server/repeaterData.js';
|
|
7
7
|
export { getFieldValue } from './server/fieldData.js';
|
|
8
|
+
export { guardrailMetadata } from './server/seo.js';
|
|
9
|
+
export { GRMetadataFallback } from './core/seo.js';
|
|
8
10
|
export { canEdit, isAdmin, isEditor } from './server/access.js';
|
|
9
11
|
export { ResendEmailOptions, ResendInviteContent, ResendInviteOptions, ResendInviteTemplateEvent, ResendResetTemplateEvent, resendEmails, resendInvite } from './email/resend.js';
|
|
10
12
|
export { consoleEmails } from './email/console.js';
|
|
@@ -15,3 +17,4 @@ export { ActionState, GRAsset, GRMember, GRRepeaterItem, GRRole, GRState, GRView
|
|
|
15
17
|
export { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPasswordResetEvent, GRPrefetch, GRRateLimitHit, GRRateLimitName, GRRateLimiter, GRRepeaterAdapter, GRRevertInfo, GRRevertResult, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget } from './core/adapter-types.js';
|
|
16
18
|
import 'react/jsx-runtime';
|
|
17
19
|
import 'react';
|
|
20
|
+
import 'next';
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
getRepeaterItemBySlug
|
|
23
23
|
} from "./server/repeaterData";
|
|
24
24
|
import { getFieldValue } from "./server/fieldData";
|
|
25
|
+
import { guardrailMetadata } from "./server/seo";
|
|
25
26
|
import { isAdmin, isEditor, canEdit } from "./server/access";
|
|
26
27
|
import { resendInvite, resendEmails } from "./email/resend";
|
|
27
28
|
import { consoleEmails } from "./email/console";
|
|
@@ -46,6 +47,7 @@ export {
|
|
|
46
47
|
getRepeaterItemBySlug,
|
|
47
48
|
getRevertInfo,
|
|
48
49
|
guardrailCspDirectives,
|
|
50
|
+
guardrailMetadata,
|
|
49
51
|
inviteMember,
|
|
50
52
|
isAdmin,
|
|
51
53
|
isEditor,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Server-safe entry. Client components are reached through the files that\n// carry their own \"use client\" directive, so importing this from a Server\n// Component does not pull the editor into the client bundle.\nexport { withGuardrail } from \"./server/wrapper\";\n\n// Team management and the set-password step, exported so an app can build\n// its own UI instead of using the bundled drawer / sign-in bar.\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n forgotPasswordAction,\n sendPasswordResetAction,\n setFieldValue,\n getRevertInfo,\n revertField,\n} from \"./server/actions\";\nexport { GRail } from \"./react/GRail\";\nexport { GRepeater } from \"./react/Repeater\";\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport { configureGuardrail } from \"./core/config\";\nexport {\n listRepeaterItems,\n getRepeaterItemBySlug,\n} from \"./server/repeaterData\";\nexport { getFieldValue } from \"./server/fieldData\";\n// Role / edit-mode checks for a custom admin UI and the Server Actions behind\n// it. Plain server functions, not actions — see access.ts for why.\nexport { isAdmin, isEditor, canEdit } from \"./server/access\";\n// Invite delivery through Resend, as an onInvite handler — see email/resend.ts.\nexport { resendInvite, resendEmails } from \"./email/resend\";\nexport { consoleEmails } from \"./email/console\";\nexport type {\n ResendInviteOptions,\n ResendEmailOptions,\n ResendInviteContent,\n ResendInviteTemplateEvent,\n ResendResetTemplateEvent,\n} from \"./email/resend\";\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// What a strict Content-Security-Policy has to allow for the editing chrome.\nexport { guardrailCspDirectives, cspString } from \"./core/csp\";\nexport type { GRCspDirectives, GRCspOptions } from \"./core/csp\";\n\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n GRPasswordResetEvent,\n GRRevertInfo,\n GRRevertResult,\n GRRateLimiter,\n GRRateLimitHit,\n GRRateLimitName,\n} from \"./core/adapter-types\";\n"],"mappings":"AAGA,SAAS,qBAAqB;AAI9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,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;
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Server-safe entry. Client components are reached through the files that\n// carry their own \"use client\" directive, so importing this from a Server\n// Component does not pull the editor into the client bundle.\nexport { withGuardrail } from \"./server/wrapper\";\n\n// Team management and the set-password step, exported so an app can build\n// its own UI instead of using the bundled drawer / sign-in bar.\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n forgotPasswordAction,\n sendPasswordResetAction,\n setFieldValue,\n getRevertInfo,\n revertField,\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// A page's metadata, with what editors saved from the Page settings drawer layered over your defaults.\nexport { guardrailMetadata } from \"./server/seo\";\nexport type { GRMetadataFallback } from \"./core/seo\";\n// Role / edit-mode checks for a custom admin UI and the Server Actions behind\n// it. Plain server functions, not actions — see access.ts for why.\nexport { isAdmin, isEditor, canEdit } from \"./server/access\";\n// Invite delivery through Resend, as an onInvite handler — see email/resend.ts.\nexport { resendInvite, resendEmails } from \"./email/resend\";\nexport { consoleEmails } from \"./email/console\";\nexport type {\n ResendInviteOptions,\n ResendEmailOptions,\n ResendInviteContent,\n ResendInviteTemplateEvent,\n ResendResetTemplateEvent,\n} from \"./email/resend\";\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// What a strict Content-Security-Policy has to allow for the editing chrome.\nexport { guardrailCspDirectives, cspString } from \"./core/csp\";\nexport type { GRCspDirectives, GRCspOptions } from \"./core/csp\";\n\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n GRPasswordResetEvent,\n GRRevertInfo,\n GRRevertResult,\n GRRateLimiter,\n GRRateLimitHit,\n GRRateLimitName,\n} from \"./core/adapter-types\";\n"],"mappings":"AAGA,SAAS,qBAAqB;AAI9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,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;AAE9B,SAAS,yBAAyB;AAIlC,SAAS,SAAS,UAAU,eAAe;AAE3C,SAAS,cAAc,oBAAoB;AAC3C,SAAS,qBAAqB;AAU9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;AAEzB,SAAS,wBAAwB,iBAAiB;","names":[]}
|
package/dist/react/Admin.js
CHANGED
|
@@ -5,8 +5,9 @@ import { usePathname, useRouter } from "next/navigation";
|
|
|
5
5
|
import { loginAction, logoutAction } from "../server/actions";
|
|
6
6
|
import { useGRail } from "./Provider";
|
|
7
7
|
import { TeamDrawer } from "./Team";
|
|
8
|
+
import { PageSettingsDrawer } from "./PageSettings";
|
|
8
9
|
import { AuthDrawer } from "./AuthDrawer";
|
|
9
|
-
import { CloseIcon, LogOutIcon, PeopleIcon } from "./Icons";
|
|
10
|
+
import { CloseIcon, LogOutIcon, PeopleIcon, SettingsIcon } from "./Icons";
|
|
10
11
|
import "./guardrail.css";
|
|
11
12
|
import { GR_COOKIES, GR_EDIT_MODE_OFF_TTL_SECONDS } from "../core/defs";
|
|
12
13
|
const initialState = { email: "", password: "" };
|
|
@@ -18,6 +19,7 @@ function GRailAdmin(props) {
|
|
|
18
19
|
function AdminBody({ toolbarExtra }) {
|
|
19
20
|
const { isAuth, isEditMode, needsPassword, authError, role, auth } = useGRail();
|
|
20
21
|
const [teamOpen, setTeamOpen] = useState(false);
|
|
22
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
21
23
|
const [forgotOpen, setForgotOpen] = useState(false);
|
|
22
24
|
const [errorDismissed, setErrorDismissed] = useState(false);
|
|
23
25
|
const [formState, formAction, isPending] = useActionState(
|
|
@@ -155,6 +157,7 @@ function AdminBody({ toolbarExtra }) {
|
|
|
155
157
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
156
158
|
linkErrorDrawer,
|
|
157
159
|
teamOpen && /* @__PURE__ */ jsx(TeamDrawer, { onClose: () => setTeamOpen(false) }),
|
|
160
|
+
settingsOpen && /* @__PURE__ */ jsx(PageSettingsDrawer, { onClose: () => setSettingsOpen(false) }),
|
|
158
161
|
/* @__PURE__ */ jsxs("div", { "data-guardrail": true, className: "gr-toolbar", children: [
|
|
159
162
|
/* @__PURE__ */ jsx("span", { className: "gr-dot" }),
|
|
160
163
|
/* @__PURE__ */ jsx("span", { className: "gr-toolbar-label", children: "Edit Mode" }),
|
|
@@ -169,6 +172,16 @@ function AdminBody({ toolbarExtra }) {
|
|
|
169
172
|
children: /* @__PURE__ */ jsx(PeopleIcon, {})
|
|
170
173
|
}
|
|
171
174
|
),
|
|
175
|
+
/* @__PURE__ */ jsx(
|
|
176
|
+
"button",
|
|
177
|
+
{
|
|
178
|
+
className: "gr-icon-btn gr-text-muted gr-size-24",
|
|
179
|
+
onClick: () => setSettingsOpen(true),
|
|
180
|
+
title: "Page settings",
|
|
181
|
+
"aria-label": "Page settings",
|
|
182
|
+
children: /* @__PURE__ */ jsx(SettingsIcon, {})
|
|
183
|
+
}
|
|
184
|
+
),
|
|
172
185
|
toolbarExtra && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
173
186
|
/* @__PURE__ */ jsx("span", { className: "gr-divider" }),
|
|
174
187
|
toolbarExtra
|
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 { AuthDrawer } from \"./AuthDrawer\";\nimport { CloseIcon, LogOutIcon, PeopleIcon } from \"./Icons\";\nimport \"./guardrail.css\";\nimport { GR_COOKIES, GR_EDIT_MODE_OFF_TTL_SECONDS } from \"../core/defs\";\nimport type { ActionState } from \"../core/types\";\n\nconst initialState: ActionState = { email: \"\", password: \"\" };\n\n/**\n * Imports the chrome's stylesheet (guardrail.css), which also defines the\n * light/dark theme variables, so every piece of chrome — the sign-in bar, the\n * toolbar, and the field popups portaled to <body> — picks them up from one\n * place. A stylesheet rather than inline styles so the chrome works under a\n * strict Content-Security-Policy.\n */\nexport function GRailAdmin(props: { toolbarExtra?: React.ReactNode }) {\n const { isEditMode } = useGRail();\n if (!isEditMode) return null;\n return <AdminBody {...props} />;\n}\n\nfunction AdminBody({ toolbarExtra }: { toolbarExtra?: React.ReactNode }) {\n const { isAuth, isEditMode, needsPassword, authError, role, auth } = useGRail();\n const [teamOpen, setTeamOpen] = useState(false);\n const [forgotOpen, setForgotOpen] = useState(false);\n const [errorDismissed, setErrorDismissed] = 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 // The message for an emailed link that didn't work. The cookie behind it is\n // read on the server, so dismissing clears it there too (and refreshes), but\n // hides it right away without waiting for that.\n const dismissAuthError = () => {\n document.cookie = `${GR_COOKIES.AUTH_ERROR}=; path=/; max-age=0; SameSite=Lax`;\n setErrorDismissed(true);\n router.refresh();\n };\n const linkErrorDrawer =\n authError && !errorDismissed ? (\n <AuthDrawer\n mode=\"forgot\"\n title=\"That link didn't work\"\n message={\n authError === \"expired\"\n ? \"It has expired or was already used. Links work once. Enter your email to get a new one.\"\n : \"We couldn't complete it. Enter your email to get a new link.\"\n }\n onClose={dismissAuthError}\n />\n ) : null;\n\n // Ends the editing session, not the sign-in: the overlay goes away and the\n // person's session stays, so ?edit=true brings it straight back with no\n // credentials. The sign-in form only returns once the session itself is\n // gone (expired, or ended elsewhere). Explicit \"false\" rather than deleting\n // the cookie: with a live session, deleting it would leave edit mode on —\n // see resolveEditMode. router.replace drops ?edit=true from the URL (a bare\n // refresh would leave it there), and refresh re-renders without chrome.\n //\n // Also what the sign-in bar's dismiss button does, for someone who was never\n // authenticated and doesn't want to be here at all.\n const exitEditMode = () => {\n document.cookie = `${GR_COOKIES.EDIT_MODE}=false; path=/; max-age=${GR_EDIT_MODE_OFF_TTL_SECONDS}; SameSite=Lax`;\n router.replace(pathname);\n router.refresh();\n };\n\n // A real sign-out, kept only for the \"no access\" bar below: someone on the\n // wrong account has no other way to switch to the right one. logoutAction()\n // clears the edit-mode cookie itself — a client-side navigation right after\n // it used to race Next's own post-action route refresh and lose.\n const signOut = async () => {\n await logoutAction();\n router.replace(pathname);\n router.refresh();\n };\n\n // No intent signalled — render nothing at all.\n if (!isEditMode) return null;\n\n if (!isAuth) {\n // The adapter cannot take a password here, so hand sign-in off rather than\n // showing a form that could never succeed.\n if (!auth.password) {\n return (\n <>\n <div data-guardrail className=\"gr-banner\">\n <div className=\"gr-banner-form\">\n <div className=\"gr-brand-stack\">\n <span className=\"gr-brand\">Guardrail</span>\n {!auth.signInUrl && (\n <span className=\"gr-fs-12 gr-text-muted\">\n No sign-in method is configured for this site.\n </span>\n )}\n </div>\n {auth.signInUrl && (\n <a href={auth.signInUrl} className=\"gr-primary-btn gr-no-underline\">\n Sign in\n </a>\n )}\n <button\n type=\"button\"\n className=\"gr-icon-btn gr-text-muted\"\n onClick={exitEditMode}\n title=\"Not editing? Dismiss\"\n aria-label=\"Dismiss\"\n >\n <CloseIcon />\n </button>\n </div>\n </div>\n {linkErrorDrawer}\n </>\n );\n }\n\n return (\n <>\n <div data-guardrail className=\"gr-banner\">\n <form action={formAction} className=\"gr-banner-form\">\n <div className=\"gr-brand-stack\">\n <span className=\"gr-brand\">Guardrail</span>\n {formState.error && <span className=\"gr-error\">{formState.error}</span>}\n </div>\n <input\n name=\"email\"\n type=\"email\"\n autoComplete=\"email\"\n placeholder=\"Email\"\n defaultValue={formState.email}\n required\n className=\"gr-input\"\n />\n <input\n name=\"password\"\n type=\"password\"\n autoComplete=\"current-password\"\n placeholder=\"Password\"\n required\n className=\"gr-input\"\n />\n <button type=\"submit\" disabled={isPending} className=\"gr-primary-btn\">\n {isPending ? \"Authenticating…\" : \"Sign In\"}\n </button>\n <button\n type=\"button\"\n onClick={() => setForgotOpen(true)}\n className=\"gr-text-btn\"\n >\n Forgot password?\n </button>\n <button\n type=\"button\"\n className=\"gr-icon-btn gr-text-muted\"\n onClick={exitEditMode}\n title=\"Not editing? Dismiss\"\n aria-label=\"Dismiss\"\n >\n <CloseIcon />\n </button>\n </form>\n </div>\n {linkErrorDrawer ??\n (forgotOpen && <AuthDrawer mode=\"forgot\" onClose={() => setForgotOpen(false)} />)}\n </>\n );\n }\n\n // An invite or password-reset link just completed: they're signed in, but must\n // choose a password before anything else. Ahead of the role check below —\n // getCachedViewer() already claims a pending invite as a side effect of\n // resolving them, so the role is normally set by now; this is purely about the\n // password. Not skippable: a password is the only way back in once this\n // session ends, so the overlay and toolbar wait until one exists.\n if (needsPassword) return <AuthDrawer mode=\"set-password\" />;\n\n // Signed in, but holds no role on this site.\n if (!role) {\n return (\n <>\n <div data-guardrail className=\"gr-toolbar\">\n <span className=\"gr-toolbar-label\">No access to this site</span>\n <span className=\"gr-divider\" />\n <button\n className=\"gr-icon-btn gr-text-muted\"\n onClick={signOut}\n title=\"Sign out\"\n aria-label=\"Sign out\"\n >\n <LogOutIcon />\n </button>\n </div>\n {linkErrorDrawer}\n </>\n );\n }\n\n return (\n <>\n {linkErrorDrawer}\n {teamOpen && <TeamDrawer onClose={() => setTeamOpen(false)} />}\n <div data-guardrail className=\"gr-toolbar\">\n <span className=\"gr-dot\" />\n <span className=\"gr-toolbar-label\">Edit Mode</span>\n <span className=\"gr-divider\" />\n <button\n className=\"gr-icon-btn gr-text-muted gr-size-24\"\n onClick={() => setTeamOpen(true)}\n title=\"Team\"\n aria-label=\"Team\"\n >\n <PeopleIcon />\n </button>\n\n {toolbarExtra && (\n <>\n <span className=\"gr-divider\" />\n {toolbarExtra}\n </>\n )}\n\n <span className=\"gr-divider\" />\n <button\n className=\"gr-icon-btn gr-text-muted\"\n onClick={exitEditMode}\n title=\"Exit edit mode\"\n aria-label=\"Exit edit mode\"\n >\n <LogOutIcon />\n </button>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
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 { PageSettingsDrawer } from \"./PageSettings\";\nimport { AuthDrawer } from \"./AuthDrawer\";\nimport { CloseIcon, LogOutIcon, PeopleIcon, SettingsIcon } from \"./Icons\";\nimport \"./guardrail.css\";\nimport { GR_COOKIES, GR_EDIT_MODE_OFF_TTL_SECONDS } from \"../core/defs\";\nimport type { ActionState } from \"../core/types\";\n\nconst initialState: ActionState = { email: \"\", password: \"\" };\n\n/**\n * Imports the chrome's stylesheet (guardrail.css), which also defines the\n * light/dark theme variables, so every piece of chrome — the sign-in bar, the\n * toolbar, and the field popups portaled to <body> — picks them up from one\n * place. A stylesheet rather than inline styles so the chrome works under a\n * strict Content-Security-Policy.\n */\nexport function GRailAdmin(props: { toolbarExtra?: React.ReactNode }) {\n const { isEditMode } = useGRail();\n if (!isEditMode) return null;\n return <AdminBody {...props} />;\n}\n\nfunction AdminBody({ toolbarExtra }: { toolbarExtra?: React.ReactNode }) {\n const { isAuth, isEditMode, needsPassword, authError, role, auth } = useGRail();\n const [teamOpen, setTeamOpen] = useState(false);\n const [settingsOpen, setSettingsOpen] = useState(false);\n const [forgotOpen, setForgotOpen] = useState(false);\n const [errorDismissed, setErrorDismissed] = 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 // The message for an emailed link that didn't work. The cookie behind it is\n // read on the server, so dismissing clears it there too (and refreshes), but\n // hides it right away without waiting for that.\n const dismissAuthError = () => {\n document.cookie = `${GR_COOKIES.AUTH_ERROR}=; path=/; max-age=0; SameSite=Lax`;\n setErrorDismissed(true);\n router.refresh();\n };\n const linkErrorDrawer =\n authError && !errorDismissed ? (\n <AuthDrawer\n mode=\"forgot\"\n title=\"That link didn't work\"\n message={\n authError === \"expired\"\n ? \"It has expired or was already used. Links work once. Enter your email to get a new one.\"\n : \"We couldn't complete it. Enter your email to get a new link.\"\n }\n onClose={dismissAuthError}\n />\n ) : null;\n\n // Ends the editing session, not the sign-in: the overlay goes away and the\n // person's session stays, so ?edit=true brings it straight back with no\n // credentials. The sign-in form only returns once the session itself is\n // gone (expired, or ended elsewhere). Explicit \"false\" rather than deleting\n // the cookie: with a live session, deleting it would leave edit mode on —\n // see resolveEditMode. router.replace drops ?edit=true from the URL (a bare\n // refresh would leave it there), and refresh re-renders without chrome.\n //\n // Also what the sign-in bar's dismiss button does, for someone who was never\n // authenticated and doesn't want to be here at all.\n const exitEditMode = () => {\n document.cookie = `${GR_COOKIES.EDIT_MODE}=false; path=/; max-age=${GR_EDIT_MODE_OFF_TTL_SECONDS}; SameSite=Lax`;\n router.replace(pathname);\n router.refresh();\n };\n\n // A real sign-out, kept only for the \"no access\" bar below: someone on the\n // wrong account has no other way to switch to the right one. logoutAction()\n // clears the edit-mode cookie itself — a client-side navigation right after\n // it used to race Next's own post-action route refresh and lose.\n const signOut = async () => {\n await logoutAction();\n router.replace(pathname);\n router.refresh();\n };\n\n // No intent signalled — render nothing at all.\n if (!isEditMode) return null;\n\n if (!isAuth) {\n // The adapter cannot take a password here, so hand sign-in off rather than\n // showing a form that could never succeed.\n if (!auth.password) {\n return (\n <>\n <div data-guardrail className=\"gr-banner\">\n <div className=\"gr-banner-form\">\n <div className=\"gr-brand-stack\">\n <span className=\"gr-brand\">Guardrail</span>\n {!auth.signInUrl && (\n <span className=\"gr-fs-12 gr-text-muted\">\n No sign-in method is configured for this site.\n </span>\n )}\n </div>\n {auth.signInUrl && (\n <a href={auth.signInUrl} className=\"gr-primary-btn gr-no-underline\">\n Sign in\n </a>\n )}\n <button\n type=\"button\"\n className=\"gr-icon-btn gr-text-muted\"\n onClick={exitEditMode}\n title=\"Not editing? Dismiss\"\n aria-label=\"Dismiss\"\n >\n <CloseIcon />\n </button>\n </div>\n </div>\n {linkErrorDrawer}\n </>\n );\n }\n\n return (\n <>\n <div data-guardrail className=\"gr-banner\">\n <form action={formAction} className=\"gr-banner-form\">\n <div className=\"gr-brand-stack\">\n <span className=\"gr-brand\">Guardrail</span>\n {formState.error && <span className=\"gr-error\">{formState.error}</span>}\n </div>\n <input\n name=\"email\"\n type=\"email\"\n autoComplete=\"email\"\n placeholder=\"Email\"\n defaultValue={formState.email}\n required\n className=\"gr-input\"\n />\n <input\n name=\"password\"\n type=\"password\"\n autoComplete=\"current-password\"\n placeholder=\"Password\"\n required\n className=\"gr-input\"\n />\n <button type=\"submit\" disabled={isPending} className=\"gr-primary-btn\">\n {isPending ? \"Authenticating…\" : \"Sign In\"}\n </button>\n <button\n type=\"button\"\n onClick={() => setForgotOpen(true)}\n className=\"gr-text-btn\"\n >\n Forgot password?\n </button>\n <button\n type=\"button\"\n className=\"gr-icon-btn gr-text-muted\"\n onClick={exitEditMode}\n title=\"Not editing? Dismiss\"\n aria-label=\"Dismiss\"\n >\n <CloseIcon />\n </button>\n </form>\n </div>\n {linkErrorDrawer ??\n (forgotOpen && <AuthDrawer mode=\"forgot\" onClose={() => setForgotOpen(false)} />)}\n </>\n );\n }\n\n // An invite or password-reset link just completed: they're signed in, but must\n // choose a password before anything else. Ahead of the role check below —\n // getCachedViewer() already claims a pending invite as a side effect of\n // resolving them, so the role is normally set by now; this is purely about the\n // password. Not skippable: a password is the only way back in once this\n // session ends, so the overlay and toolbar wait until one exists.\n if (needsPassword) return <AuthDrawer mode=\"set-password\" />;\n\n // Signed in, but holds no role on this site.\n if (!role) {\n return (\n <>\n <div data-guardrail className=\"gr-toolbar\">\n <span className=\"gr-toolbar-label\">No access to this site</span>\n <span className=\"gr-divider\" />\n <button\n className=\"gr-icon-btn gr-text-muted\"\n onClick={signOut}\n title=\"Sign out\"\n aria-label=\"Sign out\"\n >\n <LogOutIcon />\n </button>\n </div>\n {linkErrorDrawer}\n </>\n );\n }\n\n return (\n <>\n {linkErrorDrawer}\n {teamOpen && <TeamDrawer onClose={() => setTeamOpen(false)} />}\n {settingsOpen && <PageSettingsDrawer onClose={() => setSettingsOpen(false)} />}\n <div data-guardrail className=\"gr-toolbar\">\n <span className=\"gr-dot\" />\n <span className=\"gr-toolbar-label\">Edit Mode</span>\n <span className=\"gr-divider\" />\n <button\n className=\"gr-icon-btn gr-text-muted gr-size-24\"\n onClick={() => setTeamOpen(true)}\n title=\"Team\"\n aria-label=\"Team\"\n >\n <PeopleIcon />\n </button>\n <button\n className=\"gr-icon-btn gr-text-muted gr-size-24\"\n onClick={() => setSettingsOpen(true)}\n title=\"Page settings\"\n aria-label=\"Page settings\"\n >\n <SettingsIcon />\n </button>\n\n {toolbarExtra && (\n <>\n <span className=\"gr-divider\" />\n {toolbarExtra}\n </>\n )}\n\n <span className=\"gr-divider\" />\n <button\n className=\"gr-icon-btn gr-text-muted\"\n onClick={exitEditMode}\n title=\"Exit edit mode\"\n aria-label=\"Exit edit mode\"\n >\n <LogOutIcon />\n </button>\n </div>\n </>\n );\n}\n"],"mappings":";AA0BS,SA4ED,UA5EC,KA+EK,YA/EL;AAxBT,OAAO,SAAS,gBAAgB,WAAW,gBAAgB;AAC3D,SAAS,aAAa,iBAAiB;AACvC,SAAS,aAAa,oBAAoB;AAC1C,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,0BAA0B;AACnC,SAAS,kBAAkB;AAC3B,SAAS,WAAW,YAAY,YAAY,oBAAoB;AAChE,OAAO;AACP,SAAS,YAAY,oCAAoC;AAGzD,MAAM,eAA4B,EAAE,OAAO,IAAI,UAAU,GAAG;AASrD,SAAS,WAAW,OAA2C;AACpE,QAAM,EAAE,WAAW,IAAI,SAAS;AAChC,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO,oBAAC,aAAW,GAAG,OAAO;AAC/B;AAEA,SAAS,UAAU,EAAE,aAAa,GAAuC;AACvE,QAAM,EAAE,QAAQ,YAAY,eAAe,WAAW,MAAM,KAAK,IAAI,SAAS;AAC9E,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AACtD,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAS,KAAK;AAC1D,QAAM,CAAC,WAAW,YAAY,SAAS,IAAI;AAAA,IACzC;AAAA,IACA;AAAA,EACF;AACA,QAAM,SAAS,UAAU;AACzB,QAAM,WAAW,YAAY;AAE7B,YAAU,MAAM;AACd,QAAI,UAAU,QAAS,QAAO,QAAQ;AAAA,EACxC,GAAG,CAAC,UAAU,SAAS,MAAM,CAAC;AAK9B,QAAM,mBAAmB,MAAM;AAC7B,aAAS,SAAS,GAAG,WAAW,UAAU;AAC1C,sBAAkB,IAAI;AACtB,WAAO,QAAQ;AAAA,EACjB;AACA,QAAM,kBACJ,aAAa,CAAC,iBACZ;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,OAAM;AAAA,MACN,SACE,cAAc,YACV,4FACA;AAAA,MAEN,SAAS;AAAA;AAAA,EACX,IACE;AAYN,QAAM,eAAe,MAAM;AACzB,aAAS,SAAS,GAAG,WAAW,SAAS,2BAA2B,4BAA4B;AAChG,WAAO,QAAQ,QAAQ;AACvB,WAAO,QAAQ;AAAA,EACjB;AAMA,QAAM,UAAU,YAAY;AAC1B,UAAM,aAAa;AACnB,WAAO,QAAQ,QAAQ;AACvB,WAAO,QAAQ;AAAA,EACjB;AAGA,MAAI,CAAC,WAAY,QAAO;AAExB,MAAI,CAAC,QAAQ;AAGX,QAAI,CAAC,KAAK,UAAU;AAClB,aACE,iCACE;AAAA,4BAAC,SAAI,kBAAc,MAAC,WAAU,aAC5B,+BAAC,SAAI,WAAU,kBACb;AAAA,+BAAC,SAAI,WAAU,kBACb;AAAA,gCAAC,UAAK,WAAU,YAAW,uBAAS;AAAA,YACnC,CAAC,KAAK,aACL,oBAAC,UAAK,WAAU,0BAAyB,4DAEzC;AAAA,aAEJ;AAAA,UACC,KAAK,aACJ,oBAAC,OAAE,MAAM,KAAK,WAAW,WAAU,kCAAiC,qBAEpE;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,WAAU;AAAA,cACV,SAAS;AAAA,cACT,OAAM;AAAA,cACN,cAAW;AAAA,cAEX,8BAAC,aAAU;AAAA;AAAA,UACb;AAAA,WACF,GACF;AAAA,QACC;AAAA,SACH;AAAA,IAEJ;AAEA,WACE,iCACE;AAAA,0BAAC,SAAI,kBAAc,MAAC,WAAU,aAC5B,+BAAC,UAAK,QAAQ,YAAY,WAAU,kBAClC;AAAA,6BAAC,SAAI,WAAU,kBACb;AAAA,8BAAC,UAAK,WAAU,YAAW,uBAAS;AAAA,UACnC,UAAU,SAAS,oBAAC,UAAK,WAAU,YAAY,oBAAU,OAAM;AAAA,WAClE;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,MAAK;AAAA,YACL,cAAa;AAAA,YACb,aAAY;AAAA,YACZ,cAAc,UAAU;AAAA,YACxB,UAAQ;AAAA,YACR,WAAU;AAAA;AAAA,QACZ;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,MAAK;AAAA,YACL,cAAa;AAAA,YACb,aAAY;AAAA,YACZ,UAAQ;AAAA,YACR,WAAU;AAAA;AAAA,QACZ;AAAA,QACA,oBAAC,YAAO,MAAK,UAAS,UAAU,WAAW,WAAU,kBAClD,sBAAY,yBAAoB,WACnC;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM,cAAc,IAAI;AAAA,YACjC,WAAU;AAAA,YACX;AAAA;AAAA,QAED;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAU;AAAA,YACV,SAAS;AAAA,YACT,OAAM;AAAA,YACN,cAAW;AAAA,YAEX,8BAAC,aAAU;AAAA;AAAA,QACb;AAAA,SACF,GACF;AAAA,MACC,oBACE,cAAc,oBAAC,cAAW,MAAK,UAAS,SAAS,MAAM,cAAc,KAAK,GAAG;AAAA,OAClF;AAAA,EAEJ;AAQA,MAAI,cAAe,QAAO,oBAAC,cAAW,MAAK,gBAAe;AAG1D,MAAI,CAAC,MAAM;AACT,WACE,iCACE;AAAA,2BAAC,SAAI,kBAAc,MAAC,WAAU,cAC5B;AAAA,4BAAC,UAAK,WAAU,oBAAmB,oCAAsB;AAAA,QACzD,oBAAC,UAAK,WAAU,cAAa;AAAA,QAC7B;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,SAAS;AAAA,YACT,OAAM;AAAA,YACN,cAAW;AAAA,YAEX,8BAAC,cAAW;AAAA;AAAA,QACd;AAAA,SACF;AAAA,MACC;AAAA,OACH;AAAA,EAEJ;AAEA,SACE,iCACG;AAAA;AAAA,IACA,YAAY,oBAAC,cAAW,SAAS,MAAM,YAAY,KAAK,GAAG;AAAA,IAC3D,gBAAgB,oBAAC,sBAAmB,SAAS,MAAM,gBAAgB,KAAK,GAAG;AAAA,IAC5E,qBAAC,SAAI,kBAAc,MAAC,WAAU,cAC5B;AAAA,0BAAC,UAAK,WAAU,UAAS;AAAA,MACzB,oBAAC,UAAK,WAAU,oBAAmB,uBAAS;AAAA,MAC5C,oBAAC,UAAK,WAAU,cAAa;AAAA,MAC7B;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,SAAS,MAAM,YAAY,IAAI;AAAA,UAC/B,OAAM;AAAA,UACN,cAAW;AAAA,UAEX,8BAAC,cAAW;AAAA;AAAA,MACd;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,SAAS,MAAM,gBAAgB,IAAI;AAAA,UACnC,OAAM;AAAA,UACN,cAAW;AAAA,UAEX,8BAAC,gBAAa;AAAA;AAAA,MAChB;AAAA,MAEC,gBACC,iCACE;AAAA,4BAAC,UAAK,WAAU,cAAa;AAAA,QAC5B;AAAA,SACH;AAAA,MAGF,oBAAC,UAAK,WAAU,cAAa;AAAA,MAC7B;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,SAAS;AAAA,UACT,OAAM;AAAA,UACN,cAAW;AAAA,UAEX,8BAAC,cAAW;AAAA;AAAA,MACd;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -10,6 +10,7 @@ import { useGRail } from "../Provider";
|
|
|
10
10
|
import { useAnchorRect } from "../useAnchorRect";
|
|
11
11
|
import { CheckIcon, CloseIcon, OpenIcon, PencilIcon, RevertIcon, SpinnerIcon } from "../Icons";
|
|
12
12
|
import { openHref } from "../openHref";
|
|
13
|
+
import { revertLabel } from "../revert";
|
|
13
14
|
import { MediaPicker } from "../MediaPicker";
|
|
14
15
|
function GRailClient({
|
|
15
16
|
as,
|
|
@@ -226,16 +227,6 @@ function ImageMarker({
|
|
|
226
227
|
)
|
|
227
228
|
] });
|
|
228
229
|
}
|
|
229
|
-
function revertLabel(info) {
|
|
230
|
-
if (info.kind === "previous" && info.at) {
|
|
231
|
-
const when = new Date(info.at);
|
|
232
|
-
if (!Number.isNaN(when.getTime())) {
|
|
233
|
-
return `Revert to the version from ${when.toLocaleDateString(void 0, { month: "short", day: "numeric" })}`;
|
|
234
|
-
}
|
|
235
|
-
return "Revert to the previous version";
|
|
236
|
-
}
|
|
237
|
-
return "Reset to default";
|
|
238
|
-
}
|
|
239
230
|
function Chrome({
|
|
240
231
|
rect,
|
|
241
232
|
isInline,
|