@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 +47 -3
- package/dist/cli/seed-user.d.ts +1 -0
- package/dist/cli/seed-user.js +105 -0
- package/dist/cli/seed-user.js.map +1 -0
- package/dist/react/Admin.js +9 -4
- package/dist/react/Admin.js.map +1 -1
- package/dist/react/GRail/client.js +28 -5
- package/dist/react/GRail/client.js.map +1 -1
- package/dist/react/styles.d.ts +9 -0
- package/dist/react/styles.js +9 -0
- package/dist/react/styles.js.map +1 -1
- package/package.json +8 -3
- package/scripts/postinstall.mjs +48 -0
- package/supabase/migrations/20260911000000_guardrail_init.sql +33 -9
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/
|
|
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
|
|
133
|
-
|
|
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":[]}
|
package/dist/react/Admin.js
CHANGED
|
@@ -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__ */
|
|
33
|
-
|
|
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__ */
|
|
38
|
-
|
|
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
|
{
|
package/dist/react/Admin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/Admin.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useActionState, useEffect, useState } from \"react\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport { loginAction, logoutAction } from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { TeamDrawer } from \"./Team\";\nimport { LogOutIcon, PeopleIcon } from \"./Icons\";\nimport { S } 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
|
|
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":";AA4CY,SAmFF,UAlFI,KADF;AA1CZ,OAAO,SAAS,gBAAgB,WAAW,gBAAgB;AAC3D,SAAS,aAAa,iBAAiB;AACvC,SAAS,aAAa,oBAAoB;AAC1C,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,YAAY,kBAAkB;AACvC,SAAS,SAAS;AAGlB,MAAM,eAA4B,EAAE,OAAO,IAAI,UAAU,GAAG;AAErD,SAAS,WAAW,EAAE,aAAa,GAAuC;AAC/E,QAAM,EAAE,QAAQ,YAAY,MAAM,KAAK,IAAI,SAAS;AACpD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,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;AAE9B,QAAM,UAAU,YAAY;AAC1B,UAAM,aAAa;AACnB,aAAS,SACP;AACF,WAAO,SAAS,OAAO;AAAA,EACzB;AAGA,MAAI,CAAC,WAAY,QAAO;AAExB,MAAI,CAAC,QAAQ;AAGX,QAAI,CAAC,KAAK,UAAU;AAClB,aACE,oBAAC,SAAI,kBAAc,MAAC,OAAO,EAAE,QAC3B,+BAAC,SAAI,OAAO,EAAE,YACZ;AAAA,6BAAC,SAAI,OAAO,EAAE,YACZ;AAAA,8BAAC,UAAK,OAAO,EAAE,OAAO,uBAAS;AAAA,UAC9B,CAAC,KAAK,aACL,oBAAC,UAAK,OAAO,EAAE,UAAU,IAAI,OAAO,UAAU,GAAG,4DAEjD;AAAA,WAEJ;AAAA,QACC,KAAK,aACJ,oBAAC,OAAE,MAAM,KAAK,WAAW,OAAO,EAAE,GAAG,EAAE,YAAY,gBAAgB,OAAO,GAAG,qBAE7E;AAAA,SAEJ,GACF;AAAA,IAEJ;AAEA,WACE,oBAAC,SAAI,kBAAc,MAAC,OAAO,EAAE,QAC3B,+BAAC,UAAK,QAAQ,YAAY,OAAO,EAAE,YACjC;AAAA,2BAAC,SAAI,OAAO,EAAE,YACZ;AAAA,4BAAC,UAAK,OAAO,EAAE,OAAO,uBAAS;AAAA,QAC9B,UAAU,SAAS,oBAAC,UAAK,OAAO,EAAE,OAAQ,oBAAU,OAAM;AAAA,SAC7D;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,MAAK;AAAA,UACL,aAAY;AAAA,UACZ,cAAc,UAAU;AAAA,UACxB,UAAQ;AAAA,UACR,OAAO,EAAE;AAAA;AAAA,MACX;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,MAAK;AAAA,UACL,aAAY;AAAA,UACZ,UAAQ;AAAA,UACR,OAAO,EAAE;AAAA;AAAA,MACX;AAAA,MACA,oBAAC,YAAO,MAAK,UAAS,UAAU,WAAW,OAAO,EAAE,YACjD,sBAAY,yBAAoB,WACnC;AAAA,OACF,GACF;AAAA,EAEJ;AAGA,MAAI,CAAC,MAAM;AACT,WACE,qBAAC,SAAI,kBAAc,MAAC,OAAO,EAAE,SAC3B;AAAA,0BAAC,UAAK,OAAO,EAAE,cAAc,oCAAsB;AAAA,MACnD,oBAAC,UAAK,OAAO,EAAE,SAAS;AAAA,MACxB;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,EAAE,GAAG,EAAE,SAAS,OAAO,UAAU;AAAA,UACxC,SAAS;AAAA,UACT,OAAM;AAAA,UACN,cAAW;AAAA,UAEX,8BAAC,cAAW;AAAA;AAAA,MACd;AAAA,OACF;AAAA,EAEJ;AAEA,SACE,iCACG;AAAA,gBAAY,oBAAC,cAAW,SAAS,MAAM,YAAY,KAAK,GAAG;AAAA,IAC5D,qBAAC,SAAI,kBAAc,MAAC,OAAO,EAAE,SAC3B;AAAA,0BAAC,UAAK,OAAO,EAAE,KAAK;AAAA,MACpB,oBAAC,UAAK,OAAO,EAAE,cAAc,uBAAS;AAAA,MACtC,oBAAC,UAAK,OAAO,EAAE,SAAS;AAAA,MACxB;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,EAAE,GAAG,EAAE,SAAS,OAAO,WAAW,OAAO,IAAI,QAAQ,GAAG;AAAA,UAC/D,SAAS,MAAM,YAAY,IAAI;AAAA,UAC/B,OAAM;AAAA,UACN,cAAW;AAAA,UAEX,8BAAC,cAAW;AAAA;AAAA,MACd;AAAA,MAEC,gBACC,iCACE;AAAA,4BAAC,UAAK,OAAO,EAAE,SAAS;AAAA,QACvB;AAAA,SACH;AAAA,MAGF,oBAAC,UAAK,OAAO,EAAE,SAAS;AAAA,MACxB;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,EAAE,GAAG,EAAE,SAAS,OAAO,UAAU;AAAA,UACxC,SAAS;AAAA,UACT,OAAM;AAAA,UACN,cAAW;AAAA,UAEX,8BAAC,cAAW;AAAA;AAAA,MACd;AAAA,OACF;AAAA,KACF;AAEJ;","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":[]}
|
package/dist/react/styles.d.ts
CHANGED
|
@@ -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;
|
package/dist/react/styles.js
CHANGED
|
@@ -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",
|
package/dist/react/styles.js.map
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
278
|
-
--
|
|
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'
|