@guard-rail/core 0.1.0 → 0.2.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 CHANGED
@@ -25,7 +25,7 @@ pnpm add @guard-rail/core @supabase/ssr @supabase/supabase-js
25
25
 
26
26
  Set up the database:
27
27
 
28
- 1. Copy `node_modules/guardrail/supabase/migrations/*.sql` into your own
28
+ 1. Copy `node_modules/@guard-rail/core/supabase/migrations/*.sql` into your own
29
29
  project's `supabase/migrations/`, then run `supabase db push`. No Supabase
30
30
  CLI yet? Paste the file's contents into the SQL editor instead — it runs
31
31
  the same statements, just without the CLI's version tracking.
@@ -44,6 +44,37 @@ SUPABASE_ANON_KEY=...
44
44
  GUARDRAIL_SITE_ID=my-site # optional; defaults to "default"
45
45
  ```
46
46
 
47
+ ### Many sites, one Supabase project
48
+
49
+ Every table above is keyed by `site_id`, and every RLS policy checks a role
50
+ *for that specific site_id* — this isn't a convenience layer on top of
51
+ separate installs, it's how the schema is built. A migration, its Exposed
52
+ Schemas setting, and the default storage bucket are one-time, per-*project*
53
+ setup, not per-site.
54
+
55
+ So running several sites against a single Supabase project is a supported,
56
+ intended pattern, not a workaround — useful while a site is small, in
57
+ testing, or just doesn't warrant its own project yet. For the second and
58
+ later sites on an already-set-up project:
59
+
60
+ 1. Install the package as usual. Skip the migration and the Exposed Schemas
61
+ step entirely — both already apply to every site on that project.
62
+ 2. Point `SUPABASE_URL` / `SUPABASE_ANON_KEY` at the same project, and set a
63
+ new, unique `GUARDRAIL_SITE_ID`.
64
+ 3. Seed a bootstrap admin row for that new `site_id` — membership is also
65
+ per-site, so an existing admin on one site has no role on another until
66
+ added there specifically.
67
+
68
+ **One tradeoff worth knowing:** `gr_content` and `gr_repeater_items` are
69
+ readable by anyone (`using (true)`), not scoped by `site_id` — necessary
70
+ since anonymous visitors must be able to read a site's content to render it,
71
+ and there's no draft/unpublished state to protect in the first place. But it
72
+ does mean any one site's anon key can read every other site's content rows
73
+ directly through the REST API, not just through its own rendered pages. Low
74
+ stakes for content that's already meant to be public on its own site, but
75
+ worth factoring in before treating a shared project as a hard tenant
76
+ boundary.
77
+
47
78
  ## Usage
48
79
 
49
80
  Wrap the root layout:
@@ -129,8 +160,21 @@ dashboard, plus one route in your app:
129
160
  **Bootstrapping.** A site with no members has nobody who can add members, and
130
161
  there is deliberately no in-app way to grant yourself a role — that would mean
131
162
  an RLS policy letting a user write their own membership row, which is the same
132
- as letting anyone make themselves an admin. Seed the first admin with the SQL
133
- at the bottom of the init migration.
163
+ as letting anyone make themselves an admin. Seed the first admin with the
164
+ bundled command instead of hand-writing SQL:
165
+
166
+ ```bash
167
+ SERVICE_KEY=... npx guardrail-seed-user \
168
+ --email=you@example.com --password=... --site-id=my-site --role=admin
169
+ ```
170
+
171
+ Creates the account if it doesn't exist (or reuses it if it does — the same
172
+ person can hold different roles on different sites) via Supabase's own Admin
173
+ API, then writes the membership row. Needs the **service_role** key, not the
174
+ anon key, since it bypasses RLS by design — Settings > API in the dashboard.
175
+ Pass it inline for this one command rather than storing it anywhere; nothing
176
+ else in Guardrail ever needs it. Prefer raw SQL instead? The same insert is
177
+ still documented at the bottom of the init migration.
134
178
 
135
179
  The bundled drawer lives behind the toolbar's **Team** button. To build your
136
180
  own instead, `listTeam`, `inviteMember`, `updateMemberRole`, and `removeMember`
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ import { createClient } from "@supabase/supabase-js";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { GR_SCHEMA, GR_MEMBERS_TABLE } from "../core/defs.js";
6
+ function readEnvFile(key) {
7
+ for (const file of [".env.local", ".env"]) {
8
+ const p = path.join(process.cwd(), file);
9
+ if (!existsSync(p)) continue;
10
+ const match = readFileSync(p, "utf8").match(new RegExp(`^${key}=(.*)$`, "m"));
11
+ if (match?.[1]) return match[1].trim().replace(/^['"]|['"]$/g, "");
12
+ }
13
+ return void 0;
14
+ }
15
+ function parseArgs(argv) {
16
+ const out = {};
17
+ for (const arg of argv) {
18
+ const m = arg.match(/^--([^=]+)=([\s\S]*)$/);
19
+ const [, key, value] = m ?? [];
20
+ if (key !== void 0 && value !== void 0) out[key] = value;
21
+ }
22
+ return out;
23
+ }
24
+ function fail(message) {
25
+ console.error(`
26
+ ${message}
27
+ `);
28
+ process.exit(1);
29
+ }
30
+ const USAGE = `Usage:
31
+ guardrail-seed-user --email=<email> --password=<password> [--site-id=<site>] [--role=admin|editor]
32
+
33
+ --site-id defaults to GUARDRAIL_SITE_ID from .env.local/.env if not passed.
34
+ --role defaults to "admin".
35
+
36
+ Requires SUPABASE_URL (also read from .env.local/.env) and
37
+ SERVICE_KEY in the environment \u2014 the service_role key, not the
38
+ anon key (Settings > API in your Supabase dashboard). Pass it inline rather
39
+ than storing it anywhere:
40
+
41
+ SERVICE_KEY=... guardrail-seed-user --email=you@example.com --password=...`;
42
+ async function findUserByEmail(supabase, email) {
43
+ const perPage = 1e3;
44
+ for (let page = 1; page <= 20; page++) {
45
+ const { data, error } = await supabase.auth.admin.listUsers({ page, perPage });
46
+ if (error || !data) return null;
47
+ const match = data.users.find((u) => u.email?.toLowerCase() === email.toLowerCase());
48
+ if (match) return match;
49
+ if (data.users.length < perPage) return null;
50
+ }
51
+ return null;
52
+ }
53
+ async function main() {
54
+ const args = parseArgs(process.argv.slice(2));
55
+ const email = args.email;
56
+ const password = args.password;
57
+ const siteId = args["site-id"] ?? readEnvFile("GUARDRAIL_SITE_ID");
58
+ const role = args.role ?? "admin";
59
+ if (!email || !password || !siteId) fail(USAGE);
60
+ if (role !== "admin" && role !== "editor") {
61
+ fail(`--role must be "admin" or "editor", got "${role}"
62
+
63
+ ${USAGE}`);
64
+ }
65
+ const url = process.env.SUPABASE_URL ?? readEnvFile("SUPABASE_URL");
66
+ const serviceRoleKey = process.env.SERVICE_KEY;
67
+ if (!url || !serviceRoleKey) fail(`SUPABASE_URL and SERVICE_KEY must both be set.
68
+
69
+ ${USAGE}`);
70
+ const supabase = createClient(url, serviceRoleKey, {
71
+ auth: { autoRefreshToken: false, persistSession: false }
72
+ });
73
+ let userId;
74
+ const { data: created, error: createError } = await supabase.auth.admin.createUser({
75
+ email,
76
+ password,
77
+ email_confirm: true
78
+ });
79
+ if (created?.user) {
80
+ userId = created.user.id;
81
+ console.log(`Created auth user ${email}.`);
82
+ } else if (createError && /already.*(registered|exists)/i.test(createError.message)) {
83
+ const existing = await findUserByEmail(supabase, email);
84
+ if (!existing) fail(`${email} already exists but could not be looked up: ${createError.message}`);
85
+ userId = existing.id;
86
+ console.log(`${email} already exists \u2014 reusing that account.`);
87
+ } else {
88
+ fail(`Could not create or find ${email}: ${createError?.message ?? "unknown error"}`);
89
+ }
90
+ const { error: memberError } = await supabase.schema(GR_SCHEMA).from(GR_MEMBERS_TABLE).upsert(
91
+ { site_id: siteId, email, user_id: userId, role, status: "active" },
92
+ { onConflict: "site_id,email" }
93
+ );
94
+ if (memberError) {
95
+ fail(`Created/found the auth user, but couldn't write the membership row: ${memberError.message}`);
96
+ }
97
+ console.log(`
98
+ ${email} is now "${role}" on site "${siteId}". Sign in at /?edit=true.
99
+ `);
100
+ }
101
+ main().catch((err) => {
102
+ console.error(err);
103
+ process.exit(1);
104
+ });
105
+ //# sourceMappingURL=seed-user.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/cli/seed-user.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Bootstraps (or re-uses) a real auth user and gives them a role on a site,\n * without hand-writing SQL against auth.users — the escape hatch documented\n * at the bottom of the init migration for exactly this \"a site with no\n * members has no in-app way to grant itself a role\" problem.\n *\n * Uses Supabase's own Admin API (auth.admin.createUser), not a direct\n * auth.users insert — the crypt()/gen_salt() trick create-guardrail's local\n * Docker seed uses is fine for a throwaway local database, but this command\n * runs against real projects, so it goes through the supported path instead.\n *\n * That needs the service_role key, not the anon key — a meaningfully more\n * powerful credential than anything else in Guardrail touches, since it\n * bypasses RLS entirely. Deliberately read from the environment for this one\n * invocation rather than accepted as a flag (shell history, process list) or\n * expected in .env.local (where the running app would have no reason to\n * ever need it).\n */\nimport { createClient, type SupabaseClient } from \"@supabase/supabase-js\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport path from \"node:path\";\n// Explicit .js extensions here (unlike the rest of this package) because\n// this file, unlike everything else in it, is executed directly by plain\n// `node` rather than only ever consumed through a bundler — Node's native\n// ESM loader has no automatic extension resolution for relative imports,\n// confirmed by running the compiled output directly and hitting\n// ERR_MODULE_NOT_FOUND without them.\nimport { GR_SCHEMA, GR_MEMBERS_TABLE } from \"../core/defs.js\";\nimport type { GRRole } from \"../core/types.js\";\n\n/**\n * Reads just SUPABASE_URL and GUARDRAIL_SITE_ID out of .env.local/.env in\n * the current directory — not a general dotenv loader, and deliberately\n * never touches SERVICE_KEY: that one stays inline-only (see\n * the file-level comment on why), even if a file happened to contain it.\n */\nfunction readEnvFile(key: string): string | undefined {\n for (const file of [\".env.local\", \".env\"]) {\n const p = path.join(process.cwd(), file);\n if (!existsSync(p)) continue;\n const match = readFileSync(p, \"utf8\").match(new RegExp(`^${key}=(.*)$`, \"m\"));\n if (match?.[1]) return match[1].trim().replace(/^['\"]|['\"]$/g, \"\");\n }\n return undefined;\n}\n\nfunction parseArgs(argv: string[]): Record<string, string> {\n const out: Record<string, string> = {};\n for (const arg of argv) {\n const m = arg.match(/^--([^=]+)=([\\s\\S]*)$/);\n const [, key, value] = m ?? [];\n if (key !== undefined && value !== undefined) out[key] = value;\n }\n return out;\n}\n\nfunction fail(message: string): never {\n console.error(`\\n${message}\\n`);\n process.exit(1);\n}\n\nconst USAGE = `Usage:\n guardrail-seed-user --email=<email> --password=<password> [--site-id=<site>] [--role=admin|editor]\n\n--site-id defaults to GUARDRAIL_SITE_ID from .env.local/.env if not passed.\n--role defaults to \"admin\".\n\nRequires SUPABASE_URL (also read from .env.local/.env) and\nSERVICE_KEY in the environment — the service_role key, not the\nanon key (Settings > API in your Supabase dashboard). Pass it inline rather\nthan storing it anywhere:\n\n SERVICE_KEY=... guardrail-seed-user --email=you@example.com --password=...`;\n\nasync function findUserByEmail(supabase: SupabaseClient, email: string) {\n // The admin API has no getUserByEmail — paginate and match client-side.\n // Fine at this tool's scale (a handful of team members per site, not a\n // consumer user base).\n const perPage = 1000;\n for (let page = 1; page <= 20; page++) {\n const { data, error } = await supabase.auth.admin.listUsers({ page, perPage });\n if (error || !data) return null;\n const match = data.users.find((u) => u.email?.toLowerCase() === email.toLowerCase());\n if (match) return match;\n if (data.users.length < perPage) return null; // last page\n }\n return null;\n}\n\nasync function main() {\n const args = parseArgs(process.argv.slice(2));\n const email = args.email;\n const password = args.password;\n const siteId = args[\"site-id\"] ?? readEnvFile(\"GUARDRAIL_SITE_ID\");\n const role = (args.role ?? \"admin\") as GRRole;\n\n if (!email || !password || !siteId) fail(USAGE);\n if (role !== \"admin\" && role !== \"editor\") {\n fail(`--role must be \"admin\" or \"editor\", got \"${role}\"\\n\\n${USAGE}`);\n }\n\n const url = process.env.SUPABASE_URL ?? readEnvFile(\"SUPABASE_URL\");\n const serviceRoleKey = process.env.SERVICE_KEY;\n if (!url || !serviceRoleKey) fail(`SUPABASE_URL and SERVICE_KEY must both be set.\\n\\n${USAGE}`);\n\n const supabase = createClient(url, serviceRoleKey, {\n auth: { autoRefreshToken: false, persistSession: false },\n });\n\n let userId: string;\n const { data: created, error: createError } = await supabase.auth.admin.createUser({\n email,\n password,\n email_confirm: true,\n });\n\n if (created?.user) {\n userId = created.user.id;\n console.log(`Created auth user ${email}.`);\n } else if (createError && /already.*(registered|exists)/i.test(createError.message)) {\n const existing = await findUserByEmail(supabase, email);\n if (!existing) fail(`${email} already exists but could not be looked up: ${createError.message}`);\n userId = existing.id;\n console.log(`${email} already exists — reusing that account.`);\n } else {\n fail(`Could not create or find ${email}: ${createError?.message ?? \"unknown error\"}`);\n }\n\n const { error: memberError } = await supabase\n .schema(GR_SCHEMA)\n .from(GR_MEMBERS_TABLE)\n .upsert(\n { site_id: siteId, email, user_id: userId, role, status: \"active\" },\n { onConflict: \"site_id,email\" },\n );\n\n if (memberError) {\n fail(`Created/found the auth user, but couldn't write the membership row: ${memberError.message}`);\n }\n\n console.log(`\\n${email} is now \"${role}\" on site \"${siteId}\". Sign in at /?edit=true.\\n`);\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
@@ -29,13 +29,18 @@ function GRailAdmin({ toolbarExtra }) {
29
29
  if (!isAuth) {
30
30
  if (!auth.password) {
31
31
  return /* @__PURE__ */ jsx("div", { "data-guardrail": true, style: S.banner, children: /* @__PURE__ */ jsxs("div", { style: S.bannerForm, children: [
32
- /* @__PURE__ */ jsx("span", { style: S.brand, children: "Guardrail" }),
33
- auth.signInUrl ? /* @__PURE__ */ jsx("a", { href: auth.signInUrl, style: { ...S.primaryBtn, textDecoration: "none" }, children: "Sign in" }) : /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: "#6b7280" }, children: "No sign-in method is configured for this site." })
32
+ /* @__PURE__ */ jsxs("div", { style: S.brandStack, children: [
33
+ /* @__PURE__ */ jsx("span", { style: S.brand, children: "Guardrail" }),
34
+ !auth.signInUrl && /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: "#6b7280" }, children: "No sign-in method is configured for this site." })
35
+ ] }),
36
+ auth.signInUrl && /* @__PURE__ */ jsx("a", { href: auth.signInUrl, style: { ...S.primaryBtn, textDecoration: "none" }, children: "Sign in" })
34
37
  ] }) });
35
38
  }
36
39
  return /* @__PURE__ */ jsx("div", { "data-guardrail": true, style: S.banner, children: /* @__PURE__ */ jsxs("form", { action: formAction, style: S.bannerForm, children: [
37
- /* @__PURE__ */ jsx("span", { style: S.brand, children: "Guardrail" }),
38
- formState.error && /* @__PURE__ */ jsx("span", { style: S.error, children: formState.error }),
40
+ /* @__PURE__ */ jsxs("div", { style: S.brandStack, children: [
41
+ /* @__PURE__ */ jsx("span", { style: S.brand, children: "Guardrail" }),
42
+ formState.error && /* @__PURE__ */ jsx("span", { style: S.error, children: formState.error })
43
+ ] }),
39
44
  /* @__PURE__ */ jsx(
40
45
  "input",
41
46
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/Admin.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useActionState, useEffect, useState } from \"react\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport { loginAction, logoutAction } from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { TeamDrawer } from \"./Team\";\nimport { LogOutIcon, PeopleIcon } from \"./Icons\";\nimport { S } from \"./styles\";\nimport type { ActionState } from \"../core/types\";\n\nconst initialState: ActionState = { email: \"\", password: \"\" };\n\nexport function GRailAdmin({ toolbarExtra }: { toolbarExtra?: React.ReactNode }) {\n const { isAuth, isEditMode, role, auth } = useGRail();\n const [teamOpen, setTeamOpen] = useState(false);\n const [formState, formAction, isPending] = useActionState(\n loginAction,\n initialState,\n );\n const router = useRouter();\n const pathname = usePathname();\n\n useEffect(() => {\n if (formState.success) router.refresh();\n }, [formState.success, router]);\n\n const signOut = async () => {\n await logoutAction();\n document.cookie =\n \"gr_edit_mode=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT\";\n window.location.href = pathname;\n };\n\n // No intent signalled — render nothing at all.\n if (!isEditMode) return null;\n\n if (!isAuth) {\n // The adapter cannot take a password here, so hand sign-in off rather than\n // showing a form that could never succeed.\n if (!auth.password) {\n return (\n <div data-guardrail style={S.banner}>\n <div style={S.bannerForm}>\n <span style={S.brand}>Guardrail</span>\n {auth.signInUrl ? (\n <a href={auth.signInUrl} style={{ ...S.primaryBtn, textDecoration: \"none\" }}>\n Sign in\n </a>\n ) : (\n <span style={{ fontSize: 12, color: \"#6b7280\" }}>\n No sign-in method is configured for this site.\n </span>\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 <span style={S.brand}>Guardrail</span>\n {formState.error && <span style={S.error}>{formState.error}</span>}\n <input\n name=\"email\"\n type=\"email\"\n placeholder=\"Email\"\n defaultValue={formState.email}\n required\n style={S.input}\n />\n <input\n name=\"password\"\n type=\"password\"\n placeholder=\"Password\"\n required\n style={S.input}\n />\n <button type=\"submit\" disabled={isPending} style={S.primaryBtn}>\n {isPending ? \"Authenticating…\" : \"Sign In\"}\n </button>\n </form>\n </div>\n );\n }\n\n // Signed in, but holds no role on this site.\n if (!role) {\n return (\n <div data-guardrail style={S.toolbar}>\n <span style={S.toolbarLabel}>No access to this site</span>\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: \"#9ca3af\" }}\n onClick={signOut}\n title=\"Log out\"\n aria-label=\"Log out\"\n >\n <LogOutIcon />\n </button>\n </div>\n );\n }\n\n return (\n <>\n {teamOpen && <TeamDrawer onClose={() => setTeamOpen(false)} />}\n <div data-guardrail style={S.toolbar}>\n <span style={S.dot} />\n <span style={S.toolbarLabel}>Edit Mode</span>\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: \"#9ca3af\", width: 24, height: 24 }}\n onClick={() => setTeamOpen(true)}\n title=\"Team\"\n aria-label=\"Team\"\n >\n <PeopleIcon />\n </button>\n\n {toolbarExtra && (\n <>\n <span style={S.divider} />\n {toolbarExtra}\n </>\n )}\n\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: \"#9ca3af\" }}\n onClick={signOut}\n title=\"Log out\"\n aria-label=\"Log out\"\n >\n <LogOutIcon />\n </button>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/react/Admin.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useActionState, useEffect, useState } from \"react\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport { loginAction, logoutAction } from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { TeamDrawer } from \"./Team\";\nimport { LogOutIcon, PeopleIcon } from \"./Icons\";\nimport { S } from \"./styles\";\nimport type { ActionState } from \"../core/types\";\n\nconst initialState: ActionState = { email: \"\", password: \"\" };\n\nexport function GRailAdmin({ toolbarExtra }: { toolbarExtra?: React.ReactNode }) {\n const { isAuth, isEditMode, role, auth } = useGRail();\n const [teamOpen, setTeamOpen] = useState(false);\n const [formState, formAction, isPending] = useActionState(\n loginAction,\n initialState,\n );\n const router = useRouter();\n const pathname = usePathname();\n\n useEffect(() => {\n if (formState.success) router.refresh();\n }, [formState.success, router]);\n\n const signOut = async () => {\n await logoutAction();\n document.cookie =\n \"gr_edit_mode=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT\";\n window.location.href = pathname;\n };\n\n // No intent signalled — render nothing at all.\n if (!isEditMode) return null;\n\n if (!isAuth) {\n // The adapter cannot take a password here, so hand sign-in off rather than\n // showing a form that could never succeed.\n if (!auth.password) {\n return (\n <div data-guardrail style={S.banner}>\n <div style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {!auth.signInUrl && (\n <span style={{ fontSize: 12, color: \"#6b7280\" }}>\n No sign-in method is configured for this site.\n </span>\n )}\n </div>\n {auth.signInUrl && (\n <a href={auth.signInUrl} style={{ ...S.primaryBtn, textDecoration: \"none\" }}>\n Sign in\n </a>\n )}\n </div>\n </div>\n );\n }\n\n return (\n <div data-guardrail style={S.banner}>\n <form action={formAction} style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {formState.error && <span style={S.error}>{formState.error}</span>}\n </div>\n <input\n name=\"email\"\n type=\"email\"\n placeholder=\"Email\"\n defaultValue={formState.email}\n required\n style={S.input}\n />\n <input\n name=\"password\"\n type=\"password\"\n placeholder=\"Password\"\n required\n style={S.input}\n />\n <button type=\"submit\" disabled={isPending} style={S.primaryBtn}>\n {isPending ? \"Authenticating…\" : \"Sign In\"}\n </button>\n </form>\n </div>\n );\n }\n\n // Signed in, but holds no role on this site.\n if (!role) {\n return (\n <div data-guardrail style={S.toolbar}>\n <span style={S.toolbarLabel}>No access to this site</span>\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: \"#9ca3af\" }}\n onClick={signOut}\n title=\"Log out\"\n aria-label=\"Log out\"\n >\n <LogOutIcon />\n </button>\n </div>\n );\n }\n\n return (\n <>\n {teamOpen && <TeamDrawer onClose={() => setTeamOpen(false)} />}\n <div data-guardrail style={S.toolbar}>\n <span style={S.dot} />\n <span style={S.toolbarLabel}>Edit Mode</span>\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: \"#9ca3af\", width: 24, height: 24 }}\n onClick={() => setTeamOpen(true)}\n title=\"Team\"\n aria-label=\"Team\"\n >\n <PeopleIcon />\n </button>\n\n {toolbarExtra && (\n <>\n <span style={S.divider} />\n {toolbarExtra}\n </>\n )}\n\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: \"#9ca3af\" }}\n onClick={signOut}\n title=\"Log out\"\n aria-label=\"Log out\"\n >\n <LogOutIcon />\n </button>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import React, { useRef, useState } from "react";
3
+ import React, { useEffect, useRef, useState } from "react";
4
4
  import { saveOverride } from "../../server/actions";
5
5
  import { GRailRender } from "./render";
6
6
  import { useGRail } from "../Provider";
@@ -22,9 +22,6 @@ function GRailClient({
22
22
  const rect = useAnchorRect(elementRef, editing);
23
23
  const sessionTextRef = useRef(initialOverride.value.text ?? "");
24
24
  const draftTextRef = useRef(initialOverride.value.text ?? "");
25
- if (!isEditMode) {
26
- return /* @__PURE__ */ jsx(GRailRender, { as, ...htmlProps, override: row.value });
27
- }
28
25
  const isInline = row.value.type === "text" || row.value.type === "button";
29
26
  const beginEdit = () => {
30
27
  sessionTextRef.current = row.value.text ?? "";
@@ -56,6 +53,20 @@ function GRailClient({
56
53
  setError(null);
57
54
  setEditing(false);
58
55
  };
56
+ useEffect(() => {
57
+ if (!editing) return;
58
+ const handlePointerDown = (e) => {
59
+ const target = e.target;
60
+ if (elementRef.current?.contains(target)) return;
61
+ if (target instanceof Element && target.closest("[data-guardrail]")) return;
62
+ handleCancel();
63
+ };
64
+ document.addEventListener("mousedown", handlePointerDown);
65
+ return () => document.removeEventListener("mousedown", handlePointerDown);
66
+ }, [editing]);
67
+ if (!isEditMode) {
68
+ return /* @__PURE__ */ jsx(GRailRender, { as, ...htmlProps, override: row.value });
69
+ }
59
70
  const renderedValue = editing && isInline ? { ...row.value, text: sessionTextRef.current } : row.value;
60
71
  return /* @__PURE__ */ jsxs(Fragment, { children: [
61
72
  /* @__PURE__ */ jsx(
@@ -69,7 +80,19 @@ function GRailClient({
69
80
  ...htmlProps.style,
70
81
  outline: editing ? "2px solid #2563eb" : "1px dashed #60a5fa",
71
82
  outlineOffset: 2,
72
- cursor: editing ? "text" : "pointer"
83
+ cursor: editing ? "text" : "pointer",
84
+ ...row.value.type === "image" && {
85
+ // A decorative sibling stacked above the image (a hero's
86
+ // text/gradient scrim, say) covers the outline too — outline is
87
+ // part of this element's own paint, not a separate layer, so it
88
+ // can't out-stack a sibling on its own no matter how "on top"
89
+ // that sounds. `position` is required for z-index to apply at
90
+ // all on a normally static <img> — skipped for `fill` images,
91
+ // which next/image already positions absolutely internally;
92
+ // overriding that would break fill's own layout.
93
+ position: htmlProps.fill ? void 0 : "relative",
94
+ zIndex: 9998
95
+ }
73
96
  },
74
97
  onClick: (e) => {
75
98
  if (editing) return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/react/GRail/client.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useRef, useState } from \"react\";\nimport { saveOverride } from \"../../server/actions\";\nimport { GRailRender } from \"./render\";\nimport { useGRail } from \"../Provider\";\nimport { useAnchorRect } from \"../useAnchorRect\";\nimport { CheckIcon, CloseIcon, SpinnerIcon } from \"../Icons\";\nimport { S } from \"../styles\";\nimport { MediaPicker } from \"../MediaPicker\";\nimport type { HTMLKeys, OverrideRow, OverrideValue } from \"../../core/types\";\n\nexport function GRailClient({\n as,\n initialOverride,\n ...htmlProps\n}: {\n as?: HTMLKeys;\n initialOverride: OverrideRow;\n [key: string]: unknown;\n}) {\n const { isEditMode } = useGRail();\n const [editing, setEditing] = useState(false);\n const [saving, setSaving] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [row, setRow] = useState<OverrideRow>(initialOverride);\n\n const elementRef = useRef<HTMLElement | null>(null);\n const rect = useAnchorRect(elementRef, editing);\n\n /**\n * The text the element was rendered with when this edit session began.\n *\n * While editing, the DOM owns the text and React must not touch it. Feeding\n * state back into a contentEditable's children makes React replace the text\n * node on every keystroke, which resets the caret to offset 0 — typing then\n * appears reversed and backspace jumps to the start. Holding this value\n * constant means the children prop never changes, so React leaves the node\n * alone and the caret stays put.\n */\n const sessionTextRef = useRef<string>(initialOverride.value.text ?? \"\");\n /** Live DOM text, updated without re-rendering. Read on save. */\n const draftTextRef = useRef<string>(initialOverride.value.text ?? \"\");\n\n if (!isEditMode) {\n return <GRailRender as={as} {...htmlProps} override={row.value} />;\n }\n\n const isInline = row.value.type === \"text\" || row.value.type === \"button\";\n\n const beginEdit = () => {\n sessionTextRef.current = row.value.text ?? \"\";\n draftTextRef.current = row.value.text ?? \"\";\n setError(null);\n setEditing(true);\n };\n\n const updateAttribute = (key: string, value: string) =>\n setRow((prev) => ({ ...prev, value: { ...prev.value, [key]: value } }));\n\n const patchValue = (patch: Partial<OverrideValue>) =>\n setRow((prev) => ({ ...prev, value: { ...prev.value, ...patch } }));\n\n const handleSave = async () => {\n // Inline text lives in the DOM, not in state, so read it back here.\n const next: OverrideRow = isInline\n ? { ...row, value: { ...row.value, text: draftTextRef.current } }\n : row;\n\n setSaving(true);\n setError(null);\n const res = await saveOverride(next);\n setSaving(false);\n\n if (!res.success) {\n setError(res.error ?? \"Save failed.\");\n return;\n }\n setRow(next);\n setEditing(false);\n };\n\n const handleCancel = () => {\n setRow(initialOverride);\n draftTextRef.current = initialOverride.value.text ?? \"\";\n if (isInline && elementRef.current) {\n elementRef.current.textContent = initialOverride.value.text ?? \"\";\n }\n setError(null);\n setEditing(false);\n };\n\n // Frozen during the session so React never rewrites the live text node.\n const renderedValue =\n editing && isInline\n ? { ...row.value, text: sessionTextRef.current }\n : row.value;\n\n return (\n <>\n <GRailRender\n as={as}\n {...htmlProps}\n ref={elementRef}\n override={renderedValue}\n style={{\n ...(htmlProps.style as React.CSSProperties),\n outline: editing ? \"2px solid #2563eb\" : \"1px dashed #60a5fa\",\n outlineOffset: 2,\n cursor: editing ? \"text\" : \"pointer\",\n }}\n onClick={(e: React.MouseEvent) => {\n if (editing) return;\n e.preventDefault();\n // preventDefault only blocks the browser's native action (following\n // an href); it does not stop the click from bubbling. A field\n // rendered inside a Link would still trigger the Link's own onClick\n // and navigate anyway — stopPropagation is what actually claims the\n // click for editing instead. (Use `editable={false}` on fields\n // meant to stay pure navigation, e.g. inside a card that links to\n // that content's own page.)\n e.stopPropagation();\n beginEdit();\n }}\n // Spread only for inline text: images and links would receive invalid\n // DOM props (contentEditable on next/image, etc).\n {...(isInline\n ? {\n contentEditable: editing,\n suppressContentEditableWarning: true,\n onInput: (e: React.FormEvent<HTMLElement>) => {\n // Ref only — setState here re-renders and moves the caret.\n draftTextRef.current = e.currentTarget.textContent ?? \"\";\n },\n onKeyDown: (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n void handleSave();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n handleCancel();\n }\n },\n }\n : {})}\n />\n\n {editing && rect && (\n <Chrome\n rect={rect}\n isInline={isInline}\n row={row}\n error={error}\n saving={saving}\n onChange={updateAttribute}\n onPatch={patchValue}\n onSave={handleSave}\n onCancel={handleCancel}\n />\n )}\n </>\n );\n}\n\nfunction Chrome({\n rect,\n isInline,\n row,\n error,\n saving,\n onChange,\n onPatch,\n onSave,\n onCancel,\n}: {\n rect: { top: number; left: number; bottom: number; width: number };\n isInline: boolean;\n row: OverrideRow;\n error: string | null;\n saving: boolean;\n onChange: (key: string, value: string) => void;\n onPatch: (patch: Partial<OverrideValue>) => void;\n onSave: () => void;\n onCancel: () => void;\n}) {\n const { storage } = useGRail();\n const [pickerOpen, setPickerOpen] = useState(false);\n const GAP = 8;\n // Flip above the element when there is no room below it.\n const below = rect.bottom + GAP;\n const flip = typeof window !== \"undefined\" && below > window.innerHeight - 120;\n\n const position: React.CSSProperties = {\n left: Math.max(8, rect.left),\n ...(flip ? { top: Math.max(8, rect.top - GAP - 44) } : { top: below }),\n };\n\n const actions = (\n <>\n <button\n style={S.iconBtnPrimary}\n onClick={onSave}\n disabled={saving}\n title=\"Save (Enter)\"\n aria-label=\"Save\"\n >\n {saving ? <SpinnerIcon /> : <CheckIcon />}\n </button>\n <button\n style={S.iconBtn}\n onClick={onCancel}\n disabled={saving}\n title=\"Cancel (Esc)\"\n aria-label=\"Cancel\"\n >\n <CloseIcon />\n </button>\n </>\n );\n\n // Text and buttons edit in place, so they only need the two controls.\n if (isInline) {\n return (\n <div data-guardrail style={{ ...S.anchoredBar, ...position }}>\n {actions}\n {error && <span style={{ ...S.error, margin: \"0 6px\" }}>{error}</span>}\n </div>\n );\n }\n\n // Images and links carry attributes with no on-page representation.\n return (\n <div\n data-guardrail\n style={{ ...S.anchoredPanel, ...position }}\n onClick={(e) => e.stopPropagation()}\n >\n {row.value.type === \"image\" && (\n <>\n {row.value.src && (\n // Plain img: previews must not depend on remotePatterns.\n <img src={row.value.src} alt=\"\" style={S.previewThumb} />\n )}\n\n {storage && (\n <button\n type=\"button\"\n style={S.secondaryBtn}\n onClick={() => setPickerOpen(true)}\n >\n {row.value.src ? \"Replace image\" : \"Choose image\"}\n </button>\n )}\n\n <label style={S.label}>Alt text</label>\n <input\n style={S.field}\n value={row.value.alt || \"\"}\n placeholder=\"Describe the image for screen readers\"\n onChange={(e) => onChange(\"alt\", e.target.value)}\n />\n\n {/* Only without storage: there is no picker then, so a URL is the one\n way to set an image. A typed URL has unknown proportions, so any\n stored dimensions are cleared. */}\n {!storage && (\n <>\n <label style={S.label}>Image URL</label>\n <input\n style={S.field}\n value={row.value.src || \"\"}\n onChange={(e) =>\n onPatch({ src: e.target.value, width: undefined, height: undefined })\n }\n />\n </>\n )}\n\n {pickerOpen && (\n <MediaPicker\n current={row.value.src}\n onClose={() => setPickerOpen(false)}\n onSelect={(image) =>\n onPatch({ src: image.src, width: image.width, height: image.height })\n }\n />\n )}\n </>\n )}\n\n {row.value.type === \"link\" && (\n <>\n <label style={S.label}>Link text</label>\n <input\n style={S.field}\n value={row.value.text || \"\"}\n onChange={(e) => onChange(\"text\", e.target.value)}\n />\n <label style={S.label}>URL</label>\n <input\n style={S.field}\n value={row.value.href || \"\"}\n onChange={(e) => onChange(\"href\", e.target.value)}\n />\n </>\n )}\n\n {error && <div style={{ ...S.error, marginBottom: 8 }}>{error}</div>}\n <div style={{ display: \"flex\", gap: 4, justifyContent: \"flex-end\" }}>\n {actions}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/react/GRail/client.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { saveOverride } from \"../../server/actions\";\nimport { GRailRender } from \"./render\";\nimport { useGRail } from \"../Provider\";\nimport { useAnchorRect } from \"../useAnchorRect\";\nimport { CheckIcon, CloseIcon, SpinnerIcon } from \"../Icons\";\nimport { S } from \"../styles\";\nimport { MediaPicker } from \"../MediaPicker\";\nimport type { HTMLKeys, OverrideRow, OverrideValue } from \"../../core/types\";\n\nexport function GRailClient({\n as,\n initialOverride,\n ...htmlProps\n}: {\n as?: HTMLKeys;\n initialOverride: OverrideRow;\n [key: string]: unknown;\n}) {\n const { isEditMode } = useGRail();\n const [editing, setEditing] = useState(false);\n const [saving, setSaving] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [row, setRow] = useState<OverrideRow>(initialOverride);\n\n const elementRef = useRef<HTMLElement | null>(null);\n const rect = useAnchorRect(elementRef, editing);\n\n /**\n * The text the element was rendered with when this edit session began.\n *\n * While editing, the DOM owns the text and React must not touch it. Feeding\n * state back into a contentEditable's children makes React replace the text\n * node on every keystroke, which resets the caret to offset 0 — typing then\n * appears reversed and backspace jumps to the start. Holding this value\n * constant means the children prop never changes, so React leaves the node\n * alone and the caret stays put.\n */\n const sessionTextRef = useRef<string>(initialOverride.value.text ?? \"\");\n /** Live DOM text, updated without re-rendering. Read on save. */\n const draftTextRef = useRef<string>(initialOverride.value.text ?? \"\");\n\n const isInline = row.value.type === \"text\" || row.value.type === \"button\";\n\n const beginEdit = () => {\n sessionTextRef.current = row.value.text ?? \"\";\n draftTextRef.current = row.value.text ?? \"\";\n setError(null);\n setEditing(true);\n };\n\n const updateAttribute = (key: string, value: string) =>\n setRow((prev) => ({ ...prev, value: { ...prev.value, [key]: value } }));\n\n const patchValue = (patch: Partial<OverrideValue>) =>\n setRow((prev) => ({ ...prev, value: { ...prev.value, ...patch } }));\n\n const handleSave = async () => {\n // Inline text lives in the DOM, not in state, so read it back here.\n const next: OverrideRow = isInline\n ? { ...row, value: { ...row.value, text: draftTextRef.current } }\n : row;\n\n setSaving(true);\n setError(null);\n const res = await saveOverride(next);\n setSaving(false);\n\n if (!res.success) {\n setError(res.error ?? \"Save failed.\");\n return;\n }\n setRow(next);\n setEditing(false);\n };\n\n const handleCancel = () => {\n setRow(initialOverride);\n draftTextRef.current = initialOverride.value.text ?? \"\";\n if (isInline && elementRef.current) {\n elementRef.current.textContent = initialOverride.value.text ?? \"\";\n }\n setError(null);\n setEditing(false);\n };\n\n // Clicking away from an active edit cancels it, the same as Escape. Anything\n // inside the field itself (repositioning the caret) or inside the anchored\n // toolbar/panel (both carry data-guardrail) is not \"away\" — checked via\n // closest() rather than React's onClick so a stopPropagation() inside the\n // chrome (image/link panel inputs) reliably suppresses this too, since that\n // call stops the native event before it ever reaches this document listener.\n useEffect(() => {\n if (!editing) return;\n const handlePointerDown = (e: MouseEvent) => {\n const target = e.target as Node;\n if (elementRef.current?.contains(target)) return;\n if (target instanceof Element && target.closest(\"[data-guardrail]\")) return;\n handleCancel();\n };\n document.addEventListener(\"mousedown\", handlePointerDown);\n return () => document.removeEventListener(\"mousedown\", handlePointerDown);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editing]);\n\n if (!isEditMode) {\n return <GRailRender as={as} {...htmlProps} override={row.value} />;\n }\n\n // Frozen during the session so React never rewrites the live text node.\n const renderedValue =\n editing && isInline\n ? { ...row.value, text: sessionTextRef.current }\n : row.value;\n\n return (\n <>\n <GRailRender\n as={as}\n {...htmlProps}\n ref={elementRef}\n override={renderedValue}\n style={{\n ...(htmlProps.style as React.CSSProperties),\n outline: editing ? \"2px solid #2563eb\" : \"1px dashed #60a5fa\",\n outlineOffset: 2,\n cursor: editing ? \"text\" : \"pointer\",\n ...(row.value.type === \"image\" && {\n // A decorative sibling stacked above the image (a hero's\n // text/gradient scrim, say) covers the outline too — outline is\n // part of this element's own paint, not a separate layer, so it\n // can't out-stack a sibling on its own no matter how \"on top\"\n // that sounds. `position` is required for z-index to apply at\n // all on a normally static <img> — skipped for `fill` images,\n // which next/image already positions absolutely internally;\n // overriding that would break fill's own layout.\n position: htmlProps.fill ? undefined : \"relative\",\n zIndex: 9998,\n }),\n }}\n onClick={(e: React.MouseEvent) => {\n if (editing) return;\n e.preventDefault();\n // preventDefault only blocks the browser's native action (following\n // an href); it does not stop the click from bubbling. A field\n // rendered inside a Link would still trigger the Link's own onClick\n // and navigate anyway — stopPropagation is what actually claims the\n // click for editing instead. (Use `editable={false}` on fields\n // meant to stay pure navigation, e.g. inside a card that links to\n // that content's own page.)\n e.stopPropagation();\n beginEdit();\n }}\n // Spread only for inline text: images and links would receive invalid\n // DOM props (contentEditable on next/image, etc).\n {...(isInline\n ? {\n contentEditable: editing,\n suppressContentEditableWarning: true,\n onInput: (e: React.FormEvent<HTMLElement>) => {\n // Ref only — setState here re-renders and moves the caret.\n draftTextRef.current = e.currentTarget.textContent ?? \"\";\n },\n onKeyDown: (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n void handleSave();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n handleCancel();\n }\n },\n }\n : {})}\n />\n\n {editing && rect && (\n <Chrome\n rect={rect}\n isInline={isInline}\n row={row}\n error={error}\n saving={saving}\n onChange={updateAttribute}\n onPatch={patchValue}\n onSave={handleSave}\n onCancel={handleCancel}\n />\n )}\n </>\n );\n}\n\nfunction Chrome({\n rect,\n isInline,\n row,\n error,\n saving,\n onChange,\n onPatch,\n onSave,\n onCancel,\n}: {\n rect: { top: number; left: number; bottom: number; width: number };\n isInline: boolean;\n row: OverrideRow;\n error: string | null;\n saving: boolean;\n onChange: (key: string, value: string) => void;\n onPatch: (patch: Partial<OverrideValue>) => void;\n onSave: () => void;\n onCancel: () => void;\n}) {\n const { storage } = useGRail();\n const [pickerOpen, setPickerOpen] = useState(false);\n const GAP = 8;\n // Flip above the element when there is no room below it.\n const below = rect.bottom + GAP;\n const flip = typeof window !== \"undefined\" && below > window.innerHeight - 120;\n\n const position: React.CSSProperties = {\n left: Math.max(8, rect.left),\n ...(flip ? { top: Math.max(8, rect.top - GAP - 44) } : { top: below }),\n };\n\n const actions = (\n <>\n <button\n style={S.iconBtnPrimary}\n onClick={onSave}\n disabled={saving}\n title=\"Save (Enter)\"\n aria-label=\"Save\"\n >\n {saving ? <SpinnerIcon /> : <CheckIcon />}\n </button>\n <button\n style={S.iconBtn}\n onClick={onCancel}\n disabled={saving}\n title=\"Cancel (Esc)\"\n aria-label=\"Cancel\"\n >\n <CloseIcon />\n </button>\n </>\n );\n\n // Text and buttons edit in place, so they only need the two controls.\n if (isInline) {\n return (\n <div data-guardrail style={{ ...S.anchoredBar, ...position }}>\n {actions}\n {error && <span style={{ ...S.error, margin: \"0 6px\" }}>{error}</span>}\n </div>\n );\n }\n\n // Images and links carry attributes with no on-page representation.\n return (\n <div\n data-guardrail\n style={{ ...S.anchoredPanel, ...position }}\n onClick={(e) => e.stopPropagation()}\n >\n {row.value.type === \"image\" && (\n <>\n {row.value.src && (\n // Plain img: previews must not depend on remotePatterns.\n <img src={row.value.src} alt=\"\" style={S.previewThumb} />\n )}\n\n {storage && (\n <button\n type=\"button\"\n style={S.secondaryBtn}\n onClick={() => setPickerOpen(true)}\n >\n {row.value.src ? \"Replace image\" : \"Choose image\"}\n </button>\n )}\n\n <label style={S.label}>Alt text</label>\n <input\n style={S.field}\n value={row.value.alt || \"\"}\n placeholder=\"Describe the image for screen readers\"\n onChange={(e) => onChange(\"alt\", e.target.value)}\n />\n\n {/* Only without storage: there is no picker then, so a URL is the one\n way to set an image. A typed URL has unknown proportions, so any\n stored dimensions are cleared. */}\n {!storage && (\n <>\n <label style={S.label}>Image URL</label>\n <input\n style={S.field}\n value={row.value.src || \"\"}\n onChange={(e) =>\n onPatch({ src: e.target.value, width: undefined, height: undefined })\n }\n />\n </>\n )}\n\n {pickerOpen && (\n <MediaPicker\n current={row.value.src}\n onClose={() => setPickerOpen(false)}\n onSelect={(image) =>\n onPatch({ src: image.src, width: image.width, height: image.height })\n }\n />\n )}\n </>\n )}\n\n {row.value.type === \"link\" && (\n <>\n <label style={S.label}>Link text</label>\n <input\n style={S.field}\n value={row.value.text || \"\"}\n onChange={(e) => onChange(\"text\", e.target.value)}\n />\n <label style={S.label}>URL</label>\n <input\n style={S.field}\n value={row.value.href || \"\"}\n onChange={(e) => onChange(\"href\", e.target.value)}\n />\n </>\n )}\n\n {error && <div style={{ ...S.error, marginBottom: 8 }}>{error}</div>}\n <div style={{ display: \"flex\", gap: 4, justifyContent: \"flex-end\" }}>\n {actions}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -30,6 +30,15 @@ declare const S: {
30
30
  fontSize: number;
31
31
  textTransform: "uppercase";
32
32
  };
33
+ /** Brand plus any status/error text under it, stacked instead of sharing
34
+ * the form's row — otherwise a login error reads as if it's labeling the
35
+ * next field over, not feedback about the brand's own sign-in attempt. */
36
+ brandStack: {
37
+ display: "flex";
38
+ flexDirection: "column";
39
+ alignItems: "flex-start";
40
+ gap: number;
41
+ };
33
42
  error: {
34
43
  color: "#dc2626";
35
44
  fontSize: number;
@@ -24,6 +24,15 @@ const S = {
24
24
  fontSize: 12,
25
25
  textTransform: "uppercase"
26
26
  },
27
+ /** Brand plus any status/error text under it, stacked instead of sharing
28
+ * the form's row — otherwise a login error reads as if it's labeling the
29
+ * next field over, not feedback about the brand's own sign-in attempt. */
30
+ brandStack: {
31
+ display: "flex",
32
+ flexDirection: "column",
33
+ alignItems: "flex-start",
34
+ gap: 2
35
+ },
27
36
  error: { color: "#dc2626", fontSize: 12, fontWeight: 700 },
28
37
  input: {
29
38
  border: "1px solid #d1d5db",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/styles.ts"],"sourcesContent":["import type { CSSProperties } from \"react\";\n\n/**\n * Guardrail's own chrome is styled with inline styles on purpose. Utility\n * classes would make the package depend on the consumer having Tailwind *and*\n * adding node_modules/guardrail to their content globs — it would render\n * unstyled on a clean install. Developer-authored content is never touched.\n */\nexport const S = {\n banner: {\n position: \"fixed\",\n bottom: 0,\n left: 0,\n width: \"100%\",\n background: \"#fff\",\n color: \"#000\",\n padding: \"12px 16px\",\n zIndex: 10000,\n borderTop: \"1px solid #e5e7eb\",\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n },\n bannerForm: {\n display: \"flex\",\n gap: 12,\n justifyContent: \"center\",\n alignItems: \"center\",\n flexWrap: \"wrap\",\n },\n brand: {\n fontWeight: 900,\n letterSpacing: \"0.15em\",\n fontSize: 12,\n textTransform: \"uppercase\",\n },\n error: { color: \"#dc2626\", fontSize: 12, fontWeight: 700 },\n input: {\n border: \"1px solid #d1d5db\",\n borderRadius: 4,\n padding: \"6px 8px\",\n fontSize: 13,\n color: \"#000\",\n background: \"#fff\",\n outline: \"none\",\n },\n primaryBtn: {\n background: \"#111827\",\n color: \"#fff\",\n padding: \"7px 16px\",\n borderRadius: 4,\n fontSize: 12,\n fontWeight: 700,\n border: \"none\",\n cursor: \"pointer\",\n },\n toolbar: {\n position: \"fixed\",\n bottom: 24,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n zIndex: 9999,\n background: \"rgba(17,24,39,0.95)\",\n backdropFilter: \"blur(8px)\",\n color: \"#fff\",\n padding: \"12px 20px\",\n borderRadius: 9999,\n boxShadow: \"0 20px 40px rgba(0,0,0,0.35)\",\n display: \"flex\",\n alignItems: \"center\",\n gap: 20,\n border: \"1px solid rgba(255,255,255,0.1)\",\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n },\n dot: {\n display: \"inline-block\",\n width: 8,\n height: 8,\n borderRadius: \"50%\",\n background: \"#3b82f6\",\n },\n toolbarLabel: {\n fontSize: 10,\n fontWeight: 900,\n textTransform: \"uppercase\",\n letterSpacing: \"0.2em\",\n color: \"#dbeafe\",\n },\n divider: { width: 1, height: 16, background: \"rgba(255,255,255,0.2)\" },\n linkBtn: {\n fontSize: 10,\n fontWeight: 700,\n textTransform: \"uppercase\",\n letterSpacing: \"0.15em\",\n color: \"#9ca3af\",\n background: \"none\",\n border: \"none\",\n cursor: \"pointer\",\n },\n popover: {\n position: \"fixed\",\n bottom: 90,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n zIndex: 10001,\n background: \"#fff\",\n padding: 16,\n borderRadius: 8,\n boxShadow: \"0 20px 40px rgba(0,0,0,0.25)\",\n border: \"1px solid #e5e7eb\",\n minWidth: 300,\n cursor: \"default\",\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n },\n label: {\n display: \"block\",\n fontSize: 10,\n fontWeight: 700,\n color: \"#9ca3af\",\n marginBottom: 4,\n textTransform: \"uppercase\",\n },\n field: {\n border: \"1px solid #d1d5db\",\n borderRadius: 4,\n padding: \"6px 8px\",\n fontSize: 13,\n color: \"#000\",\n background: \"#fff\",\n width: \"100%\",\n marginBottom: 8,\n boxSizing: \"border-box\",\n },\n row: {\n display: \"flex\",\n gap: 8,\n marginTop: 8,\n paddingTop: 8,\n borderTop: \"1px solid #f3f4f6\",\n },\n cancelBtn: {\n background: \"#f3f4f6\",\n color: \"#4b5563\",\n padding: \"7px 14px\",\n borderRadius: 4,\n fontSize: 12,\n border: \"none\",\n cursor: \"pointer\",\n },\n\n /** Anchored to the element's rect at runtime; top/left are set inline. */\n anchoredBar: {\n position: \"fixed\",\n zIndex: 10001,\n display: \"flex\",\n alignItems: \"center\",\n gap: 4,\n background: \"rgba(17,24,39,0.97)\",\n backdropFilter: \"blur(8px)\",\n padding: 4,\n borderRadius: 8,\n boxShadow: \"0 8px 24px rgba(0,0,0,0.3)\",\n border: \"1px solid rgba(255,255,255,0.12)\",\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n },\n anchoredPanel: {\n position: \"fixed\",\n zIndex: 10001,\n maxHeight: \"calc(100vh - 24px)\",\n overflowY: \"auto\",\n background: \"#fff\",\n padding: 12,\n borderRadius: 8,\n boxShadow: \"0 12px 32px rgba(0,0,0,0.25)\",\n border: \"1px solid #e5e7eb\",\n minWidth: 260,\n maxWidth: \"min(340px, calc(100vw - 16px))\",\n cursor: \"default\",\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n },\n iconBtn: {\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: 26,\n height: 26,\n borderRadius: 6,\n border: \"none\",\n background: \"transparent\",\n color: \"#e5e7eb\",\n cursor: \"pointer\",\n padding: 0,\n },\n iconBtnPrimary: {\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: 26,\n height: 26,\n borderRadius: 6,\n border: \"none\",\n background: \"#2563eb\",\n color: \"#fff\",\n cursor: \"pointer\",\n padding: 0,\n },\n\n scrim: {\n position: \"fixed\",\n inset: 0,\n background: \"rgba(0,0,0,0.4)\",\n zIndex: 10002,\n },\n drawer: {\n position: \"fixed\",\n top: 0,\n right: 0,\n height: \"100%\",\n width: \"min(420px, 100vw)\",\n background: \"#fff\",\n color: \"#111827\",\n zIndex: 10003,\n boxShadow: \"-12px 0 32px rgba(0,0,0,0.2)\",\n display: \"flex\",\n flexDirection: \"column\",\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n },\n drawerHead: {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n padding: \"16px 20px\",\n borderBottom: \"1px solid #e5e7eb\",\n },\n drawerTitle: { fontSize: 14, fontWeight: 800, margin: 0 },\n drawerBody: { padding: 20, overflowY: \"auto\", flex: 1 },\n memberRow: {\n display: \"flex\",\n alignItems: \"center\",\n gap: 8,\n padding: \"10px 0\",\n borderBottom: \"1px solid #f3f4f6\",\n },\n memberEmail: {\n flex: 1,\n fontSize: 13,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n },\n select: {\n border: \"1px solid #d1d5db\",\n borderRadius: 4,\n padding: \"4px 6px\",\n fontSize: 12,\n background: \"#fff\",\n color: \"#111827\",\n },\n badge: {\n fontSize: 9,\n fontWeight: 800,\n textTransform: \"uppercase\",\n letterSpacing: \"0.1em\",\n padding: \"2px 6px\",\n borderRadius: 999,\n background: \"#fef3c7\",\n color: \"#92400e\",\n },\n sectionLabel: {\n fontSize: 10,\n fontWeight: 800,\n textTransform: \"uppercase\",\n letterSpacing: \"0.15em\",\n color: \"#9ca3af\",\n margin: \"20px 0 8px\",\n },\n\n pickerScrim: {\n position: \"fixed\",\n inset: 0,\n background: \"rgba(0,0,0,0.5)\",\n zIndex: 10010,\n },\n picker: {\n position: \"fixed\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n width: \"min(720px, calc(100vw - 32px))\",\n maxHeight: \"min(640px, calc(100vh - 32px))\",\n background: \"#fff\",\n color: \"#111827\",\n borderRadius: 12,\n zIndex: 10011,\n boxShadow: \"0 24px 64px rgba(0,0,0,0.35)\",\n display: \"flex\",\n flexDirection: \"column\",\n overflow: \"hidden\",\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n },\n pickerGrid: {\n display: \"grid\",\n gridTemplateColumns: \"repeat(auto-fill, minmax(120px, 1fr))\",\n gap: 10,\n },\n pickerTile: {\n position: \"relative\",\n aspectRatio: \"1\",\n borderRadius: 8,\n overflow: \"hidden\",\n border: \"2px solid transparent\",\n background: \"#f3f4f6\",\n cursor: \"pointer\",\n padding: 0,\n },\n pickerTileActive: { borderColor: \"#2563eb\" },\n pickerThumb: {\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n display: \"block\",\n },\n dropzone: {\n border: \"1.5px dashed #d1d5db\",\n borderRadius: 8,\n padding: 16,\n textAlign: \"center\",\n fontSize: 13,\n color: \"#6b7280\",\n marginBottom: 14,\n cursor: \"pointer\",\n },\n dropzoneActive: {\n borderColor: \"#2563eb\",\n background: \"#eff6ff\",\n color: \"#1d4ed8\",\n },\n previewThumb: {\n // maxWidth also caps what the image contributes to the panel's width. The\n // panel shrink-wraps its content, so an uncapped width: 100% image grew\n // the whole panel to the picture's natural size.\n width: \"100%\",\n maxWidth: 300,\n height: \"auto\",\n maxHeight: 200,\n objectFit: \"contain\",\n background: \"#f3f4f6\",\n borderRadius: 6,\n display: \"block\",\n marginBottom: 8,\n },\n secondaryBtn: {\n background: \"#f3f4f6\",\n color: \"#111827\",\n padding: \"7px 12px\",\n borderRadius: 4,\n fontSize: 12,\n fontWeight: 600,\n border: \"none\",\n cursor: \"pointer\",\n width: \"100%\",\n marginBottom: 10,\n },\n\n repeaterItem: {\n position: \"relative\",\n outline: \"1px dashed #60a5fa\",\n outlineOffset: 2,\n },\n repeaterToolbar: {\n position: \"absolute\",\n top: 4,\n right: 4,\n zIndex: 20,\n display: \"flex\",\n gap: 2,\n background: \"rgba(17,24,39,0.9)\",\n backdropFilter: \"blur(6px)\",\n padding: 3,\n borderRadius: 6,\n boxShadow: \"0 4px 12px rgba(0,0,0,0.25)\",\n },\n repeaterAddRow: {\n display: \"flex\",\n gap: 8,\n alignItems: \"center\",\n marginTop: 8,\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n },\n repeaterAddBtn: {\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: 6,\n padding: \"8px 14px\",\n borderRadius: 6,\n border: \"1.5px dashed #93c5fd\",\n background: \"transparent\",\n color: \"#2563eb\",\n fontSize: 12,\n fontWeight: 700,\n cursor: \"pointer\",\n },\n repeaterSlugInput: {\n border: \"1px solid #d1d5db\",\n borderRadius: 4,\n padding: \"7px 9px\",\n fontSize: 13,\n color: \"#000\",\n background: \"#fff\",\n minWidth: 180,\n },\n} satisfies Record<string, CSSProperties>;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/react/styles.ts"],"sourcesContent":["import type { CSSProperties } from \"react\";\n\n/**\n * Guardrail's own chrome is styled with inline styles on purpose. Utility\n * classes would make the package depend on the consumer having Tailwind *and*\n * adding node_modules/guardrail to their content globs — it would render\n * unstyled on a clean install. Developer-authored content is never touched.\n */\nexport const S = {\n banner: {\n position: \"fixed\",\n bottom: 0,\n left: 0,\n width: \"100%\",\n background: \"#fff\",\n color: \"#000\",\n padding: \"12px 16px\",\n zIndex: 10000,\n borderTop: \"1px solid #e5e7eb\",\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n },\n bannerForm: {\n display: \"flex\",\n gap: 12,\n justifyContent: \"center\",\n alignItems: \"center\",\n flexWrap: \"wrap\",\n },\n brand: {\n fontWeight: 900,\n letterSpacing: \"0.15em\",\n fontSize: 12,\n textTransform: \"uppercase\",\n },\n /** Brand plus any status/error text under it, stacked instead of sharing\n * the form's row — otherwise a login error reads as if it's labeling the\n * next field over, not feedback about the brand's own sign-in attempt. */\n brandStack: {\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"flex-start\",\n gap: 2,\n },\n error: { color: \"#dc2626\", fontSize: 12, fontWeight: 700 },\n input: {\n border: \"1px solid #d1d5db\",\n borderRadius: 4,\n padding: \"6px 8px\",\n fontSize: 13,\n color: \"#000\",\n background: \"#fff\",\n outline: \"none\",\n },\n primaryBtn: {\n background: \"#111827\",\n color: \"#fff\",\n padding: \"7px 16px\",\n borderRadius: 4,\n fontSize: 12,\n fontWeight: 700,\n border: \"none\",\n cursor: \"pointer\",\n },\n toolbar: {\n position: \"fixed\",\n bottom: 24,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n zIndex: 9999,\n background: \"rgba(17,24,39,0.95)\",\n backdropFilter: \"blur(8px)\",\n color: \"#fff\",\n padding: \"12px 20px\",\n borderRadius: 9999,\n boxShadow: \"0 20px 40px rgba(0,0,0,0.35)\",\n display: \"flex\",\n alignItems: \"center\",\n gap: 20,\n border: \"1px solid rgba(255,255,255,0.1)\",\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n },\n dot: {\n display: \"inline-block\",\n width: 8,\n height: 8,\n borderRadius: \"50%\",\n background: \"#3b82f6\",\n },\n toolbarLabel: {\n fontSize: 10,\n fontWeight: 900,\n textTransform: \"uppercase\",\n letterSpacing: \"0.2em\",\n color: \"#dbeafe\",\n },\n divider: { width: 1, height: 16, background: \"rgba(255,255,255,0.2)\" },\n linkBtn: {\n fontSize: 10,\n fontWeight: 700,\n textTransform: \"uppercase\",\n letterSpacing: \"0.15em\",\n color: \"#9ca3af\",\n background: \"none\",\n border: \"none\",\n cursor: \"pointer\",\n },\n popover: {\n position: \"fixed\",\n bottom: 90,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n zIndex: 10001,\n background: \"#fff\",\n padding: 16,\n borderRadius: 8,\n boxShadow: \"0 20px 40px rgba(0,0,0,0.25)\",\n border: \"1px solid #e5e7eb\",\n minWidth: 300,\n cursor: \"default\",\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n },\n label: {\n display: \"block\",\n fontSize: 10,\n fontWeight: 700,\n color: \"#9ca3af\",\n marginBottom: 4,\n textTransform: \"uppercase\",\n },\n field: {\n border: \"1px solid #d1d5db\",\n borderRadius: 4,\n padding: \"6px 8px\",\n fontSize: 13,\n color: \"#000\",\n background: \"#fff\",\n width: \"100%\",\n marginBottom: 8,\n boxSizing: \"border-box\",\n },\n row: {\n display: \"flex\",\n gap: 8,\n marginTop: 8,\n paddingTop: 8,\n borderTop: \"1px solid #f3f4f6\",\n },\n cancelBtn: {\n background: \"#f3f4f6\",\n color: \"#4b5563\",\n padding: \"7px 14px\",\n borderRadius: 4,\n fontSize: 12,\n border: \"none\",\n cursor: \"pointer\",\n },\n\n /** Anchored to the element's rect at runtime; top/left are set inline. */\n anchoredBar: {\n position: \"fixed\",\n zIndex: 10001,\n display: \"flex\",\n alignItems: \"center\",\n gap: 4,\n background: \"rgba(17,24,39,0.97)\",\n backdropFilter: \"blur(8px)\",\n padding: 4,\n borderRadius: 8,\n boxShadow: \"0 8px 24px rgba(0,0,0,0.3)\",\n border: \"1px solid rgba(255,255,255,0.12)\",\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n },\n anchoredPanel: {\n position: \"fixed\",\n zIndex: 10001,\n maxHeight: \"calc(100vh - 24px)\",\n overflowY: \"auto\",\n background: \"#fff\",\n padding: 12,\n borderRadius: 8,\n boxShadow: \"0 12px 32px rgba(0,0,0,0.25)\",\n border: \"1px solid #e5e7eb\",\n minWidth: 260,\n maxWidth: \"min(340px, calc(100vw - 16px))\",\n cursor: \"default\",\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n },\n iconBtn: {\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: 26,\n height: 26,\n borderRadius: 6,\n border: \"none\",\n background: \"transparent\",\n color: \"#e5e7eb\",\n cursor: \"pointer\",\n padding: 0,\n },\n iconBtnPrimary: {\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: 26,\n height: 26,\n borderRadius: 6,\n border: \"none\",\n background: \"#2563eb\",\n color: \"#fff\",\n cursor: \"pointer\",\n padding: 0,\n },\n\n scrim: {\n position: \"fixed\",\n inset: 0,\n background: \"rgba(0,0,0,0.4)\",\n zIndex: 10002,\n },\n drawer: {\n position: \"fixed\",\n top: 0,\n right: 0,\n height: \"100%\",\n width: \"min(420px, 100vw)\",\n background: \"#fff\",\n color: \"#111827\",\n zIndex: 10003,\n boxShadow: \"-12px 0 32px rgba(0,0,0,0.2)\",\n display: \"flex\",\n flexDirection: \"column\",\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n },\n drawerHead: {\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n padding: \"16px 20px\",\n borderBottom: \"1px solid #e5e7eb\",\n },\n drawerTitle: { fontSize: 14, fontWeight: 800, margin: 0 },\n drawerBody: { padding: 20, overflowY: \"auto\", flex: 1 },\n memberRow: {\n display: \"flex\",\n alignItems: \"center\",\n gap: 8,\n padding: \"10px 0\",\n borderBottom: \"1px solid #f3f4f6\",\n },\n memberEmail: {\n flex: 1,\n fontSize: 13,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n },\n select: {\n border: \"1px solid #d1d5db\",\n borderRadius: 4,\n padding: \"4px 6px\",\n fontSize: 12,\n background: \"#fff\",\n color: \"#111827\",\n },\n badge: {\n fontSize: 9,\n fontWeight: 800,\n textTransform: \"uppercase\",\n letterSpacing: \"0.1em\",\n padding: \"2px 6px\",\n borderRadius: 999,\n background: \"#fef3c7\",\n color: \"#92400e\",\n },\n sectionLabel: {\n fontSize: 10,\n fontWeight: 800,\n textTransform: \"uppercase\",\n letterSpacing: \"0.15em\",\n color: \"#9ca3af\",\n margin: \"20px 0 8px\",\n },\n\n pickerScrim: {\n position: \"fixed\",\n inset: 0,\n background: \"rgba(0,0,0,0.5)\",\n zIndex: 10010,\n },\n picker: {\n position: \"fixed\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n width: \"min(720px, calc(100vw - 32px))\",\n maxHeight: \"min(640px, calc(100vh - 32px))\",\n background: \"#fff\",\n color: \"#111827\",\n borderRadius: 12,\n zIndex: 10011,\n boxShadow: \"0 24px 64px rgba(0,0,0,0.35)\",\n display: \"flex\",\n flexDirection: \"column\",\n overflow: \"hidden\",\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n },\n pickerGrid: {\n display: \"grid\",\n gridTemplateColumns: \"repeat(auto-fill, minmax(120px, 1fr))\",\n gap: 10,\n },\n pickerTile: {\n position: \"relative\",\n aspectRatio: \"1\",\n borderRadius: 8,\n overflow: \"hidden\",\n border: \"2px solid transparent\",\n background: \"#f3f4f6\",\n cursor: \"pointer\",\n padding: 0,\n },\n pickerTileActive: { borderColor: \"#2563eb\" },\n pickerThumb: {\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n display: \"block\",\n },\n dropzone: {\n border: \"1.5px dashed #d1d5db\",\n borderRadius: 8,\n padding: 16,\n textAlign: \"center\",\n fontSize: 13,\n color: \"#6b7280\",\n marginBottom: 14,\n cursor: \"pointer\",\n },\n dropzoneActive: {\n borderColor: \"#2563eb\",\n background: \"#eff6ff\",\n color: \"#1d4ed8\",\n },\n previewThumb: {\n // maxWidth also caps what the image contributes to the panel's width. The\n // panel shrink-wraps its content, so an uncapped width: 100% image grew\n // the whole panel to the picture's natural size.\n width: \"100%\",\n maxWidth: 300,\n height: \"auto\",\n maxHeight: 200,\n objectFit: \"contain\",\n background: \"#f3f4f6\",\n borderRadius: 6,\n display: \"block\",\n marginBottom: 8,\n },\n secondaryBtn: {\n background: \"#f3f4f6\",\n color: \"#111827\",\n padding: \"7px 12px\",\n borderRadius: 4,\n fontSize: 12,\n fontWeight: 600,\n border: \"none\",\n cursor: \"pointer\",\n width: \"100%\",\n marginBottom: 10,\n },\n\n repeaterItem: {\n position: \"relative\",\n outline: \"1px dashed #60a5fa\",\n outlineOffset: 2,\n },\n repeaterToolbar: {\n position: \"absolute\",\n top: 4,\n right: 4,\n zIndex: 20,\n display: \"flex\",\n gap: 2,\n background: \"rgba(17,24,39,0.9)\",\n backdropFilter: \"blur(6px)\",\n padding: 3,\n borderRadius: 6,\n boxShadow: \"0 4px 12px rgba(0,0,0,0.25)\",\n },\n repeaterAddRow: {\n display: \"flex\",\n gap: 8,\n alignItems: \"center\",\n marginTop: 8,\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n },\n repeaterAddBtn: {\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: 6,\n padding: \"8px 14px\",\n borderRadius: 6,\n border: \"1.5px dashed #93c5fd\",\n background: \"transparent\",\n color: \"#2563eb\",\n fontSize: 12,\n fontWeight: 700,\n cursor: \"pointer\",\n },\n repeaterSlugInput: {\n border: \"1px solid #d1d5db\",\n borderRadius: 4,\n padding: \"7px 9px\",\n fontSize: 13,\n color: \"#000\",\n background: \"#fff\",\n minWidth: 180,\n },\n} satisfies Record<string, CSSProperties>;\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guard-rail/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Lightweight inline content editing for developer-owned Next.js sites.",
5
5
  "license": "ISC",
6
6
  "type": "module",
@@ -19,8 +19,12 @@
19
19
  },
20
20
  "files": [
21
21
  "dist",
22
- "supabase"
22
+ "supabase",
23
+ "scripts"
23
24
  ],
25
+ "bin": {
26
+ "guardrail-seed-user": "dist/cli/seed-user.js"
27
+ },
24
28
  "publishConfig": {
25
29
  "access": "public"
26
30
  },
@@ -29,7 +33,8 @@
29
33
  "dev": "tsup --watch",
30
34
  "typecheck": "tsc --noEmit",
31
35
  "prepare": "npm run build",
32
- "prepublishOnly": "npm run build"
36
+ "prepublishOnly": "npm run build",
37
+ "postinstall": "node scripts/postinstall.mjs"
33
38
  },
34
39
  "peerDependencies": {
35
40
  "@supabase/ssr": "^0.8.0",
@@ -0,0 +1,48 @@
1
+ // Plain, already-final JS — deliberately NOT part of the tsup build. A
2
+ // postinstall script has to exist the instant npm/pnpm/yarn extracts the
3
+ // package, with no dependency on a build step having run yet, which would
4
+ // otherwise race tsup during this very monorepo's own installs (lifecycle
5
+ // script ordering between "postinstall" and "prepare" isn't a guarantee
6
+ // worth relying on).
7
+ //
8
+ // Runs after `npm install @guard-rail/core` in a consuming project — just
9
+ // onboarding guidance, easy to miss otherwise among the rest of an install's
10
+ // output.
11
+ //
12
+ // Deliberately silent in two cases:
13
+ // - CI: this is guidance for a human at a keyboard, not something that
14
+ // belongs in build logs.
15
+ // - Running inside this very monorepo: pnpm runs a workspace package's own
16
+ // lifecycle scripts too (already visible via the "prepare" build step on
17
+ // every `pnpm install`), which would otherwise print this on every
18
+ // contributor's install of this repo, not just a real consumer's. Resolved
19
+ // through the pnpm workspace symlink, this file's own real path sits under
20
+ // packages/guardrail/ — never inside a node_modules/ — which a real
21
+ // consumer's install always does.
22
+ import { fileURLToPath } from "node:url";
23
+
24
+ const isRealInstall = fileURLToPath(import.meta.url).includes("node_modules");
25
+
26
+ if (isRealInstall && !process.env.CI) {
27
+ console.log(`
28
+ Guardrail installed. Next steps:
29
+
30
+ 1. Set up the database (skip both if this Supabase project already has
31
+ Guardrail applied for another site — see "Many sites, one Supabase
32
+ project" in the README):
33
+ - Copy node_modules/@guard-rail/core/supabase/migrations/*.sql into
34
+ your own supabase/migrations/, then \`supabase db push\`.
35
+ - Add "guardrail" under Settings > API > Exposed schemas in the
36
+ Supabase dashboard.
37
+
38
+ 2. Wrap your root layout with withGuardrail and mark fields with <GRail>.
39
+ Full guide: https://www.npmjs.com/package/@guard-rail/core
40
+
41
+ 3. Once SUPABASE_URL / SUPABASE_ANON_KEY are set, seed yourself as the
42
+ first admin on a site (needs the service_role key, not the anon key —
43
+ Settings > API in the dashboard; pass it inline, don't store it):
44
+
45
+ SERVICE_KEY=... npx guardrail-seed-user \\
46
+ --email=you@example.com --password=... --site-id=my-site
47
+ `);
48
+ }
@@ -17,7 +17,12 @@
17
17
  -- sufficient — keep both.
18
18
 
19
19
  create schema if not exists guardrail;
20
- grant usage on schema guardrail to anon, authenticated;
20
+ -- service_role bypasses RLS (a Postgres role attribute Supabase configures
21
+ -- automatically), but that doesn't waive ordinary schema/table grants —
22
+ -- without this, any service_role-authenticated call (guardrail-seed-user,
23
+ -- say) fails with "permission denied for schema guardrail" despite using
24
+ -- the right key entirely.
25
+ grant usage on schema guardrail to anon, authenticated, service_role;
21
26
 
22
27
  /* ------------------------------------------------------------- members --- */
23
28
 
@@ -38,7 +43,7 @@ alter table guardrail.gr_members enable row level security;
38
43
  -- Anon has no legitimate reason to read team membership, so it gets no grant
39
44
  -- at all here (unlike gr_content, which anonymous visitors must be able to
40
45
  -- read to render the site).
41
- grant select, insert, update, delete on guardrail.gr_members to authenticated;
46
+ grant select, insert, update, delete on guardrail.gr_members to authenticated, service_role;
42
47
 
43
48
  -- Does the current user hold one of these roles on this site? SECURITY
44
49
  -- DEFINER so the membership policies below can call it without recursing
@@ -121,8 +126,8 @@ create index gr_content_site_path_idx on guardrail.gr_content (site_id, path_key
121
126
 
122
127
  alter table guardrail.gr_content enable row level security;
123
128
 
124
- grant select on guardrail.gr_content to anon, authenticated;
125
- grant insert, update, delete on guardrail.gr_content to authenticated;
129
+ grant select on guardrail.gr_content to anon, authenticated, service_role;
130
+ grant insert, update, delete on guardrail.gr_content to authenticated, service_role;
126
131
 
127
132
  -- Anyone may read published content: this is what anonymous visitors render.
128
133
  create policy "content is publicly readable"
@@ -179,8 +184,8 @@ create index gr_repeater_items_order_idx
179
184
 
180
185
  alter table guardrail.gr_repeater_items enable row level security;
181
186
 
182
- grant select on guardrail.gr_repeater_items to anon, authenticated;
183
- grant insert, update, delete on guardrail.gr_repeater_items to authenticated;
187
+ grant select on guardrail.gr_repeater_items to anon, authenticated, service_role;
188
+ grant insert, update, delete on guardrail.gr_repeater_items to authenticated, service_role;
184
189
 
185
190
  -- Anyone may enumerate items: this is what renders the page for anonymous
186
191
  -- visitors, same reasoning as gr_content's public read policy.
@@ -204,6 +209,10 @@ create policy "editors manage repeater items"
204
209
  -- Listing and uploading still go through the policies below. The size and
205
210
  -- type limits are enforced by Storage itself, backing up the server action.
206
211
 
212
+ -- on conflict do nothing: a project that already has a 'gr-media' bucket
213
+ -- (a re-run, or one created by hand before this migration existed) must not
214
+ -- fail the whole migration over a bucket that already exists in the shape
215
+ -- this would create anyway.
207
216
  insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
208
217
  values (
209
218
  'gr-media',
@@ -211,12 +220,22 @@ values (
211
220
  true,
212
221
  10485760,
213
222
  array['image/png', 'image/jpeg', 'image/webp', 'image/gif', 'image/avif']
214
- );
223
+ )
224
+ on conflict (id) do nothing;
215
225
 
216
226
  -- Objects live under '<site_id>/…', so the first folder names the site and
217
227
  -- membership on that site decides who may upload or browse. Scoped to the
218
228
  -- 'gr-' prefix, not one bucket name, so any bucket a developer names via
219
229
  -- supabaseStorage({ bucket }) is covered without another migration.
230
+ --
231
+ -- storage.objects is shared across the whole project, unlike the guardrail
232
+ -- schema's own tables above (which this same migration just created, so
233
+ -- they cannot already have policies on them) — Postgres has no
234
+ -- `create policy if not exists`, so a project with same-named leftover
235
+ -- policies (e.g. from a hand-built setup predating this migration) needs
236
+ -- the drop first or the whole migration fails on a policy that doesn't
237
+ -- conflict in substance, just in name.
238
+ drop policy if exists "gr editors upload media" on storage.objects;
220
239
  create policy "gr editors upload media"
221
240
  on storage.objects for insert
222
241
  to authenticated
@@ -225,6 +244,7 @@ create policy "gr editors upload media"
225
244
  and guardrail.gr_has_role((storage.foldername(name))[1], array['admin', 'editor'])
226
245
  );
227
246
 
247
+ drop policy if exists "gr editors browse media" on storage.objects;
228
248
  create policy "gr editors browse media"
229
249
  on storage.objects for select
230
250
  to authenticated
@@ -274,8 +294,12 @@ grant execute on function guardrail.gr_ensure_bucket(text) to authenticated;
274
294
 
275
295
  /* ----------------------------------------------------------- bootstrap --- */
276
296
  -- A site with no members has nobody who can add members, and there is
277
- -- deliberately no in-app way to grant yourself a role. Seed the first admin
278
- -- here, replacing both values:
297
+ -- deliberately no in-app way to grant yourself a role. Prefer the bundled
298
+ -- `guardrail-seed-user` command (see the README's Bootstrapping section) —
299
+ -- it creates the account via Supabase's own Admin API if it doesn't exist
300
+ -- yet, rather than assuming one already does. The raw SQL below still works
301
+ -- if you'd rather not use the service_role key it needs, replacing both
302
+ -- values:
279
303
  --
280
304
  -- insert into guardrail.gr_members (site_id, email, user_id, role, status)
281
305
  -- select 'your-site-id', u.email, u.id, 'admin', 'active'