@guard-rail/core 0.2.0 → 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 +19 -3
- 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 +86 -27
- 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 +2 -1
package/README.md
CHANGED
|
@@ -164,11 +164,12 @@ as letting anyone make themselves an admin. Seed the first admin with the
|
|
|
164
164
|
bundled command instead of hand-writing SQL:
|
|
165
165
|
|
|
166
166
|
```bash
|
|
167
|
-
SERVICE_KEY=... npx guardrail-seed-user
|
|
168
|
-
--email=you@example.com --password=... --site-id=my-site --role=admin
|
|
167
|
+
SERVICE_KEY=... npx guardrail-seed-user --email=you@example.com --password=...
|
|
169
168
|
```
|
|
170
169
|
|
|
171
|
-
|
|
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
|
|
172
173
|
person can hold different roles on different sites) via Supabase's own Admin
|
|
173
174
|
API, then writes the membership row. Needs the **service_role** key, not the
|
|
174
175
|
anon key, since it bypasses RLS by design — Settings > API in the dashboard.
|
|
@@ -302,6 +303,21 @@ scratch. A visual page builder (Puck, say) is a better fit for that and can
|
|
|
302
303
|
sit alongside `<GRepeater>` later — it would decide composition, while
|
|
303
304
|
`<GRail>` still owns each block's leaf content.
|
|
304
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
|
+
|
|
305
321
|
## Extending the toolbar
|
|
306
322
|
|
|
307
323
|
The bundled toolbar (edit indicator, Team, Log out) has one extension point:
|
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,20 +80,13 @@ function GRailClient({
|
|
|
78
80
|
override: renderedValue,
|
|
79
81
|
style: {
|
|
80
82
|
...htmlProps.style,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
...
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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"
|
|
95
90
|
}
|
|
96
91
|
},
|
|
97
92
|
onClick: (e) => {
|
|
@@ -118,18 +113,82 @@ function GRailClient({
|
|
|
118
113
|
} : {}
|
|
119
114
|
}
|
|
120
115
|
),
|
|
121
|
-
|
|
122
|
-
|
|
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",
|
|
123
163
|
{
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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",
|
|
177
|
+
{
|
|
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
|
+
]
|
|
133
192
|
}
|
|
134
193
|
)
|
|
135
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 ...(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":[]}
|
|
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":";AAgHW,SA6ED,UA7EC,KA6ED,YA7EC;AA9GX,OAAO,SAAS,WAAW,QAAQ,gBAAgB;AACnD,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAC9B,SAAS,WAAW,WAAW,YAAY,mBAAmB;AAC9D,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,UAAU,IAAI,MAAM,SAAS;AAGnC,QAAM,OAAO,cAAc,YAAY,WAAY,cAAc,OAAQ;AAYzE,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;AAAA;AAAA;AAAA,UAId,GAAI,UACA,EAAE,QAAQ,UAAU,IACpB;AAAA,YACE,SAAS,UAAU,sBAAsB;AAAA,YACzC,eAAe;AAAA,YACf,QAAQ,UAAU,SAAS;AAAA,UAC7B;AAAA,QACN;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,IAWC,SACE,WAAW,YACZ;AAAA,MACE,iCACG;AAAA,mBACC,oBAAC,eAAY,MAAY,SAAkB,QAAQ,WAAW;AAAA,QAE/D,WACC;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA,UAAU;AAAA,YACV,SAAS;AAAA,YACT,QAAQ;AAAA,YACR,UAAU;AAAA;AAAA,QACZ;AAAA,SAEJ;AAAA,MACA,SAAS;AAAA,IACX;AAAA,KACJ;AAEJ;AAQA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,SAAS,KAAK,SAAS,KAAK;AAElC,MAAI,KAAK,SAAS,KAAK,UAAU,EAAG,QAAO;AAC3C,MAAI,OAAO,WAAW,gBAAgB,KAAK,SAAS,KAAK,KAAK,MAAM,OAAO,cAAc;AACvF,WAAO;AAAA,EACT;AAEA,QAAM,WAAW;AAGjB,QAAM,QAAQ,SAAS,WAAW,KAAK,KAAK,QAAQ;AACpD,MAAI;AACJ,MAAI,OAAO;AACT,gBAAY,KAAK,MAAM,WAAW,KAAK,IAAI,KAAK,MAAM,WAAW,IAAI,KAAK,SAAS;AAAA,EACrF,OAAO;AAGL,gBAAY,KAAK;AAAA,MACf,KAAK,IAAI,KAAK,MAAM,GAAG,CAAC;AAAA,MACxB,KAAK,IAAI,KAAK,MAAM,GAAG,KAAK,SAAS,WAAW,CAAC;AAAA,IACnD;AAAA,EACF;AACA,QAAM,aAAa,QAAQ,KAAK,OAAO,KAAK,OAAO;AAEnD,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,kBAAc;AAAA,QACd,OAAO;AAAA,UACL,GAAG,EAAE;AAAA,UACL,KAAK,KAAK,MAAM;AAAA,UAChB,MAAM,KAAK,OAAO;AAAA,UAClB,OAAO,KAAK,QAAQ;AAAA,UACpB,QAAQ,SAAS;AAAA,UACjB,QAAQ,UAAU,sBAAsB;AAAA,QAC1C;AAAA;AAAA,IACF;AAAA,IACC,CAAC,WACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,kBAAc;AAAA,QACd,OAAO,EAAE,GAAG,EAAE,cAAc,KAAK,WAAW,MAAM,WAAW;AAAA,QAC7D,cAAW;AAAA,QACX,OAAM;AAAA,QACN,SAAS,CAAC,MAAM;AAKd,YAAE,eAAe;AACjB,YAAE,gBAAgB;AAClB,iBAAO;AAAA,QACT;AAAA,QAEA;AAAA,8BAAC,cAAW;AAAA,UAAE;AAAA;AAAA;AAAA,IAChB;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":[]}
|
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":[]}
|
|
@@ -3,7 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { createAssetUpload, listAssets } from "../server/actions";
|
|
5
5
|
import { CloseIcon, SpinnerIcon } from "./Icons";
|
|
6
|
-
import { S } from "./styles";
|
|
6
|
+
import { S, v } from "./styles";
|
|
7
7
|
import { prepareImage } from "./resizeImage";
|
|
8
8
|
function measure(url) {
|
|
9
9
|
return new Promise((resolve) => {
|
|
@@ -96,7 +96,7 @@ function MediaPicker({
|
|
|
96
96
|
/* @__PURE__ */ jsx(
|
|
97
97
|
"button",
|
|
98
98
|
{
|
|
99
|
-
style: { ...S.iconBtn, color: "
|
|
99
|
+
style: { ...S.iconBtn, color: v("muted") },
|
|
100
100
|
onClick: onClose,
|
|
101
101
|
"aria-label": "Close",
|
|
102
102
|
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
@@ -146,7 +146,7 @@ function MediaPicker({
|
|
|
146
146
|
}
|
|
147
147
|
),
|
|
148
148
|
error && /* @__PURE__ */ jsx("div", { style: { ...S.error, marginBottom: 12 }, children: error }),
|
|
149
|
-
assets === null ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13, color: "
|
|
149
|
+
assets === null ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13, color: v("muted") }, children: "Loading\u2026" }) : assets.length === 0 ? /* @__PURE__ */ jsx("p", { style: { fontSize: 13, color: v("muted") }, children: "No images uploaded yet." }) : /* @__PURE__ */ jsx("div", { style: S.pickerGrid, children: assets.map((asset) => /* @__PURE__ */ jsx(
|
|
150
150
|
"button",
|
|
151
151
|
{
|
|
152
152
|
type: "button",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/MediaPicker.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createAssetUpload, listAssets } from \"../server/actions\";\nimport { CloseIcon, SpinnerIcon } from \"./Icons\";\nimport { S } from \"./styles\";\nimport { prepareImage } from \"./resizeImage\";\nimport type { GRAsset } from \"../core/types\";\n\nexport interface PickedImage {\n src: string;\n width?: number;\n height?: number;\n}\n\ntype Size = { width?: number; height?: number };\n\n/**\n * Natural size is read in the browser, so no dimensions need storing on the\n * storage side and every adapter gets them for free.\n */\nfunction measure(url: string): Promise<Size> {\n return new Promise((resolve) => {\n const img = new Image();\n img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });\n img.onerror = () => resolve({});\n img.src = url;\n });\n}\n\nexport function MediaPicker({\n current,\n onSelect,\n onClose,\n}: {\n current?: string;\n onSelect: (image: PickedImage) => void;\n onClose: () => void;\n}) {\n const [assets, setAssets] = useState<GRAsset[] | null>(null);\n const [uploading, setUploading] = useState(false);\n const [dragging, setDragging] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n void listAssets().then(setAssets);\n }, []);\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n e.stopPropagation();\n onClose();\n };\n window.addEventListener(\"keydown\", onKey, true);\n return () => window.removeEventListener(\"keydown\", onKey, true);\n }, [onClose]);\n\n const choose = useCallback(\n (src: string, size: Size) => {\n onSelect({ src, ...size });\n onClose();\n },\n [onSelect, onClose],\n );\n\n const upload = async (file: File) => {\n if (uploading) return;\n setError(null);\n setUploading(true);\n try {\n // Scaled first, so the size check and the upload both see the file\n // that will actually be stored — an oversized phone photo that shrinks\n // under the cap is accepted.\n const prepared = await prepareImage(file);\n const payload = prepared.file;\n\n const res = await createAssetUpload({\n name: payload.name,\n contentType: payload.type,\n size: payload.size,\n });\n if (!res.success) {\n setError(res.error);\n return;\n }\n const { target } = res;\n\n // Straight to storage — the bytes never touch the Next.js server.\n const put = await fetch(target.url, {\n method: target.method,\n headers: target.headers,\n body: payload,\n });\n if (!put.ok) {\n setError(`Upload failed (${put.status}).`);\n return;\n }\n\n setAssets((prev) => [target.asset, ...(prev ?? [])]);\n choose(target.asset.url, { width: prepared.width, height: prepared.height });\n } catch {\n setError(\"Upload failed. Check your connection and try again.\");\n } finally {\n setUploading(false);\n }\n };\n\n const onFiles = (files: FileList | null | undefined) => {\n const file = files?.[0];\n if (file) void upload(file);\n };\n\n return (\n <>\n <div data-guardrail style={S.pickerScrim} onClick={onClose} />\n <div\n data-guardrail\n role=\"dialog\"\n aria-label=\"Choose an image\"\n style={S.picker}\n onClick={(e) => e.stopPropagation()}\n >\n <header style={S.drawerHead}>\n <h2 style={S.drawerTitle}>Choose an image</h2>\n <button\n style={{ ...S.iconBtn, color: \"#6b7280\" }}\n onClick={onClose}\n aria-label=\"Close\"\n >\n <CloseIcon />\n </button>\n </header>\n\n <div style={S.drawerBody}>\n <div\n role=\"button\"\n tabIndex={0}\n style={{ ...S.dropzone, ...(dragging ? S.dropzoneActive : {}) }}\n onClick={() => !uploading && inputRef.current?.click()}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") inputRef.current?.click();\n }}\n onDragOver={(e) => {\n e.preventDefault();\n setDragging(true);\n }}\n onDragLeave={() => setDragging(false)}\n onDrop={(e) => {\n e.preventDefault();\n setDragging(false);\n onFiles(e.dataTransfer.files);\n }}\n >\n {uploading ? (\n <span style={{ display: \"inline-flex\", gap: 8, alignItems: \"center\" }}>\n <SpinnerIcon /> Uploading…\n </span>\n ) : (\n \"Drop an image here, or click to upload\"\n )}\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/png,image/jpeg,image/webp,image/gif,image/avif\"\n hidden\n onChange={(e) => {\n onFiles(e.target.files);\n e.target.value = \"\";\n }}\n />\n </div>\n\n {error && <div style={{ ...S.error, marginBottom: 12 }}>{error}</div>}\n\n {assets === null ? (\n <p style={{ fontSize: 13, color: \"#6b7280\" }}>Loading…</p>\n ) : assets.length === 0 ? (\n <p style={{ fontSize: 13, color: \"#6b7280\" }}>No images uploaded yet.</p>\n ) : (\n <div style={S.pickerGrid}>\n {assets.map((asset) => (\n <button\n key={asset.id}\n type=\"button\"\n title={asset.name}\n style={{\n ...S.pickerTile,\n ...(asset.url === current ? S.pickerTileActive : {}),\n }}\n onClick={async () => choose(asset.url, await measure(asset.url))}\n >\n {/* Plain img: the admin UI must not depend on next/image's\n remotePatterns being configured. */}\n <img src={asset.url} alt=\"\" loading=\"lazy\" style={S.pickerThumb} />\n </button>\n ))}\n </div>\n )}\n </div>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/MediaPicker.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createAssetUpload, listAssets } from \"../server/actions\";\nimport { CloseIcon, SpinnerIcon } from \"./Icons\";\nimport { S, v } from \"./styles\";\nimport { prepareImage } from \"./resizeImage\";\nimport type { GRAsset } from \"../core/types\";\n\nexport interface PickedImage {\n src: string;\n width?: number;\n height?: number;\n}\n\ntype Size = { width?: number; height?: number };\n\n/**\n * Natural size is read in the browser, so no dimensions need storing on the\n * storage side and every adapter gets them for free.\n */\nfunction measure(url: string): Promise<Size> {\n return new Promise((resolve) => {\n const img = new Image();\n img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });\n img.onerror = () => resolve({});\n img.src = url;\n });\n}\n\nexport function MediaPicker({\n current,\n onSelect,\n onClose,\n}: {\n current?: string;\n onSelect: (image: PickedImage) => void;\n onClose: () => void;\n}) {\n const [assets, setAssets] = useState<GRAsset[] | null>(null);\n const [uploading, setUploading] = useState(false);\n const [dragging, setDragging] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n void listAssets().then(setAssets);\n }, []);\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n e.stopPropagation();\n onClose();\n };\n window.addEventListener(\"keydown\", onKey, true);\n return () => window.removeEventListener(\"keydown\", onKey, true);\n }, [onClose]);\n\n const choose = useCallback(\n (src: string, size: Size) => {\n onSelect({ src, ...size });\n onClose();\n },\n [onSelect, onClose],\n );\n\n const upload = async (file: File) => {\n if (uploading) return;\n setError(null);\n setUploading(true);\n try {\n // Scaled first, so the size check and the upload both see the file\n // that will actually be stored — an oversized phone photo that shrinks\n // under the cap is accepted.\n const prepared = await prepareImage(file);\n const payload = prepared.file;\n\n const res = await createAssetUpload({\n name: payload.name,\n contentType: payload.type,\n size: payload.size,\n });\n if (!res.success) {\n setError(res.error);\n return;\n }\n const { target } = res;\n\n // Straight to storage — the bytes never touch the Next.js server.\n const put = await fetch(target.url, {\n method: target.method,\n headers: target.headers,\n body: payload,\n });\n if (!put.ok) {\n setError(`Upload failed (${put.status}).`);\n return;\n }\n\n setAssets((prev) => [target.asset, ...(prev ?? [])]);\n choose(target.asset.url, { width: prepared.width, height: prepared.height });\n } catch {\n setError(\"Upload failed. Check your connection and try again.\");\n } finally {\n setUploading(false);\n }\n };\n\n const onFiles = (files: FileList | null | undefined) => {\n const file = files?.[0];\n if (file) void upload(file);\n };\n\n return (\n <>\n <div data-guardrail style={S.pickerScrim} onClick={onClose} />\n <div\n data-guardrail\n role=\"dialog\"\n aria-label=\"Choose an image\"\n style={S.picker}\n onClick={(e) => e.stopPropagation()}\n >\n <header style={S.drawerHead}>\n <h2 style={S.drawerTitle}>Choose an image</h2>\n <button\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={onClose}\n aria-label=\"Close\"\n >\n <CloseIcon />\n </button>\n </header>\n\n <div style={S.drawerBody}>\n <div\n role=\"button\"\n tabIndex={0}\n style={{ ...S.dropzone, ...(dragging ? S.dropzoneActive : {}) }}\n onClick={() => !uploading && inputRef.current?.click()}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") inputRef.current?.click();\n }}\n onDragOver={(e) => {\n e.preventDefault();\n setDragging(true);\n }}\n onDragLeave={() => setDragging(false)}\n onDrop={(e) => {\n e.preventDefault();\n setDragging(false);\n onFiles(e.dataTransfer.files);\n }}\n >\n {uploading ? (\n <span style={{ display: \"inline-flex\", gap: 8, alignItems: \"center\" }}>\n <SpinnerIcon /> Uploading…\n </span>\n ) : (\n \"Drop an image here, or click to upload\"\n )}\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/png,image/jpeg,image/webp,image/gif,image/avif\"\n hidden\n onChange={(e) => {\n onFiles(e.target.files);\n e.target.value = \"\";\n }}\n />\n </div>\n\n {error && <div style={{ ...S.error, marginBottom: 12 }}>{error}</div>}\n\n {assets === null ? (\n <p style={{ fontSize: 13, color: v(\"muted\") }}>Loading…</p>\n ) : assets.length === 0 ? (\n <p style={{ fontSize: 13, color: v(\"muted\") }}>No images uploaded yet.</p>\n ) : (\n <div style={S.pickerGrid}>\n {assets.map((asset) => (\n <button\n key={asset.id}\n type=\"button\"\n title={asset.name}\n style={{\n ...S.pickerTile,\n ...(asset.url === current ? S.pickerTileActive : {}),\n }}\n onClick={async () => choose(asset.url, await measure(asset.url))}\n >\n {/* Plain img: the admin UI must not depend on next/image's\n remotePatterns being configured. */}\n <img src={asset.url} alt=\"\" loading=\"lazy\" style={S.pickerThumb} />\n </button>\n ))}\n </div>\n )}\n </div>\n </div>\n </>\n );\n}\n"],"mappings":";AAmHI,mBACE,KAQE,YATJ;AAjHJ,OAAO,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AAChE,SAAS,mBAAmB,kBAAkB;AAC9C,SAAS,WAAW,mBAAmB;AACvC,SAAS,GAAG,SAAS;AACrB,SAAS,oBAAoB;AAe7B,SAAS,QAAQ,KAA4B;AAC3C,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,MAAM,IAAI,MAAM;AACtB,QAAI,SAAS,MAAM,QAAQ,EAAE,OAAO,IAAI,cAAc,QAAQ,IAAI,cAAc,CAAC;AACjF,QAAI,UAAU,MAAM,QAAQ,CAAC,CAAC;AAC9B,QAAI,MAAM;AAAA,EACZ,CAAC;AACH;AAEO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA2B,IAAI;AAC3D,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,WAAW,OAAyB,IAAI;AAE9C,YAAU,MAAM;AACd,SAAK,WAAW,EAAE,KAAK,SAAS;AAAA,EAClC,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,UAAM,QAAQ,CAAC,MAAqB;AAClC,UAAI,EAAE,QAAQ,SAAU;AACxB,QAAE,gBAAgB;AAClB,cAAQ;AAAA,IACV;AACA,WAAO,iBAAiB,WAAW,OAAO,IAAI;AAC9C,WAAO,MAAM,OAAO,oBAAoB,WAAW,OAAO,IAAI;AAAA,EAChE,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,SAAS;AAAA,IACb,CAAC,KAAa,SAAe;AAC3B,eAAS,EAAE,KAAK,GAAG,KAAK,CAAC;AACzB,cAAQ;AAAA,IACV;AAAA,IACA,CAAC,UAAU,OAAO;AAAA,EACpB;AAEA,QAAM,SAAS,OAAO,SAAe;AACnC,QAAI,UAAW;AACf,aAAS,IAAI;AACb,iBAAa,IAAI;AACjB,QAAI;AAIF,YAAM,WAAW,MAAM,aAAa,IAAI;AACxC,YAAM,UAAU,SAAS;AAEzB,YAAM,MAAM,MAAM,kBAAkB;AAAA,QAClC,MAAM,QAAQ;AAAA,QACd,aAAa,QAAQ;AAAA,QACrB,MAAM,QAAQ;AAAA,MAChB,CAAC;AACD,UAAI,CAAC,IAAI,SAAS;AAChB,iBAAS,IAAI,KAAK;AAClB;AAAA,MACF;AACA,YAAM,EAAE,OAAO,IAAI;AAGnB,YAAM,MAAM,MAAM,MAAM,OAAO,KAAK;AAAA,QAClC,QAAQ,OAAO;AAAA,QACf,SAAS,OAAO;AAAA,QAChB,MAAM;AAAA,MACR,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,iBAAS,kBAAkB,IAAI,MAAM,IAAI;AACzC;AAAA,MACF;AAEA,gBAAU,CAAC,SAAS,CAAC,OAAO,OAAO,GAAI,QAAQ,CAAC,CAAE,CAAC;AACnD,aAAO,OAAO,MAAM,KAAK,EAAE,OAAO,SAAS,OAAO,QAAQ,SAAS,OAAO,CAAC;AAAA,IAC7E,QAAQ;AACN,eAAS,qDAAqD;AAAA,IAChE,UAAE;AACA,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF;AAEA,QAAM,UAAU,CAAC,UAAuC;AACtD,UAAM,OAAO,QAAQ,CAAC;AACtB,QAAI,KAAM,MAAK,OAAO,IAAI;AAAA,EAC5B;AAEA,SACE,iCACE;AAAA,wBAAC,SAAI,kBAAc,MAAC,OAAO,EAAE,aAAa,SAAS,SAAS;AAAA,IAC5D;AAAA,MAAC;AAAA;AAAA,QACC,kBAAc;AAAA,QACd,MAAK;AAAA,QACL,cAAW;AAAA,QACX,OAAO,EAAE;AAAA,QACT,SAAS,CAAC,MAAM,EAAE,gBAAgB;AAAA,QAElC;AAAA,+BAAC,YAAO,OAAO,EAAE,YACf;AAAA,gCAAC,QAAG,OAAO,EAAE,aAAa,6BAAe;AAAA,YACzC;AAAA,cAAC;AAAA;AAAA,gBACC,OAAO,EAAE,GAAG,EAAE,SAAS,OAAO,EAAE,OAAO,EAAE;AAAA,gBACzC,SAAS;AAAA,gBACT,cAAW;AAAA,gBAEX,8BAAC,aAAU;AAAA;AAAA,YACb;AAAA,aACF;AAAA,UAEA,qBAAC,SAAI,OAAO,EAAE,YACZ;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,UAAU;AAAA,gBACV,OAAO,EAAE,GAAG,EAAE,UAAU,GAAI,WAAW,EAAE,iBAAiB,CAAC,EAAG;AAAA,gBAC9D,SAAS,MAAM,CAAC,aAAa,SAAS,SAAS,MAAM;AAAA,gBACrD,WAAW,CAAC,MAAM;AAChB,sBAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,IAAK,UAAS,SAAS,MAAM;AAAA,gBAClE;AAAA,gBACA,YAAY,CAAC,MAAM;AACjB,oBAAE,eAAe;AACjB,8BAAY,IAAI;AAAA,gBAClB;AAAA,gBACA,aAAa,MAAM,YAAY,KAAK;AAAA,gBACpC,QAAQ,CAAC,MAAM;AACb,oBAAE,eAAe;AACjB,8BAAY,KAAK;AACjB,0BAAQ,EAAE,aAAa,KAAK;AAAA,gBAC9B;AAAA,gBAEC;AAAA,8BACC,qBAAC,UAAK,OAAO,EAAE,SAAS,eAAe,KAAK,GAAG,YAAY,SAAS,GAClE;AAAA,wCAAC,eAAY;AAAA,oBAAE;AAAA,qBACjB,IAEA;AAAA,kBAEF;AAAA,oBAAC;AAAA;AAAA,sBACC,KAAK;AAAA,sBACL,MAAK;AAAA,sBACL,QAAO;AAAA,sBACP,QAAM;AAAA,sBACN,UAAU,CAAC,MAAM;AACf,gCAAQ,EAAE,OAAO,KAAK;AACtB,0BAAE,OAAO,QAAQ;AAAA,sBACnB;AAAA;AAAA,kBACF;AAAA;AAAA;AAAA,YACF;AAAA,YAEC,SAAS,oBAAC,SAAI,OAAO,EAAE,GAAG,EAAE,OAAO,cAAc,GAAG,GAAI,iBAAM;AAAA,YAE9D,WAAW,OACV,oBAAC,OAAE,OAAO,EAAE,UAAU,IAAI,OAAO,EAAE,OAAO,EAAE,GAAG,2BAAQ,IACrD,OAAO,WAAW,IACpB,oBAAC,OAAE,OAAO,EAAE,UAAU,IAAI,OAAO,EAAE,OAAO,EAAE,GAAG,qCAAuB,IAEtE,oBAAC,SAAI,OAAO,EAAE,YACX,iBAAO,IAAI,CAAC,UACX;AAAA,cAAC;AAAA;AAAA,gBAEC,MAAK;AAAA,gBACL,OAAO,MAAM;AAAA,gBACb,OAAO;AAAA,kBACL,GAAG,EAAE;AAAA,kBACL,GAAI,MAAM,QAAQ,UAAU,EAAE,mBAAmB,CAAC;AAAA,gBACpD;AAAA,gBACA,SAAS,YAAY,OAAO,MAAM,KAAK,MAAM,QAAQ,MAAM,GAAG,CAAC;AAAA,gBAI/D,8BAAC,SAAI,KAAK,MAAM,KAAK,KAAI,IAAG,SAAQ,QAAO,OAAO,EAAE,aAAa;AAAA;AAAA,cAX5D,MAAM;AAAA,YAYb,CACD,GACH;AAAA,aAEJ;AAAA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
SpinnerIcon,
|
|
11
11
|
TrashIcon
|
|
12
12
|
} from "../Icons";
|
|
13
|
-
import { S } from "../styles";
|
|
13
|
+
import { S, v } from "../styles";
|
|
14
14
|
function RepeaterClient({
|
|
15
15
|
repeaterId,
|
|
16
16
|
routed,
|
|
@@ -82,7 +82,7 @@ function RepeaterClient({
|
|
|
82
82
|
/* @__PURE__ */ jsx(
|
|
83
83
|
"button",
|
|
84
84
|
{
|
|
85
|
-
style: { ...S.iconBtn, color: "
|
|
85
|
+
style: { ...S.iconBtn, color: v("danger") },
|
|
86
86
|
disabled: pending,
|
|
87
87
|
onClick: () => {
|
|
88
88
|
if (confirm("Remove this item and its content?")) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/react/Repeater/client.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useState, useTransition } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from \"../../server/repeaterActions\";\nimport {\n ArrowDownIcon,\n ArrowUpIcon,\n PlusIcon,\n SpinnerIcon,\n TrashIcon,\n} from \"../Icons\";\nimport { S } from \"../styles\";\n\ninterface RenderedItem {\n id: string;\n /** Already rendered server-side — this component only wraps it in chrome. */\n node: React.ReactNode;\n}\n\nexport function RepeaterClient({\n repeaterId,\n routed,\n items,\n children,\n}: {\n repeaterId: string;\n routed?: boolean;\n items: RenderedItem[];\n children?: React.ReactNode;\n}) {\n const router = useRouter();\n const [error, setError] = useState<string | null>(null);\n const [adding, setAdding] = useState(false);\n const [slug, setSlug] = useState(\"\");\n const [pending, startTransition] = useTransition();\n\n /**\n * There is deliberately no local copy of `items`. Each item's node can\n * contain <GRail> fields, which only exist as a resolved server render —\n * they cannot be reproduced by holding data in client state and re-invoking\n * a component function in the browser. router.refresh() re-runs <GRepeater>\n * on the server and delivers freshly rendered nodes as new props, the same\n * pattern used for the login flow in Admin.tsx.\n */\n const run = (fn: () => Promise<{ success: boolean; error?: string }>) =>\n startTransition(async () => {\n setError(null);\n const res = await fn();\n if (!res.success) setError(res.error ?? \"Something went wrong.\");\n router.refresh();\n });\n\n const move = (id: string, dir: -1 | 1) => {\n const ids = items.map((i) => i.id);\n const index = ids.indexOf(id);\n const target = index + dir;\n if (target < 0 || target >= ids.length) return;\n const a = ids[index]!;\n const b = ids[target]!;\n ids[index] = b;\n ids[target] = a;\n run(() => reorderRepeaterItems(repeaterId, ids));\n };\n\n const submitAdd = () => {\n if (routed && !slug.trim()) return;\n run(async () => {\n const res = await addRepeaterItem(\n repeaterId,\n routed ? { slug: slug.trim() } : undefined,\n );\n if (res.success) {\n setAdding(false);\n setSlug(\"\");\n }\n return res;\n });\n };\n\n return (\n <>\n {items.length === 0 && children}\n\n {items.map(({ id, node }, index) => (\n <div key={id} style={S.repeaterItem}>\n <div style={S.repeaterToolbar}>\n <button\n style={S.iconBtn}\n disabled={pending || index === 0}\n onClick={() => move(id, -1)}\n aria-label=\"Move up\"\n title=\"Move up\"\n >\n <ArrowUpIcon />\n </button>\n <button\n style={S.iconBtn}\n disabled={pending || index === items.length - 1}\n onClick={() => move(id, 1)}\n aria-label=\"Move down\"\n title=\"Move down\"\n >\n <ArrowDownIcon />\n </button>\n <button\n style={{ ...S.iconBtn, color: \"
|
|
1
|
+
{"version":3,"sources":["../../../src/react/Repeater/client.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useState, useTransition } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from \"../../server/repeaterActions\";\nimport {\n ArrowDownIcon,\n ArrowUpIcon,\n PlusIcon,\n SpinnerIcon,\n TrashIcon,\n} from \"../Icons\";\nimport { S, v } from \"../styles\";\n\ninterface RenderedItem {\n id: string;\n /** Already rendered server-side — this component only wraps it in chrome. */\n node: React.ReactNode;\n}\n\nexport function RepeaterClient({\n repeaterId,\n routed,\n items,\n children,\n}: {\n repeaterId: string;\n routed?: boolean;\n items: RenderedItem[];\n children?: React.ReactNode;\n}) {\n const router = useRouter();\n const [error, setError] = useState<string | null>(null);\n const [adding, setAdding] = useState(false);\n const [slug, setSlug] = useState(\"\");\n const [pending, startTransition] = useTransition();\n\n /**\n * There is deliberately no local copy of `items`. Each item's node can\n * contain <GRail> fields, which only exist as a resolved server render —\n * they cannot be reproduced by holding data in client state and re-invoking\n * a component function in the browser. router.refresh() re-runs <GRepeater>\n * on the server and delivers freshly rendered nodes as new props, the same\n * pattern used for the login flow in Admin.tsx.\n */\n const run = (fn: () => Promise<{ success: boolean; error?: string }>) =>\n startTransition(async () => {\n setError(null);\n const res = await fn();\n if (!res.success) setError(res.error ?? \"Something went wrong.\");\n router.refresh();\n });\n\n const move = (id: string, dir: -1 | 1) => {\n const ids = items.map((i) => i.id);\n const index = ids.indexOf(id);\n const target = index + dir;\n if (target < 0 || target >= ids.length) return;\n const a = ids[index]!;\n const b = ids[target]!;\n ids[index] = b;\n ids[target] = a;\n run(() => reorderRepeaterItems(repeaterId, ids));\n };\n\n const submitAdd = () => {\n if (routed && !slug.trim()) return;\n run(async () => {\n const res = await addRepeaterItem(\n repeaterId,\n routed ? { slug: slug.trim() } : undefined,\n );\n if (res.success) {\n setAdding(false);\n setSlug(\"\");\n }\n return res;\n });\n };\n\n return (\n <>\n {items.length === 0 && children}\n\n {items.map(({ id, node }, index) => (\n <div key={id} style={S.repeaterItem}>\n <div style={S.repeaterToolbar}>\n <button\n style={S.iconBtn}\n disabled={pending || index === 0}\n onClick={() => move(id, -1)}\n aria-label=\"Move up\"\n title=\"Move up\"\n >\n <ArrowUpIcon />\n </button>\n <button\n style={S.iconBtn}\n disabled={pending || index === items.length - 1}\n onClick={() => move(id, 1)}\n aria-label=\"Move down\"\n title=\"Move down\"\n >\n <ArrowDownIcon />\n </button>\n <button\n style={{ ...S.iconBtn, color: v(\"danger\") }}\n disabled={pending}\n onClick={() => {\n if (confirm(\"Remove this item and its content?\")) {\n run(() => removeRepeaterItem(repeaterId, id));\n }\n }}\n aria-label=\"Remove\"\n title=\"Remove\"\n >\n <TrashIcon />\n </button>\n </div>\n {node}\n </div>\n ))}\n\n {error && <div style={{ ...S.error, marginTop: 8 }}>{error}</div>}\n\n <div style={S.repeaterAddRow}>\n {adding && routed ? (\n <>\n <input\n style={S.repeaterSlugInput}\n placeholder=\"url-slug\"\n value={slug}\n autoFocus\n onChange={(e) => setSlug(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") submitAdd();\n if (e.key === \"Escape\") setAdding(false);\n }}\n />\n <button style={S.primaryBtn} disabled={pending} onClick={submitAdd}>\n {pending ? <SpinnerIcon /> : \"Add\"}\n </button>\n <button\n style={S.cancelBtn}\n disabled={pending}\n onClick={() => {\n setAdding(false);\n setSlug(\"\");\n }}\n >\n Cancel\n </button>\n </>\n ) : (\n <button\n type=\"button\"\n style={S.repeaterAddBtn}\n disabled={pending}\n onClick={() => (routed ? setAdding(true) : submitAdd())}\n >\n <PlusIcon /> Add {routed ? \"item\" : \"card\"}\n </button>\n )}\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|