@guard-rail/core 0.1.1 → 0.2.1
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 +63 -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.d.ts +6 -1
- package/dist/react/Admin.js +14 -6
- package/dist/react/Admin.js.map +1 -1
- package/dist/react/GRail/client.js +87 -16
- package/dist/react/GRail/client.js.map +1 -1
- package/dist/react/Icons.d.ts +2 -1
- package/dist/react/Icons.js +5 -0
- package/dist/react/Icons.js.map +1 -1
- package/dist/react/MediaPicker.js +3 -3
- package/dist/react/MediaPicker.js.map +1 -1
- package/dist/react/Repeater/client.js +2 -2
- package/dist/react/Repeater/client.js.map +1 -1
- package/dist/react/Team.js +6 -6
- package/dist/react/Team.js.map +1 -1
- package/dist/react/styles.d.ts +104 -30
- package/dist/react/styles.js +154 -65
- package/dist/react/styles.js.map +1 -1
- package/dist/react/useAnchorRect.js +1 -0
- package/dist/react/useAnchorRect.js.map +1 -1
- package/package.json +9 -3
- package/scripts/postinstall.mjs +48 -0
- package/supabase/migrations/20260911000000_guardrail_init.sql +17 -8
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,22 @@ 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 --email=you@example.com --password=...
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`--site-id` defaults to `GUARDRAIL_SITE_ID` from your `.env.local` (and
|
|
171
|
+
`SUPABASE_URL` is read from there too); `--role` defaults to `admin`. Pass
|
|
172
|
+
either explicitly to override. Creates the account if it doesn't exist (or reuses it if it does — the same
|
|
173
|
+
person can hold different roles on different sites) via Supabase's own Admin
|
|
174
|
+
API, then writes the membership row. Needs the **service_role** key, not the
|
|
175
|
+
anon key, since it bypasses RLS by design — Settings > API in the dashboard.
|
|
176
|
+
Pass it inline for this one command rather than storing it anywhere; nothing
|
|
177
|
+
else in Guardrail ever needs it. Prefer raw SQL instead? The same insert is
|
|
178
|
+
still documented at the bottom of the init migration.
|
|
134
179
|
|
|
135
180
|
The bundled drawer lives behind the toolbar's **Team** button. To build your
|
|
136
181
|
own instead, `listTeam`, `inviteMember`, `updateMemberRole`, and `removeMember`
|
|
@@ -258,6 +303,21 @@ scratch. A visual page builder (Puck, say) is a better fit for that and can
|
|
|
258
303
|
sit alongside `<GRepeater>` later — it would decide composition, while
|
|
259
304
|
`<GRail>` still owns each block's leaf content.
|
|
260
305
|
|
|
306
|
+
## Appearance
|
|
307
|
+
|
|
308
|
+
Guardrail's own chrome (sign-in bar, toolbar, edit popups, image picker, team
|
|
309
|
+
drawer) follows the visitor's OS light/dark preference via `prefers-color-scheme`
|
|
310
|
+
— a dark-blue surface with light text in dark mode, white with dark text in light
|
|
311
|
+
mode. Popups render in a portal on `<body>`, so a field nested inside a z-indexed
|
|
312
|
+
or transformed container can't trap its own popup underneath other page content.
|
|
313
|
+
Your own markup is never restyled — including image fields. Instead of
|
|
314
|
+
outlining the `<img>` itself, an image field gets a dashed outline and a small
|
|
315
|
+
**Edit** handle drawn as separate elements over its measured position. That's what
|
|
316
|
+
makes it work for a `next/image` `fill` background with content layered on top:
|
|
317
|
+
the outline ignores the pointer so your overlaid content stays fully clickable,
|
|
318
|
+
and the handle is the only thing that opens the editor (it also won't trigger a
|
|
319
|
+
surrounding `<Link>`).
|
|
320
|
+
|
|
261
321
|
## Extending the toolbar
|
|
262
322
|
|
|
263
323
|
The bundled toolbar (edit indicator, Team, Log out) has one extension point:
|
|
@@ -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.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Owns the theme stylesheet so every piece of chrome — the sign-in bar, the
|
|
6
|
+
* toolbar, and the field popups portaled to <body> — picks up the same
|
|
7
|
+
* light/dark variables from one place.
|
|
8
|
+
*/
|
|
9
|
+
declare function GRailAdmin(props: {
|
|
5
10
|
toolbarExtra?: React.ReactNode;
|
|
6
11
|
}): react_jsx_runtime.JSX.Element | null;
|
|
7
12
|
|
package/dist/react/Admin.js
CHANGED
|
@@ -6,9 +6,17 @@ import { loginAction, logoutAction } from "../server/actions";
|
|
|
6
6
|
import { useGRail } from "./Provider";
|
|
7
7
|
import { TeamDrawer } from "./Team";
|
|
8
8
|
import { LogOutIcon, PeopleIcon } from "./Icons";
|
|
9
|
-
import { S } from "./styles";
|
|
9
|
+
import { S, THEME_CSS, v } from "./styles";
|
|
10
10
|
const initialState = { email: "", password: "" };
|
|
11
|
-
function GRailAdmin(
|
|
11
|
+
function GRailAdmin(props) {
|
|
12
|
+
const { isEditMode } = useGRail();
|
|
13
|
+
if (!isEditMode) return null;
|
|
14
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
15
|
+
/* @__PURE__ */ jsx("style", { children: THEME_CSS }),
|
|
16
|
+
/* @__PURE__ */ jsx(AdminBody, { ...props })
|
|
17
|
+
] });
|
|
18
|
+
}
|
|
19
|
+
function AdminBody({ toolbarExtra }) {
|
|
12
20
|
const { isAuth, isEditMode, role, auth } = useGRail();
|
|
13
21
|
const [teamOpen, setTeamOpen] = useState(false);
|
|
14
22
|
const [formState, formAction, isPending] = useActionState(
|
|
@@ -31,7 +39,7 @@ function GRailAdmin({ toolbarExtra }) {
|
|
|
31
39
|
return /* @__PURE__ */ jsx("div", { "data-guardrail": true, style: S.banner, children: /* @__PURE__ */ jsxs("div", { style: S.bannerForm, children: [
|
|
32
40
|
/* @__PURE__ */ jsxs("div", { style: S.brandStack, children: [
|
|
33
41
|
/* @__PURE__ */ jsx("span", { style: S.brand, children: "Guardrail" }),
|
|
34
|
-
!auth.signInUrl && /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: "
|
|
42
|
+
!auth.signInUrl && /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: v("muted") }, children: "No sign-in method is configured for this site." })
|
|
35
43
|
] }),
|
|
36
44
|
auth.signInUrl && /* @__PURE__ */ jsx("a", { href: auth.signInUrl, style: { ...S.primaryBtn, textDecoration: "none" }, children: "Sign in" })
|
|
37
45
|
] }) });
|
|
@@ -72,7 +80,7 @@ function GRailAdmin({ toolbarExtra }) {
|
|
|
72
80
|
/* @__PURE__ */ jsx(
|
|
73
81
|
"button",
|
|
74
82
|
{
|
|
75
|
-
style: { ...S.iconBtn, color: "
|
|
83
|
+
style: { ...S.iconBtn, color: v("muted") },
|
|
76
84
|
onClick: signOut,
|
|
77
85
|
title: "Log out",
|
|
78
86
|
"aria-label": "Log out",
|
|
@@ -90,7 +98,7 @@ function GRailAdmin({ toolbarExtra }) {
|
|
|
90
98
|
/* @__PURE__ */ jsx(
|
|
91
99
|
"button",
|
|
92
100
|
{
|
|
93
|
-
style: { ...S.iconBtn, color: "
|
|
101
|
+
style: { ...S.iconBtn, color: v("muted"), width: 24, height: 24 },
|
|
94
102
|
onClick: () => setTeamOpen(true),
|
|
95
103
|
title: "Team",
|
|
96
104
|
"aria-label": "Team",
|
|
@@ -105,7 +113,7 @@ function GRailAdmin({ toolbarExtra }) {
|
|
|
105
113
|
/* @__PURE__ */ jsx(
|
|
106
114
|
"button",
|
|
107
115
|
{
|
|
108
|
-
style: { ...S.iconBtn, color: "
|
|
116
|
+
style: { ...S.iconBtn, color: v("muted") },
|
|
109
117
|
onClick: signOut,
|
|
110
118
|
title: "Log out",
|
|
111
119
|
"aria-label": "Log out",
|
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 <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {!auth.signInUrl && (\n <span style={{ fontSize: 12, color: \"
|
|
1
|
+
{"version":3,"sources":["../../src/react/Admin.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useActionState, useEffect, useState } from \"react\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport { loginAction, logoutAction } from \"../server/actions\";\nimport { useGRail } from \"./Provider\";\nimport { TeamDrawer } from \"./Team\";\nimport { LogOutIcon, PeopleIcon } from \"./Icons\";\nimport { S, THEME_CSS, v } from \"./styles\";\nimport type { ActionState } from \"../core/types\";\n\nconst initialState: ActionState = { email: \"\", password: \"\" };\n\n/**\n * Owns the theme stylesheet so every piece of chrome — the sign-in bar, the\n * toolbar, and the field popups portaled to <body> — picks up the same\n * light/dark variables from one place.\n */\nexport function GRailAdmin(props: { toolbarExtra?: React.ReactNode }) {\n const { isEditMode } = useGRail();\n if (!isEditMode) return null;\n return (\n <>\n <style>{THEME_CSS}</style>\n <AdminBody {...props} />\n </>\n );\n}\n\nfunction AdminBody({ toolbarExtra }: { toolbarExtra?: React.ReactNode }) {\n const { isAuth, isEditMode, role, auth } = useGRail();\n const [teamOpen, setTeamOpen] = useState(false);\n const [formState, formAction, isPending] = useActionState(\n loginAction,\n initialState,\n );\n const router = useRouter();\n const pathname = usePathname();\n\n useEffect(() => {\n if (formState.success) router.refresh();\n }, [formState.success, router]);\n\n const signOut = async () => {\n await logoutAction();\n document.cookie =\n \"gr_edit_mode=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT\";\n window.location.href = pathname;\n };\n\n // No intent signalled — render nothing at all.\n if (!isEditMode) return null;\n\n if (!isAuth) {\n // The adapter cannot take a password here, so hand sign-in off rather than\n // showing a form that could never succeed.\n if (!auth.password) {\n return (\n <div data-guardrail style={S.banner}>\n <div style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {!auth.signInUrl && (\n <span style={{ fontSize: 12, color: v(\"muted\") }}>\n No sign-in method is configured for this site.\n </span>\n )}\n </div>\n {auth.signInUrl && (\n <a href={auth.signInUrl} style={{ ...S.primaryBtn, textDecoration: \"none\" }}>\n Sign in\n </a>\n )}\n </div>\n </div>\n );\n }\n\n return (\n <div data-guardrail style={S.banner}>\n <form action={formAction} style={S.bannerForm}>\n <div style={S.brandStack}>\n <span style={S.brand}>Guardrail</span>\n {formState.error && <span style={S.error}>{formState.error}</span>}\n </div>\n <input\n name=\"email\"\n type=\"email\"\n placeholder=\"Email\"\n defaultValue={formState.email}\n required\n style={S.input}\n />\n <input\n name=\"password\"\n type=\"password\"\n placeholder=\"Password\"\n required\n style={S.input}\n />\n <button type=\"submit\" disabled={isPending} style={S.primaryBtn}>\n {isPending ? \"Authenticating…\" : \"Sign In\"}\n </button>\n </form>\n </div>\n );\n }\n\n // Signed in, but holds no role on this site.\n if (!role) {\n return (\n <div data-guardrail style={S.toolbar}>\n <span style={S.toolbarLabel}>No access to this site</span>\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={signOut}\n title=\"Log out\"\n aria-label=\"Log out\"\n >\n <LogOutIcon />\n </button>\n </div>\n );\n }\n\n return (\n <>\n {teamOpen && <TeamDrawer onClose={() => setTeamOpen(false)} />}\n <div data-guardrail style={S.toolbar}>\n <span style={S.dot} />\n <span style={S.toolbarLabel}>Edit Mode</span>\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\"), width: 24, height: 24 }}\n onClick={() => setTeamOpen(true)}\n title=\"Team\"\n aria-label=\"Team\"\n >\n <PeopleIcon />\n </button>\n\n {toolbarExtra && (\n <>\n <span style={S.divider} />\n {toolbarExtra}\n </>\n )}\n\n <span style={S.divider} />\n <button\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={signOut}\n title=\"Log out\"\n aria-label=\"Log out\"\n >\n <LogOutIcon />\n </button>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import React, { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
4
5
|
import { saveOverride } from "../../server/actions";
|
|
5
6
|
import { GRailRender } from "./render";
|
|
6
7
|
import { useGRail } from "../Provider";
|
|
7
8
|
import { useAnchorRect } from "../useAnchorRect";
|
|
8
|
-
import { CheckIcon, CloseIcon, SpinnerIcon } from "../Icons";
|
|
9
|
+
import { CheckIcon, CloseIcon, PencilIcon, SpinnerIcon } from "../Icons";
|
|
9
10
|
import { S } from "../styles";
|
|
10
11
|
import { MediaPicker } from "../MediaPicker";
|
|
11
12
|
function GRailClient({
|
|
@@ -19,7 +20,8 @@ function GRailClient({
|
|
|
19
20
|
const [error, setError] = useState(null);
|
|
20
21
|
const [row, setRow] = useState(initialOverride);
|
|
21
22
|
const elementRef = useRef(null);
|
|
22
|
-
const
|
|
23
|
+
const isImage = row.value.type === "image";
|
|
24
|
+
const rect = useAnchorRect(elementRef, editing || isEditMode && isImage);
|
|
23
25
|
const sessionTextRef = useRef(initialOverride.value.text ?? "");
|
|
24
26
|
const draftTextRef = useRef(initialOverride.value.text ?? "");
|
|
25
27
|
const isInline = row.value.type === "text" || row.value.type === "button";
|
|
@@ -78,9 +80,14 @@ function GRailClient({
|
|
|
78
80
|
override: renderedValue,
|
|
79
81
|
style: {
|
|
80
82
|
...htmlProps.style,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
// Images are left alone — no outline, position, or z-index — and
|
|
84
|
+
// get a separate ImageMarker instead. Restyling the <img> itself
|
|
85
|
+
// can't work for a background image with content layered over it.
|
|
86
|
+
...isImage ? { cursor: "pointer" } : {
|
|
87
|
+
outline: editing ? "2px solid #2563eb" : "1px dashed #60a5fa",
|
|
88
|
+
outlineOffset: 2,
|
|
89
|
+
cursor: editing ? "text" : "pointer"
|
|
90
|
+
}
|
|
84
91
|
},
|
|
85
92
|
onClick: (e) => {
|
|
86
93
|
if (editing) return;
|
|
@@ -106,18 +113,82 @@ function GRailClient({
|
|
|
106
113
|
} : {}
|
|
107
114
|
}
|
|
108
115
|
),
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
rect && (isImage || editing) && createPortal(
|
|
117
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
118
|
+
isImage && /* @__PURE__ */ jsx(ImageMarker, { rect, editing, onEdit: beginEdit }),
|
|
119
|
+
editing && /* @__PURE__ */ jsx(
|
|
120
|
+
Chrome,
|
|
121
|
+
{
|
|
122
|
+
rect,
|
|
123
|
+
isInline,
|
|
124
|
+
row,
|
|
125
|
+
error,
|
|
126
|
+
saving,
|
|
127
|
+
onChange: updateAttribute,
|
|
128
|
+
onPatch: patchValue,
|
|
129
|
+
onSave: handleSave,
|
|
130
|
+
onCancel: handleCancel
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
] }),
|
|
134
|
+
document.body
|
|
135
|
+
)
|
|
136
|
+
] });
|
|
137
|
+
}
|
|
138
|
+
function ImageMarker({
|
|
139
|
+
rect,
|
|
140
|
+
editing,
|
|
141
|
+
onEdit
|
|
142
|
+
}) {
|
|
143
|
+
const height = rect.bottom - rect.top;
|
|
144
|
+
if (rect.width <= 0 || height <= 0) return null;
|
|
145
|
+
if (typeof window !== "undefined" && (rect.bottom < 0 || rect.top > window.innerHeight)) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const HANDLE_H = 26;
|
|
149
|
+
const small = height < HANDLE_H * 2 || rect.width < 120;
|
|
150
|
+
let handleTop;
|
|
151
|
+
if (small) {
|
|
152
|
+
handleTop = rect.top - HANDLE_H - 4 >= 4 ? rect.top - HANDLE_H - 4 : rect.bottom + 4;
|
|
153
|
+
} else {
|
|
154
|
+
handleTop = Math.min(
|
|
155
|
+
Math.max(rect.top + 6, 6),
|
|
156
|
+
Math.max(rect.top + 6, rect.bottom - HANDLE_H - 6)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const handleLeft = small ? rect.left : rect.left + 6;
|
|
160
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
161
|
+
/* @__PURE__ */ jsx(
|
|
162
|
+
"div",
|
|
163
|
+
{
|
|
164
|
+
"data-guardrail": true,
|
|
165
|
+
style: {
|
|
166
|
+
...S.markerOutline,
|
|
167
|
+
top: rect.top - 2,
|
|
168
|
+
left: rect.left - 2,
|
|
169
|
+
width: rect.width + 4,
|
|
170
|
+
height: height + 4,
|
|
171
|
+
border: editing ? "2px solid #2563eb" : "1px dashed #60a5fa"
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
),
|
|
175
|
+
!editing && /* @__PURE__ */ jsxs(
|
|
176
|
+
"button",
|
|
111
177
|
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
178
|
+
type: "button",
|
|
179
|
+
"data-guardrail": true,
|
|
180
|
+
style: { ...S.markerHandle, top: handleTop, left: handleLeft },
|
|
181
|
+
"aria-label": "Edit image",
|
|
182
|
+
title: "Edit image",
|
|
183
|
+
onClick: (e) => {
|
|
184
|
+
e.preventDefault();
|
|
185
|
+
e.stopPropagation();
|
|
186
|
+
onEdit();
|
|
187
|
+
},
|
|
188
|
+
children: [
|
|
189
|
+
/* @__PURE__ */ jsx(PencilIcon, {}),
|
|
190
|
+
" Edit"
|
|
191
|
+
]
|
|
121
192
|
}
|
|
122
193
|
)
|
|
123
194
|
] });
|
|
@@ -1 +1 @@
|
|
|
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 }}\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":";AA4GW,SAUP,UAVO,KAUP,YAVO;AA1GX,OAAO,SAAS,WAAW,QAAQ,gBAAgB;AACnD,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAC9B,SAAS,WAAW,WAAW,mBAAmB;AAClD,SAAS,SAAS;AAClB,SAAS,mBAAmB;AAGrB,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAIG;AACD,QAAM,EAAE,WAAW,IAAI,SAAS;AAChC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,CAAC,KAAK,MAAM,IAAI,SAAsB,eAAe;AAE3D,QAAM,aAAa,OAA2B,IAAI;AAClD,QAAM,OAAO,cAAc,YAAY,OAAO;AAY9C,QAAM,iBAAiB,OAAe,gBAAgB,MAAM,QAAQ,EAAE;AAEtE,QAAM,eAAe,OAAe,gBAAgB,MAAM,QAAQ,EAAE;AAEpE,QAAM,WAAW,IAAI,MAAM,SAAS,UAAU,IAAI,MAAM,SAAS;AAEjE,QAAM,YAAY,MAAM;AACtB,mBAAe,UAAU,IAAI,MAAM,QAAQ;AAC3C,iBAAa,UAAU,IAAI,MAAM,QAAQ;AACzC,aAAS,IAAI;AACb,eAAW,IAAI;AAAA,EACjB;AAEA,QAAM,kBAAkB,CAAC,KAAa,UACpC,OAAO,CAAC,UAAU,EAAE,GAAG,MAAM,OAAO,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,GAAG,MAAM,EAAE,EAAE;AAExE,QAAM,aAAa,CAAC,UAClB,OAAO,CAAC,UAAU,EAAE,GAAG,MAAM,OAAO,EAAE,GAAG,KAAK,OAAO,GAAG,MAAM,EAAE,EAAE;AAEpE,QAAM,aAAa,YAAY;AAE7B,UAAM,OAAoB,WACtB,EAAE,GAAG,KAAK,OAAO,EAAE,GAAG,IAAI,OAAO,MAAM,aAAa,QAAQ,EAAE,IAC9D;AAEJ,cAAU,IAAI;AACd,aAAS,IAAI;AACb,UAAM,MAAM,MAAM,aAAa,IAAI;AACnC,cAAU,KAAK;AAEf,QAAI,CAAC,IAAI,SAAS;AAChB,eAAS,IAAI,SAAS,cAAc;AACpC;AAAA,IACF;AACA,WAAO,IAAI;AACX,eAAW,KAAK;AAAA,EAClB;AAEA,QAAM,eAAe,MAAM;AACzB,WAAO,eAAe;AACtB,iBAAa,UAAU,gBAAgB,MAAM,QAAQ;AACrD,QAAI,YAAY,WAAW,SAAS;AAClC,iBAAW,QAAQ,cAAc,gBAAgB,MAAM,QAAQ;AAAA,IACjE;AACA,aAAS,IAAI;AACb,eAAW,KAAK;AAAA,EAClB;AAQA,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,UAAM,oBAAoB,CAAC,MAAkB;AAC3C,YAAM,SAAS,EAAE;AACjB,UAAI,WAAW,SAAS,SAAS,MAAM,EAAG;AAC1C,UAAI,kBAAkB,WAAW,OAAO,QAAQ,kBAAkB,EAAG;AACrE,mBAAa;AAAA,IACf;AACA,aAAS,iBAAiB,aAAa,iBAAiB;AACxD,WAAO,MAAM,SAAS,oBAAoB,aAAa,iBAAiB;AAAA,EAE1E,GAAG,CAAC,OAAO,CAAC;AAEZ,MAAI,CAAC,YAAY;AACf,WAAO,oBAAC,eAAY,IAAS,GAAG,WAAW,UAAU,IAAI,OAAO;AAAA,EAClE;AAGA,QAAM,gBACJ,WAAW,WACP,EAAE,GAAG,IAAI,OAAO,MAAM,eAAe,QAAQ,IAC7C,IAAI;AAEV,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACC,GAAG;AAAA,QACJ,KAAK;AAAA,QACL,UAAU;AAAA,QACV,OAAO;AAAA,UACL,GAAI,UAAU;AAAA,UACd,SAAS,UAAU,sBAAsB;AAAA,UACzC,eAAe;AAAA,UACf,QAAQ,UAAU,SAAS;AAAA,QAC7B;AAAA,QACA,SAAS,CAAC,MAAwB;AAChC,cAAI,QAAS;AACb,YAAE,eAAe;AAQjB,YAAE,gBAAgB;AAClB,oBAAU;AAAA,QACZ;AAAA,QAGC,GAAI,WACD;AAAA,UACE,iBAAiB;AAAA,UACjB,gCAAgC;AAAA,UAChC,SAAS,CAAC,MAAoC;AAE5C,yBAAa,UAAU,EAAE,cAAc,eAAe;AAAA,UACxD;AAAA,UACA,WAAW,CAAC,MAA2B;AACrC,gBAAI,EAAE,QAAQ,WAAW,CAAC,EAAE,UAAU;AACpC,gBAAE,eAAe;AACjB,mBAAK,WAAW;AAAA,YAClB,WAAW,EAAE,QAAQ,UAAU;AAC7B,gBAAE,eAAe;AACjB,2BAAa;AAAA,YACf;AAAA,UACF;AAAA,QACF,IACA,CAAC;AAAA;AAAA,IACP;AAAA,IAEC,WAAW,QACV;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,UAAU;AAAA;AAAA,IACZ;AAAA,KAEJ;AAEJ;AAEA,SAAS,OAAO;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAUG;AACD,QAAM,EAAE,QAAQ,IAAI,SAAS;AAC7B,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,MAAM;AAEZ,QAAM,QAAQ,KAAK,SAAS;AAC5B,QAAM,OAAO,OAAO,WAAW,eAAe,QAAQ,OAAO,cAAc;AAE3E,QAAM,WAAgC;AAAA,IACpC,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI;AAAA,IAC3B,GAAI,OAAO,EAAE,KAAK,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,MAAM;AAAA,EACtE;AAEA,QAAM,UACJ,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE;AAAA,QACT,SAAS;AAAA,QACT,UAAU;AAAA,QACV,OAAM;AAAA,QACN,cAAW;AAAA,QAEV,mBAAS,oBAAC,eAAY,IAAK,oBAAC,aAAU;AAAA;AAAA,IACzC;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE;AAAA,QACT,SAAS;AAAA,QACT,UAAU;AAAA,QACV,OAAM;AAAA,QACN,cAAW;AAAA,QAEX,8BAAC,aAAU;AAAA;AAAA,IACb;AAAA,KACF;AAIF,MAAI,UAAU;AACZ,WACE,qBAAC,SAAI,kBAAc,MAAC,OAAO,EAAE,GAAG,EAAE,aAAa,GAAG,SAAS,GACxD;AAAA;AAAA,MACA,SAAS,oBAAC,UAAK,OAAO,EAAE,GAAG,EAAE,OAAO,QAAQ,QAAQ,GAAI,iBAAM;AAAA,OACjE;AAAA,EAEJ;AAGA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,kBAAc;AAAA,MACd,OAAO,EAAE,GAAG,EAAE,eAAe,GAAG,SAAS;AAAA,MACzC,SAAS,CAAC,MAAM,EAAE,gBAAgB;AAAA,MAEjC;AAAA,YAAI,MAAM,SAAS,WAClB,iCACG;AAAA,cAAI,MAAM;AAAA,UAET,oBAAC,SAAI,KAAK,IAAI,MAAM,KAAK,KAAI,IAAG,OAAO,EAAE,cAAc;AAAA,UAGxD,WACC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,OAAO,EAAE;AAAA,cACT,SAAS,MAAM,cAAc,IAAI;AAAA,cAEhC,cAAI,MAAM,MAAM,kBAAkB;AAAA;AAAA,UACrC;AAAA,UAGF,oBAAC,WAAM,OAAO,EAAE,OAAO,sBAAQ;AAAA,UAC/B;AAAA,YAAC;AAAA;AAAA,cACC,OAAO,EAAE;AAAA,cACT,OAAO,IAAI,MAAM,OAAO;AAAA,cACxB,aAAY;AAAA,cACZ,UAAU,CAAC,MAAM,SAAS,OAAO,EAAE,OAAO,KAAK;AAAA;AAAA,UACjD;AAAA,UAKC,CAAC,WACA,iCACE;AAAA,gCAAC,WAAM,OAAO,EAAE,OAAO,uBAAS;AAAA,YAChC;AAAA,cAAC;AAAA;AAAA,gBACC,OAAO,EAAE;AAAA,gBACT,OAAO,IAAI,MAAM,OAAO;AAAA,gBACxB,UAAU,CAAC,MACT,QAAQ,EAAE,KAAK,EAAE,OAAO,OAAO,OAAO,QAAW,QAAQ,OAAU,CAAC;AAAA;AAAA,YAExE;AAAA,aACF;AAAA,UAGD,cACC;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,IAAI,MAAM;AAAA,cACnB,SAAS,MAAM,cAAc,KAAK;AAAA,cAClC,UAAU,CAAC,UACT,QAAQ,EAAE,KAAK,MAAM,KAAK,OAAO,MAAM,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA;AAAA,UAExE;AAAA,WAEJ;AAAA,QAGD,IAAI,MAAM,SAAS,UAClB,iCACE;AAAA,8BAAC,WAAM,OAAO,EAAE,OAAO,uBAAS;AAAA,UAChC;AAAA,YAAC;AAAA;AAAA,cACC,OAAO,EAAE;AAAA,cACT,OAAO,IAAI,MAAM,QAAQ;AAAA,cACzB,UAAU,CAAC,MAAM,SAAS,QAAQ,EAAE,OAAO,KAAK;AAAA;AAAA,UAClD;AAAA,UACA,oBAAC,WAAM,OAAO,EAAE,OAAO,iBAAG;AAAA,UAC1B;AAAA,YAAC;AAAA;AAAA,cACC,OAAO,EAAE;AAAA,cACT,OAAO,IAAI,MAAM,QAAQ;AAAA,cACzB,UAAU,CAAC,MAAM,SAAS,QAAQ,EAAE,OAAO,KAAK;AAAA;AAAA,UAClD;AAAA,WACF;AAAA,QAGD,SAAS,oBAAC,SAAI,OAAO,EAAE,GAAG,EAAE,OAAO,cAAc,EAAE,GAAI,iBAAM;AAAA,QAC9D,oBAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,gBAAgB,WAAW,GAC/D,mBACH;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/react/GRail/client.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { saveOverride } from \"../../server/actions\";\nimport { GRailRender } from \"./render\";\nimport { useGRail } from \"../Provider\";\nimport { useAnchorRect } from \"../useAnchorRect\";\nimport { CheckIcon, CloseIcon, PencilIcon, 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 isImage = row.value.type === \"image\";\n // Image fields keep a rect for their whole time in edit mode (their marker\n // needs it); everything else only while actively editing.\n const rect = useAnchorRect(elementRef, editing || (isEditMode && isImage));\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 // Images are left alone — no outline, position, or z-index — and\n // get a separate ImageMarker instead. Restyling the <img> itself\n // can't work for a background image with content layered over it.\n ...(isImage\n ? { cursor: \"pointer\" }\n : {\n outline: editing ? \"2px solid #2563eb\" : \"1px dashed #60a5fa\",\n outlineOffset: 2,\n cursor: editing ? \"text\" : \"pointer\",\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 {/*\n Portaled to <body>, not rendered next to the field. The chrome is\n position:fixed with a high z-index, but z-index only ranks within the\n nearest stacking context — rendered inline, a field inside any ancestor\n that forms one (a z-indexed or transformed hero, say) traps its own\n popup there, where unrelated content stacked higher elsewhere on the\n page can paint straight over it. Also sidesteps a transformed ancestor\n silently re-anchoring position:fixed to itself instead of the viewport.\n */}\n {rect &&\n (isImage || editing) &&\n createPortal(\n <>\n {isImage && (\n <ImageMarker rect={rect} editing={editing} onEdit={beginEdit} />\n )}\n {editing && (\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 document.body,\n )}\n </>\n );\n}\n\n/**\n * Dashed outline plus an \"Edit\" handle for an image field, positioned from the\n * image's measured rect rather than attached to the image. Everything here is\n * portaled to <body> by the caller, so it ranks above page content whatever the\n * image's own ancestors do, and never shifts the developer's layout.\n */\nfunction ImageMarker({\n rect,\n editing,\n onEdit,\n}: {\n rect: { top: number; left: number; bottom: number; width: number };\n editing: boolean;\n onEdit: () => void;\n}) {\n const height = rect.bottom - rect.top;\n // Collapsed (display:none, zero-size) or scrolled fully out of view.\n if (rect.width <= 0 || height <= 0) return null;\n if (typeof window !== \"undefined\" && (rect.bottom < 0 || rect.top > window.innerHeight)) {\n return null;\n }\n\n const HANDLE_H = 26;\n // A logo-sized image would be mostly covered by its own handle, so small\n // ones get it just outside (above, or below when there's no room above).\n const small = height < HANDLE_H * 2 || rect.width < 120;\n let handleTop: number;\n if (small) {\n handleTop = rect.top - HANDLE_H - 4 >= 4 ? rect.top - HANDLE_H - 4 : rect.bottom + 4;\n } else {\n // Stays on screen for a tall background that's partly scrolled past, but\n // never leaves the image's own bounds.\n handleTop = Math.min(\n Math.max(rect.top + 6, 6),\n Math.max(rect.top + 6, rect.bottom - HANDLE_H - 6),\n );\n }\n const handleLeft = small ? rect.left : rect.left + 6;\n\n return (\n <>\n <div\n data-guardrail\n style={{\n ...S.markerOutline,\n top: rect.top - 2,\n left: rect.left - 2,\n width: rect.width + 4,\n height: height + 4,\n border: editing ? \"2px solid #2563eb\" : \"1px dashed #60a5fa\",\n }}\n />\n {!editing && (\n <button\n type=\"button\"\n data-guardrail\n style={{ ...S.markerHandle, top: handleTop, left: handleLeft }}\n aria-label=\"Edit image\"\n title=\"Edit image\"\n onClick={(e) => {\n // React bubbles synthetic events through portals to the field's\n // *React* ancestors — a field inside a <Link> would otherwise\n // navigate on this click, same reason the field's own onClick\n // claims its click.\n e.preventDefault();\n e.stopPropagation();\n onEdit();\n }}\n >\n <PencilIcon /> Edit\n </button>\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/Icons.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
2
2
|
|
|
3
3
|
declare const CheckIcon: () => react_jsx_runtime.JSX.Element;
|
|
4
4
|
declare const CloseIcon: () => react_jsx_runtime.JSX.Element;
|
|
5
|
+
declare const PencilIcon: () => react_jsx_runtime.JSX.Element;
|
|
5
6
|
declare const PeopleIcon: () => react_jsx_runtime.JSX.Element;
|
|
6
7
|
declare const PlusIcon: () => react_jsx_runtime.JSX.Element;
|
|
7
8
|
declare const TrashIcon: () => react_jsx_runtime.JSX.Element;
|
|
@@ -10,4 +11,4 @@ declare const ArrowDownIcon: () => react_jsx_runtime.JSX.Element;
|
|
|
10
11
|
declare const LogOutIcon: () => react_jsx_runtime.JSX.Element;
|
|
11
12
|
declare const SpinnerIcon: () => react_jsx_runtime.JSX.Element;
|
|
12
13
|
|
|
13
|
-
export { ArrowDownIcon, ArrowUpIcon, CheckIcon, CloseIcon, LogOutIcon, PeopleIcon, PlusIcon, SpinnerIcon, TrashIcon };
|
|
14
|
+
export { ArrowDownIcon, ArrowUpIcon, CheckIcon, CloseIcon, LogOutIcon, PencilIcon, PeopleIcon, PlusIcon, SpinnerIcon, TrashIcon };
|
package/dist/react/Icons.js
CHANGED
|
@@ -15,6 +15,10 @@ const CloseIcon = () => /* @__PURE__ */ jsxs("svg", { ...base, "aria-hidden": "t
|
|
|
15
15
|
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
16
16
|
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
17
17
|
] });
|
|
18
|
+
const PencilIcon = () => /* @__PURE__ */ jsxs("svg", { ...base, strokeWidth: 2, "aria-hidden": "true", children: [
|
|
19
|
+
/* @__PURE__ */ jsx("path", { d: "M12 20h9" }),
|
|
20
|
+
/* @__PURE__ */ jsx("path", { d: "M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" })
|
|
21
|
+
] });
|
|
18
22
|
const PeopleIcon = () => /* @__PURE__ */ jsxs("svg", { ...base, strokeWidth: 2, "aria-hidden": "true", children: [
|
|
19
23
|
/* @__PURE__ */ jsx("path", { d: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" }),
|
|
20
24
|
/* @__PURE__ */ jsx("circle", { cx: "9", cy: "7", r: "4" }),
|
|
@@ -60,6 +64,7 @@ export {
|
|
|
60
64
|
CheckIcon,
|
|
61
65
|
CloseIcon,
|
|
62
66
|
LogOutIcon,
|
|
67
|
+
PencilIcon,
|
|
63
68
|
PeopleIcon,
|
|
64
69
|
PlusIcon,
|
|
65
70
|
SpinnerIcon,
|
package/dist/react/Icons.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/Icons.tsx"],"sourcesContent":["import React from \"react\";\n\n/**\n * Inline SVG rather than an icon package: Guardrail's chrome should not add a\n * runtime dependency to every consuming app.\n */\nconst base = {\n width: 14,\n height: 14,\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n strokeWidth: 3,\n strokeLinecap: \"round\" as const,\n strokeLinejoin: \"round\" as const,\n};\n\nexport const CheckIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n);\n\nexport const CloseIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n);\n\nexport const PeopleIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2\" />\n <circle cx=\"9\" cy=\"7\" r=\"4\" />\n <path d=\"M23 21v-2a4 4 0 0 0-3-3.87\" />\n <path d=\"M16 3.13a4 4 0 0 1 0 7.75\" />\n </svg>\n);\n\nexport const PlusIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\" />\n </svg>\n);\n\nexport const TrashIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <polyline points=\"3 6 5 6 21 6\" />\n <path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\" />\n <path d=\"M10 11v6M14 11v6M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2\" />\n </svg>\n);\n\nexport const ArrowUpIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"5\" />\n <polyline points=\"6 11 12 5 18 11\" />\n </svg>\n);\n\nexport const ArrowDownIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <polyline points=\"18 13 12 19 6 13\" />\n </svg>\n);\n\nexport const LogOutIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" />\n <polyline points=\"16 17 21 12 16 7\" />\n <line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\" />\n </svg>\n);\n\nexport const SpinnerIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <path d=\"M21 12a9 9 0 1 1-6.2-8.6\">\n <animateTransform\n attributeName=\"transform\"\n type=\"rotate\"\n from=\"0 12 12\"\n to=\"360 12 12\"\n dur=\"0.7s\"\n repeatCount=\"indefinite\"\n />\n </path>\n </svg>\n);\n"],"mappings":"AAmBI,cAKF,YALE;AAnBJ,OAAO,WAAW;AAMlB,MAAM,OAAO;AAAA,EACX,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,eAAe;AAAA,EACf,gBAAgB;AAClB;AAEO,MAAM,YAAY,MACvB,oBAAC,SAAK,GAAG,MAAM,eAAY,QACzB,8BAAC,cAAS,QAAO,kBAAiB,GACpC;AAGK,MAAM,YAAY,MACvB,qBAAC,SAAK,GAAG,MAAM,eAAY,QACzB;AAAA,sBAAC,UAAK,IAAG,MAAK,IAAG,KAAI,IAAG,KAAI,IAAG,MAAK;AAAA,EACpC,oBAAC,UAAK,IAAG,KAAI,IAAG,KAAI,IAAG,MAAK,IAAG,MAAK;AAAA,GACtC;AAGK,MAAM,aAAa,MACxB,qBAAC,SAAK,GAAG,MAAM,aAAa,GAAG,eAAY,QACzC;AAAA,sBAAC,UAAK,GAAE,6CAA4C;AAAA,EACpD,oBAAC,YAAO,IAAG,KAAI,IAAG,KAAI,GAAE,KAAI;AAAA,EAC5B,oBAAC,UAAK,GAAE,8BAA6B;AAAA,EACrC,oBAAC,UAAK,GAAE,6BAA4B;AAAA,GACtC;AAGK,MAAM,WAAW,MACtB,qBAAC,SAAK,GAAG,MAAM,eAAY,QACzB;AAAA,sBAAC,UAAK,IAAG,MAAK,IAAG,KAAI,IAAG,MAAK,IAAG,MAAK;AAAA,EACrC,oBAAC,UAAK,IAAG,KAAI,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK;AAAA,GACvC;AAGK,MAAM,YAAY,MACvB,qBAAC,SAAK,GAAG,MAAM,aAAa,GAAG,eAAY,QACzC;AAAA,sBAAC,cAAS,QAAO,gBAAe;AAAA,EAChC,oBAAC,UAAK,GAAE,iDAAgD;AAAA,EACxD,oBAAC,UAAK,GAAE,0DAAyD;AAAA,GACnE;AAGK,MAAM,cAAc,MACzB,qBAAC,SAAK,GAAG,MAAM,eAAY,QACzB;AAAA,sBAAC,UAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK,IAAG,KAAI;AAAA,EACrC,oBAAC,cAAS,QAAO,mBAAkB;AAAA,GACrC;AAGK,MAAM,gBAAgB,MAC3B,qBAAC,SAAK,GAAG,MAAM,eAAY,QACzB;AAAA,sBAAC,UAAK,IAAG,MAAK,IAAG,KAAI,IAAG,MAAK,IAAG,MAAK;AAAA,EACrC,oBAAC,cAAS,QAAO,oBAAmB;AAAA,GACtC;AAGK,MAAM,aAAa,MACxB,qBAAC,SAAK,GAAG,MAAM,aAAa,GAAG,eAAY,QACzC;AAAA,sBAAC,UAAK,GAAE,2CAA0C;AAAA,EAClD,oBAAC,cAAS,QAAO,oBAAmB;AAAA,EACpC,oBAAC,UAAK,IAAG,MAAK,IAAG,MAAK,IAAG,KAAI,IAAG,MAAK;AAAA,GACvC;AAGK,MAAM,cAAc,MACzB,oBAAC,SAAK,GAAG,MAAM,eAAY,QACzB,8BAAC,UAAK,GAAE,4BACN;AAAA,EAAC;AAAA;AAAA,IACC,eAAc;AAAA,IACd,MAAK;AAAA,IACL,MAAK;AAAA,IACL,IAAG;AAAA,IACH,KAAI;AAAA,IACJ,aAAY;AAAA;AACd,GACF,GACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/Icons.tsx"],"sourcesContent":["import React from \"react\";\n\n/**\n * Inline SVG rather than an icon package: Guardrail's chrome should not add a\n * runtime dependency to every consuming app.\n */\nconst base = {\n width: 14,\n height: 14,\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n strokeWidth: 3,\n strokeLinecap: \"round\" as const,\n strokeLinejoin: \"round\" as const,\n};\n\nexport const CheckIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n);\n\nexport const CloseIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n);\n\nexport const PencilIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M12 20h9\" />\n <path d=\"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z\" />\n </svg>\n);\n\nexport const PeopleIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2\" />\n <circle cx=\"9\" cy=\"7\" r=\"4\" />\n <path d=\"M23 21v-2a4 4 0 0 0-3-3.87\" />\n <path d=\"M16 3.13a4 4 0 0 1 0 7.75\" />\n </svg>\n);\n\nexport const PlusIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\" />\n </svg>\n);\n\nexport const TrashIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <polyline points=\"3 6 5 6 21 6\" />\n <path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\" />\n <path d=\"M10 11v6M14 11v6M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2\" />\n </svg>\n);\n\nexport const ArrowUpIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"5\" />\n <polyline points=\"6 11 12 5 18 11\" />\n </svg>\n);\n\nexport const ArrowDownIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <polyline points=\"18 13 12 19 6 13\" />\n </svg>\n);\n\nexport const LogOutIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" />\n <polyline points=\"16 17 21 12 16 7\" />\n <line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\" />\n </svg>\n);\n\nexport const SpinnerIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <path d=\"M21 12a9 9 0 1 1-6.2-8.6\">\n <animateTransform\n attributeName=\"transform\"\n type=\"rotate\"\n from=\"0 12 12\"\n to=\"360 12 12\"\n dur=\"0.7s\"\n repeatCount=\"indefinite\"\n />\n </path>\n </svg>\n);\n"],"mappings":"AAmBI,cAKF,YALE;AAnBJ,OAAO,WAAW;AAMlB,MAAM,OAAO;AAAA,EACX,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,eAAe;AAAA,EACf,gBAAgB;AAClB;AAEO,MAAM,YAAY,MACvB,oBAAC,SAAK,GAAG,MAAM,eAAY,QACzB,8BAAC,cAAS,QAAO,kBAAiB,GACpC;AAGK,MAAM,YAAY,MACvB,qBAAC,SAAK,GAAG,MAAM,eAAY,QACzB;AAAA,sBAAC,UAAK,IAAG,MAAK,IAAG,KAAI,IAAG,KAAI,IAAG,MAAK;AAAA,EACpC,oBAAC,UAAK,IAAG,KAAI,IAAG,KAAI,IAAG,MAAK,IAAG,MAAK;AAAA,GACtC;AAGK,MAAM,aAAa,MACxB,qBAAC,SAAK,GAAG,MAAM,aAAa,GAAG,eAAY,QACzC;AAAA,sBAAC,UAAK,GAAE,YAAW;AAAA,EACnB,oBAAC,UAAK,GAAE,2DAA0D;AAAA,GACpE;AAGK,MAAM,aAAa,MACxB,qBAAC,SAAK,GAAG,MAAM,aAAa,GAAG,eAAY,QACzC;AAAA,sBAAC,UAAK,GAAE,6CAA4C;AAAA,EACpD,oBAAC,YAAO,IAAG,KAAI,IAAG,KAAI,GAAE,KAAI;AAAA,EAC5B,oBAAC,UAAK,GAAE,8BAA6B;AAAA,EACrC,oBAAC,UAAK,GAAE,6BAA4B;AAAA,GACtC;AAGK,MAAM,WAAW,MACtB,qBAAC,SAAK,GAAG,MAAM,eAAY,QACzB;AAAA,sBAAC,UAAK,IAAG,MAAK,IAAG,KAAI,IAAG,MAAK,IAAG,MAAK;AAAA,EACrC,oBAAC,UAAK,IAAG,KAAI,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK;AAAA,GACvC;AAGK,MAAM,YAAY,MACvB,qBAAC,SAAK,GAAG,MAAM,aAAa,GAAG,eAAY,QACzC;AAAA,sBAAC,cAAS,QAAO,gBAAe;AAAA,EAChC,oBAAC,UAAK,GAAE,iDAAgD;AAAA,EACxD,oBAAC,UAAK,GAAE,0DAAyD;AAAA,GACnE;AAGK,MAAM,cAAc,MACzB,qBAAC,SAAK,GAAG,MAAM,eAAY,QACzB;AAAA,sBAAC,UAAK,IAAG,MAAK,IAAG,MAAK,IAAG,MAAK,IAAG,KAAI;AAAA,EACrC,oBAAC,cAAS,QAAO,mBAAkB;AAAA,GACrC;AAGK,MAAM,gBAAgB,MAC3B,qBAAC,SAAK,GAAG,MAAM,eAAY,QACzB;AAAA,sBAAC,UAAK,IAAG,MAAK,IAAG,KAAI,IAAG,MAAK,IAAG,MAAK;AAAA,EACrC,oBAAC,cAAS,QAAO,oBAAmB;AAAA,GACtC;AAGK,MAAM,aAAa,MACxB,qBAAC,SAAK,GAAG,MAAM,aAAa,GAAG,eAAY,QACzC;AAAA,sBAAC,UAAK,GAAE,2CAA0C;AAAA,EAClD,oBAAC,cAAS,QAAO,oBAAmB;AAAA,EACpC,oBAAC,UAAK,IAAG,MAAK,IAAG,MAAK,IAAG,KAAI,IAAG,MAAK;AAAA,GACvC;AAGK,MAAM,cAAc,MACzB,oBAAC,SAAK,GAAG,MAAM,eAAY,QACzB,8BAAC,UAAK,GAAE,4BACN;AAAA,EAAC;AAAA;AAAA,IACC,eAAc;AAAA,IACd,MAAK;AAAA,IACL,MAAK;AAAA,IACL,IAAG;AAAA,IACH,KAAI;AAAA,IACJ,aAAY;AAAA;AACd,GACF,GACF;","names":[]}
|