@realiizlabs/admin 0.11.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Sidebar-CNuphww6.d.cts +60 -0
- package/dist/Sidebar-CNuphww6.d.ts +60 -0
- package/dist/auth/index.d.cts +5 -127
- package/dist/auth/index.d.ts +5 -127
- package/dist/auth-ui/index.js +124 -1
- package/dist/auth-ui/index.js.map +1 -1
- package/dist/forms/index.js +287 -1
- package/dist/forms/index.js.map +1 -1
- package/dist/forms-ui/index.js +355 -6
- package/dist/forms-ui/index.js.map +1 -1
- package/dist/git/index.cjs.map +1 -1
- package/dist/git/index.d.cts +3 -77
- package/dist/git/index.d.ts +3 -77
- package/dist/git/index.js.map +1 -1
- package/dist/members-nhbRSYyr.d.cts +106 -0
- package/dist/members-nhbRSYyr.d.ts +106 -0
- package/dist/roles-BjWuj2_v.d.cts +25 -0
- package/dist/roles-Dnwj-DAm.d.ts +25 -0
- package/dist/shell/index.d.cts +3 -58
- package/dist/shell/index.d.ts +3 -58
- package/dist/studio/index.cjs +1150 -0
- package/dist/studio/index.cjs.map +1 -0
- package/dist/studio/index.d.cts +325 -0
- package/dist/studio/index.d.ts +325 -0
- package/dist/studio/index.js +1131 -0
- package/dist/studio/index.js.map +1 -0
- package/dist/studio-ui/index.cjs +3931 -0
- package/dist/studio-ui/index.cjs.map +1 -0
- package/dist/studio-ui/index.d.cts +375 -0
- package/dist/studio-ui/index.d.ts +375 -0
- package/dist/studio-ui/index.js +3900 -0
- package/dist/studio-ui/index.js.map +1 -0
- package/dist/types-BP5G1myE.d.cts +78 -0
- package/dist/types-BP5G1myE.d.ts +78 -0
- package/package.json +20 -2
- package/dist/chunk-2GSYBARR.js +0 -126
- package/dist/chunk-2GSYBARR.js.map +0 -1
- package/dist/chunk-JLR5K6RP.js +0 -289
- package/dist/chunk-JLR5K6RP.js.map +0 -1
|
@@ -0,0 +1,3931 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
var navigation = require('next/navigation');
|
|
7
|
+
var ssr = require('@supabase/ssr');
|
|
8
|
+
var Link3 = require('next/link');
|
|
9
|
+
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
var Link3__default = /*#__PURE__*/_interopDefault(Link3);
|
|
13
|
+
|
|
14
|
+
// src/studio-ui/context.tsx
|
|
15
|
+
var Ctx = react.createContext(null);
|
|
16
|
+
function StudioProvider({ contentTypes, actions, supportName = "your web team", siteName = "Studio", children }) {
|
|
17
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Ctx.Provider, { value: { contentTypes, actions, supportName, siteName }, children });
|
|
18
|
+
}
|
|
19
|
+
function useStudio() {
|
|
20
|
+
const v = react.useContext(Ctx);
|
|
21
|
+
if (!v) throw new Error("Studio components need a <StudioProvider> above them \u2014 see @realiizlabs/admin/studio-ui.");
|
|
22
|
+
return v;
|
|
23
|
+
}
|
|
24
|
+
function useEntry(typeId) {
|
|
25
|
+
return useStudio().contentTypes[typeId] ?? null;
|
|
26
|
+
}
|
|
27
|
+
function publicDirOf(entry) {
|
|
28
|
+
return `/${entry.folder.split("/").filter(Boolean).pop() ?? "images"}/`;
|
|
29
|
+
}
|
|
30
|
+
var base = {
|
|
31
|
+
width: 18,
|
|
32
|
+
height: 18,
|
|
33
|
+
viewBox: "0 0 24 24",
|
|
34
|
+
fill: "none",
|
|
35
|
+
stroke: "currentColor",
|
|
36
|
+
strokeWidth: 1.7,
|
|
37
|
+
strokeLinecap: "round",
|
|
38
|
+
strokeLinejoin: "round",
|
|
39
|
+
"aria-hidden": true,
|
|
40
|
+
style: { flexShrink: 0 }
|
|
41
|
+
};
|
|
42
|
+
function Icon({ name }) {
|
|
43
|
+
switch (name) {
|
|
44
|
+
case "home":
|
|
45
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...base, children: [
|
|
46
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "7", height: "9", rx: "1" }),
|
|
47
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "14", y: "3", width: "7", height: "5", rx: "1" }),
|
|
48
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "14", y: "12", width: "7", height: "9", rx: "1" }),
|
|
49
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "16", width: "7", height: "5", rx: "1" })
|
|
50
|
+
] });
|
|
51
|
+
case "posts":
|
|
52
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...base, children: [
|
|
53
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 3h9l4 4v14H6z" }),
|
|
54
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15 3v4h4" }),
|
|
55
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 12h7M9 16h7M9 8h3" })
|
|
56
|
+
] });
|
|
57
|
+
case "events":
|
|
58
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...base, children: [
|
|
59
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "5", width: "18", height: "16", rx: "2" }),
|
|
60
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 3v4M16 3v4M3 10h18" })
|
|
61
|
+
] });
|
|
62
|
+
case "venues":
|
|
63
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...base, children: [
|
|
64
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 21s7-6.2 7-11.5A7 7 0 0 0 5 9.5C5 14.8 12 21 12 21z" }),
|
|
65
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "9.5", r: "2.5" })
|
|
66
|
+
] });
|
|
67
|
+
case "users":
|
|
68
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...base, children: [
|
|
69
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "8", r: "3.5" }),
|
|
70
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2.5 20a6.5 6.5 0 0 1 13 0" }),
|
|
71
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "17", cy: "9", r: "2.5" }),
|
|
72
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15.5 14.5a5 5 0 0 1 6 5" })
|
|
73
|
+
] });
|
|
74
|
+
case "settings":
|
|
75
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...base, children: [
|
|
76
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 7h9" }),
|
|
77
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "16", cy: "7", r: "2" }),
|
|
78
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 7h1" }),
|
|
79
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 17h9" }),
|
|
80
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "16", cy: "17", r: "2" }),
|
|
81
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 17h1" })
|
|
82
|
+
] });
|
|
83
|
+
default:
|
|
84
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...base, children: [
|
|
85
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 3h9l4 4v14H6z" }),
|
|
86
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15 3v4h4" })
|
|
87
|
+
] });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function iconFor(id) {
|
|
91
|
+
const known = ["home", "posts", "events", "venues", "users", "settings"];
|
|
92
|
+
return known.includes(id) ? id : "doc";
|
|
93
|
+
}
|
|
94
|
+
var SIDEBAR_KEY = "rz-admin-sidebar";
|
|
95
|
+
function isActive(item, activeHref, basePath) {
|
|
96
|
+
if (item.href === basePath) return activeHref === basePath || activeHref === `${basePath}/`;
|
|
97
|
+
return activeHref === item.href || activeHref.startsWith(`${item.href}/`);
|
|
98
|
+
}
|
|
99
|
+
function Sidebar({ items, pinned = [], activeHref, basePath }) {
|
|
100
|
+
const [collapsed, setCollapsed] = react.useState(false);
|
|
101
|
+
react.useEffect(() => {
|
|
102
|
+
try {
|
|
103
|
+
if (window.localStorage.getItem(SIDEBAR_KEY) === "collapsed") setCollapsed(true);
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
}, []);
|
|
107
|
+
const toggle = () => {
|
|
108
|
+
const next = !collapsed;
|
|
109
|
+
setCollapsed(next);
|
|
110
|
+
try {
|
|
111
|
+
window.localStorage.setItem(SIDEBAR_KEY, next ? "collapsed" : "open");
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const renderItem = (item) => {
|
|
116
|
+
const active = !item.disabled && isActive(item, activeHref, basePath);
|
|
117
|
+
if (item.disabled) {
|
|
118
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "rz-sidebar__item rz-sidebar__item--disabled", "aria-disabled": "true", title: collapsed ? `${item.label} \u2014 coming soon` : "Coming soon", children: [
|
|
119
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: iconFor(item.icon ?? item.id) }),
|
|
120
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-sidebar__label", children: item.label })
|
|
121
|
+
] }, item.id);
|
|
122
|
+
}
|
|
123
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
124
|
+
"a",
|
|
125
|
+
{
|
|
126
|
+
href: item.href,
|
|
127
|
+
className: ["rz-sidebar__item", active && "rz-sidebar__item--active"].filter(Boolean).join(" "),
|
|
128
|
+
"aria-current": active ? "page" : void 0,
|
|
129
|
+
title: collapsed ? item.label : void 0,
|
|
130
|
+
children: [
|
|
131
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: iconFor(item.icon ?? item.id) }),
|
|
132
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-sidebar__label", children: item.label })
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
item.id
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: ["rz-sidebar", collapsed && "rz-sidebar--collapsed"].filter(Boolean).join(" "), "aria-label": "Admin navigation", children: [
|
|
139
|
+
/* @__PURE__ */ jsxRuntime.jsx("nav", { className: "rz-sidebar__nav", children: items.map(renderItem) }),
|
|
140
|
+
pinned.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "rz-sidebar__pinned", "aria-label": "Settings", children: pinned.map(renderItem) }),
|
|
141
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
142
|
+
"button",
|
|
143
|
+
{
|
|
144
|
+
type: "button",
|
|
145
|
+
className: "rz-sidebar__collapse",
|
|
146
|
+
onClick: toggle,
|
|
147
|
+
"aria-label": collapsed ? "Expand navigation" : "Collapse navigation",
|
|
148
|
+
title: collapsed ? "Expand" : "Collapse",
|
|
149
|
+
children: [
|
|
150
|
+
/* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", style: { transform: collapsed ? "rotate(180deg)" : void 0 }, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15 6l-6 6 6 6" }) }),
|
|
151
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-sidebar__label", children: "Collapse" })
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
] });
|
|
156
|
+
}
|
|
157
|
+
var THEME_KEY = "rz-admin-theme";
|
|
158
|
+
function useTheme(defaultTheme = "light") {
|
|
159
|
+
const [theme, setTheme] = react.useState(defaultTheme);
|
|
160
|
+
react.useEffect(() => {
|
|
161
|
+
try {
|
|
162
|
+
const stored = window.localStorage.getItem(THEME_KEY);
|
|
163
|
+
if (stored === "light" || stored === "dark") setTheme(stored);
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
}, []);
|
|
167
|
+
const set = (t) => {
|
|
168
|
+
setTheme(t);
|
|
169
|
+
try {
|
|
170
|
+
window.localStorage.setItem(THEME_KEY, t);
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
return [theme, set];
|
|
175
|
+
}
|
|
176
|
+
function ThemeToggle({ theme, onChange }) {
|
|
177
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "rz-seg", role: "group", "aria-label": "Theme", children: [
|
|
178
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-pressed": theme === "light", onClick: () => onChange("light"), children: "Light" }),
|
|
179
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-pressed": theme === "dark", onClick: () => onChange("dark"), children: "Dark" })
|
|
180
|
+
] });
|
|
181
|
+
}
|
|
182
|
+
function firstNameOf(user) {
|
|
183
|
+
const fromName = user.name?.trim().split(/\s+/)[0];
|
|
184
|
+
if (fromName) return fromName;
|
|
185
|
+
const local = user.email.split("@")[0] ?? "";
|
|
186
|
+
return local ? local.charAt(0).toUpperCase() + local.slice(1) : "there";
|
|
187
|
+
}
|
|
188
|
+
function initialsOf(user) {
|
|
189
|
+
const parts = user.name?.trim().split(/\s+/).filter(Boolean) ?? [];
|
|
190
|
+
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
191
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
192
|
+
return (user.email[0] ?? "?").toUpperCase();
|
|
193
|
+
}
|
|
194
|
+
var ico = { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true };
|
|
195
|
+
var ShieldIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { ...ico, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 3l7 3v5c0 5-3.5 8.5-7 10-3.5-1.5-7-5-7-10V6z" }) });
|
|
196
|
+
var SunIcon = () => /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...ico, children: [
|
|
197
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "4" }),
|
|
198
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" })
|
|
199
|
+
] });
|
|
200
|
+
var UserIcon = () => /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...ico, children: [
|
|
201
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "8", r: "4" }),
|
|
202
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 21a8 8 0 0 1 16 0" })
|
|
203
|
+
] });
|
|
204
|
+
var HelpIcon = () => /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...ico, children: [
|
|
205
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 4h6a4 4 0 0 1 4 4v12a3 3 0 0 0-3-3H2z" }),
|
|
206
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M22 4h-6a4 4 0 0 0-4 4v12a3 3 0 0 1 3-3h7z" })
|
|
207
|
+
] });
|
|
208
|
+
var OutIcon = () => /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...ico, children: [
|
|
209
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
|
|
210
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 17l5-5-5-5" }),
|
|
211
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 12H9" })
|
|
212
|
+
] });
|
|
213
|
+
function AccountMenu({ user, role, theme, onTheme, signOutPath, accountHref, helpHref }) {
|
|
214
|
+
const [open, setOpen] = react.useState(false);
|
|
215
|
+
const wrap = react.useRef(null);
|
|
216
|
+
react.useEffect(() => {
|
|
217
|
+
if (!open) return;
|
|
218
|
+
const onClick = (e) => {
|
|
219
|
+
if (!wrap.current?.contains(e.target)) setOpen(false);
|
|
220
|
+
};
|
|
221
|
+
const onKey = (e) => {
|
|
222
|
+
if (e.key === "Escape") setOpen(false);
|
|
223
|
+
};
|
|
224
|
+
document.addEventListener("mousedown", onClick);
|
|
225
|
+
document.addEventListener("keydown", onKey);
|
|
226
|
+
return () => {
|
|
227
|
+
document.removeEventListener("mousedown", onClick);
|
|
228
|
+
document.removeEventListener("keydown", onKey);
|
|
229
|
+
};
|
|
230
|
+
}, [open]);
|
|
231
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: wrap, style: { position: "relative" }, children: [
|
|
232
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "rz-avatar", "aria-haspopup": "menu", "aria-expanded": open, "aria-label": "Account menu", onClick: () => setOpen((v) => !v), children: user.avatarUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: user.avatarUrl, alt: "", className: "rz-avatar__img" }) : initialsOf(user) }),
|
|
233
|
+
open && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-menu", role: "menu", children: [
|
|
234
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-menu__who", children: [
|
|
235
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-menu__name", children: user.name?.trim() || user.email }),
|
|
236
|
+
user.name?.trim() && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-menu__email", children: user.email })
|
|
237
|
+
] }),
|
|
238
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-menu__item rz-menu__item--static", role: "menuitem", "aria-disabled": "true", children: [
|
|
239
|
+
/* @__PURE__ */ jsxRuntime.jsx(ShieldIcon, {}),
|
|
240
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Role" }),
|
|
241
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-menu__end rz-pill rz-pill--plain", children: role })
|
|
242
|
+
] }),
|
|
243
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-menu__item rz-menu__item--static", role: "menuitem", "aria-disabled": "true", children: [
|
|
244
|
+
/* @__PURE__ */ jsxRuntime.jsx(SunIcon, {}),
|
|
245
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Theme" }),
|
|
246
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-menu__end", children: /* @__PURE__ */ jsxRuntime.jsx(ThemeToggle, { theme, onChange: onTheme }) })
|
|
247
|
+
] }),
|
|
248
|
+
(accountHref || helpHref) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-menu__sep" }),
|
|
249
|
+
accountHref && /* @__PURE__ */ jsxRuntime.jsxs("a", { href: accountHref, role: "menuitem", className: "rz-menu__item", children: [
|
|
250
|
+
/* @__PURE__ */ jsxRuntime.jsx(UserIcon, {}),
|
|
251
|
+
"Account"
|
|
252
|
+
] }),
|
|
253
|
+
helpHref && /* @__PURE__ */ jsxRuntime.jsxs("a", { href: helpHref, role: "menuitem", className: "rz-menu__item", children: [
|
|
254
|
+
/* @__PURE__ */ jsxRuntime.jsx(HelpIcon, {}),
|
|
255
|
+
"Help"
|
|
256
|
+
] }),
|
|
257
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-menu__sep" }),
|
|
258
|
+
/* @__PURE__ */ jsxRuntime.jsx("form", { method: "post", action: signOutPath, className: "rz-menu__signout", children: /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "submit", role: "menuitem", className: "rz-menu__item", children: [
|
|
259
|
+
/* @__PURE__ */ jsxRuntime.jsx(OutIcon, {}),
|
|
260
|
+
"Sign out"
|
|
261
|
+
] }) })
|
|
262
|
+
] })
|
|
263
|
+
] });
|
|
264
|
+
}
|
|
265
|
+
function TopBar({ siteName, productName, logoUrl, homeHref, user, role, theme, onTheme, signOutPath, accountHref, helpHref }) {
|
|
266
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "rz-topbar", children: [
|
|
267
|
+
/* @__PURE__ */ jsxRuntime.jsxs("a", { href: homeHref, className: "rz-topbar__brand", children: [
|
|
268
|
+
logoUrl && /* @__PURE__ */ jsxRuntime.jsx("img", { src: logoUrl, alt: "", className: "rz-topbar__logo" }),
|
|
269
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: siteName }),
|
|
270
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-topbar__product", children: productName })
|
|
271
|
+
] }),
|
|
272
|
+
user && role && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-topbar__right", children: [
|
|
273
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "rz-topbar__greeting", "data-testid": "rz-greeting", children: [
|
|
274
|
+
"Welcome back, ",
|
|
275
|
+
firstNameOf(user)
|
|
276
|
+
] }),
|
|
277
|
+
/* @__PURE__ */ jsxRuntime.jsx(AccountMenu, { user, role, theme, onTheme, signOutPath, accountHref, helpHref })
|
|
278
|
+
] })
|
|
279
|
+
] });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/shell/tokens.ts
|
|
283
|
+
var SHELL_CSS = `
|
|
284
|
+
.rz-admin, .rz-admin[data-theme="light"] {
|
|
285
|
+
/* realiiz.com palette (src/app/globals.css): ink, bone, paper, signal blue, slate, line, amber */
|
|
286
|
+
--ink: #0e1726; --bone: #f7f5f0; --signal: #1f6feb;
|
|
287
|
+
--surface-0: #f7f5f0; --surface-1: #ffffff; --surface-2: #f1eee7; --surface-3: #e8e4da;
|
|
288
|
+
--text-primary: #0e1726; --text-secondary: #5b6678; --text-muted: #8a93a5;
|
|
289
|
+
--signal-dim: #1a63d8; --signal-glow: #5b9bff; --signal-warn: #e0a03b; --series-blue: #1f6feb;
|
|
290
|
+
--border: #d9d4c8; --separator: #e9e5dc;
|
|
291
|
+
--status-up: #15803d; --status-down: #dc2626; --status-neutral: var(--text-muted); --status-blocked: var(--border);
|
|
292
|
+
--signal-fg: #ffffff;
|
|
293
|
+
}
|
|
294
|
+
.rz-admin[data-theme="dark"] { color-scheme: dark; }
|
|
295
|
+
.rz-admin[data-theme="dark"] {
|
|
296
|
+
--ink: #eef0f5; --bone: #1b2334; --signal: #3b82f6;
|
|
297
|
+
--surface-0: #1b2334; --surface-1: #222c42; --surface-2: #29344a; --surface-3: #36415c;
|
|
298
|
+
--text-primary: #eef0f5; --text-secondary: #a2abbf; --text-muted: #77819a;
|
|
299
|
+
--signal-dim: #8db4ff; --signal-glow: #aec9ff; --signal-warn: #eab058; --series-blue: #3b82f6;
|
|
300
|
+
--border: #36415c; --separator: #2c3750;
|
|
301
|
+
--status-up: #4ade80; --status-down: #f87171; --status-neutral: var(--text-muted); --status-blocked: var(--border);
|
|
302
|
+
--signal-fg: #ffffff;
|
|
303
|
+
}
|
|
304
|
+
.rz-admin {
|
|
305
|
+
--rz-font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
306
|
+
--rz-font-display: var(--rz-font);
|
|
307
|
+
--rz-font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
308
|
+
--rz-radius: 6px;
|
|
309
|
+
--rz-sidebar: 220px; --rz-sidebar-collapsed: 58px; --rz-topbar: 52px;
|
|
310
|
+
|
|
311
|
+
/* forms-ui speaks these; map them once so both packages agree */
|
|
312
|
+
--realiiz-form-fg: var(--text-primary); --realiiz-form-muted: var(--text-muted);
|
|
313
|
+
--realiiz-form-border: var(--border); --realiiz-form-accent: var(--signal);
|
|
314
|
+
--realiiz-form-accent-fg: var(--signal-fg); --realiiz-form-danger: var(--status-down);
|
|
315
|
+
--realiiz-form-radius: var(--rz-radius); --realiiz-form-font: var(--rz-font);
|
|
316
|
+
--realiiz-form-focus-ring: 0 0 0 3px color-mix(in srgb, var(--signal) 25%, transparent);
|
|
317
|
+
|
|
318
|
+
box-sizing: border-box; min-height: 100vh; display: flex; flex-direction: column;
|
|
319
|
+
background: var(--surface-0); color: var(--text-primary);
|
|
320
|
+
font-family: var(--rz-font); font-size: 14px; line-height: 1.45;
|
|
321
|
+
}
|
|
322
|
+
.rz-admin *, .rz-admin *::before, .rz-admin *::after { box-sizing: inherit; }
|
|
323
|
+
.rz-admin a { color: inherit; }
|
|
324
|
+
.rz-admin h1, .rz-admin h2, .rz-admin h3 { font-family: var(--rz-font-display); color: var(--text-primary); margin: 0; }
|
|
325
|
+
.rz-admin .realiiz-form__control { background: var(--surface-0); }
|
|
326
|
+
.rz-admin .realiiz-image { background: var(--surface-0); }
|
|
327
|
+
.rz-admin .realiiz-image__thumb { background: var(--surface-2); }
|
|
328
|
+
.rz-admin[data-theme="dark"] .realiiz-form__control::-webkit-calendar-picker-indicator { filter: invert(1) opacity(.7); }
|
|
329
|
+
.rz-admin .realiiz-form__button { background: var(--surface-1); color: var(--text-primary); }
|
|
330
|
+
.rz-admin .realiiz-form__button--primary { background: var(--signal); color: var(--signal-fg); }
|
|
331
|
+
|
|
332
|
+
/* ---- frame ---------------------------------------------------------- */
|
|
333
|
+
.rz-topbar { height: var(--rz-topbar); display: flex; align-items: center; gap: 1rem; padding: 0 1.25rem; border-bottom: 1px solid var(--border); background: var(--surface-1); flex-shrink: 0; }
|
|
334
|
+
.rz-topbar__brand { display: flex; align-items: center; gap: .6rem; font-weight: 600; font-size: .9375rem; text-decoration: none; }
|
|
335
|
+
.rz-topbar__logo { height: 22px; width: auto; display: block; }
|
|
336
|
+
.rz-topbar__product { font-weight: 400; color: var(--text-secondary); }
|
|
337
|
+
.rz-topbar__right { margin-left: auto; display: flex; align-items: center; gap: 1rem; }
|
|
338
|
+
.rz-topbar__greeting { color: var(--text-secondary); font-size: .8rem; }
|
|
339
|
+
.rz-body { display: flex; flex: 1; min-height: 0; }
|
|
340
|
+
/* Top bar and sidebar stay put while the content scrolls, so Collapse is always at the bottom-left. */
|
|
341
|
+
.rz-topbar { position: sticky; top: 0; z-index: 20; }
|
|
342
|
+
.rz-sidebar { position: sticky; top: var(--rz-topbar); height: calc(100vh - var(--rz-topbar)); width: var(--rz-sidebar); flex-shrink: 0; border-right: 1px solid var(--border); background: var(--surface-1); display: flex; flex-direction: column; transition: width .15s ease; overflow: hidden; }
|
|
343
|
+
.rz-sidebar--collapsed { width: var(--rz-sidebar-collapsed); }
|
|
344
|
+
.rz-sidebar__nav { padding: .75rem 0; flex: 1; overflow-y: auto; }
|
|
345
|
+
.rz-sidebar__item { display: flex; align-items: center; gap: .65rem; padding: .55rem 1rem; color: var(--text-secondary); text-decoration: none; font-size: .875rem; font-weight: 400; white-space: nowrap; overflow: hidden; }
|
|
346
|
+
.rz-sidebar__item:hover { background: var(--surface-2); color: var(--text-primary); }
|
|
347
|
+
.rz-sidebar__item--active { background: var(--surface-2); color: var(--signal); font-weight: 500; }
|
|
348
|
+
.rz-sidebar__item--disabled, .rz-sidebar__item--disabled:hover { color: var(--text-muted); opacity: .55; background: transparent; cursor: default; }
|
|
349
|
+
.rz-sidebar--collapsed .rz-sidebar__item { justify-content: center; padding: .55rem 0; }
|
|
350
|
+
.rz-sidebar--collapsed .rz-sidebar__label { display: none; }
|
|
351
|
+
.rz-sidebar__pinned { border-top: 1px solid var(--separator); padding: .3rem 0; }
|
|
352
|
+
.rz-sidebar__collapse { display: flex; align-items: center; gap: .7rem; padding: .6rem 1rem; border: 0; border-top: 1px solid var(--separator); background: transparent; color: var(--text-muted); cursor: pointer; font: inherit; font-size: .8rem; text-align: left; }
|
|
353
|
+
.rz-sidebar__collapse:hover { color: var(--text-primary); }
|
|
354
|
+
.rz-sidebar__collapse svg { flex-shrink: 0; }
|
|
355
|
+
.rz-sidebar--collapsed .rz-sidebar__collapse { justify-content: center; padding: .6rem 0; }
|
|
356
|
+
.rz-main { flex: 1; min-width: 0; }
|
|
357
|
+
.rz-content { max-width: 1400px; margin: 0; padding: 28px 32px 80px; }
|
|
358
|
+
.rz-content .realiiz-form { max-width: none; }
|
|
359
|
+
.rz-content .realiiz-form fieldset, .rz-content .realiiz-form__alert, .rz-content .realiiz-form__actions { max-width: 920px; }
|
|
360
|
+
/* The form's save bar pins to the bottom of the content column: bleed out over .rz-content's padding, then pad back in. */
|
|
361
|
+
.rz-content .realiiz-form__bar { position: sticky; bottom: 0; z-index: 10; margin: 24px -32px -80px; padding: 12px 32px; background: var(--surface-1); border-top: 1px solid var(--border); box-shadow: 0 -6px 16px rgba(14,23,38,.05); }
|
|
362
|
+
.rz-content .realiiz-form__status { color: var(--text-muted); }
|
|
363
|
+
.rz-content .realiiz-form__bar[data-dirty] .realiiz-form__status { color: var(--text-primary); font-weight: 500; }
|
|
364
|
+
|
|
365
|
+
/* ---- primitives ----------------------------------------------------- */
|
|
366
|
+
/* Same metrics as the form's Save button (.realiiz-form__button) so every button in the admin matches. */
|
|
367
|
+
.rz-btn { font-family: inherit; font-weight: 600; border-radius: var(--rz-radius); cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; gap: .4rem; white-space: nowrap; line-height: 1.2; }
|
|
368
|
+
.rz-btn--md { padding: 10px 16px; font-size: 15px; min-height: 40px; }
|
|
369
|
+
.rz-btn--sm { padding: 6px 12px; font-size: 13px; min-height: 32px; }
|
|
370
|
+
.rz-admin .rz-btn--primary, .rz-admin a.rz-btn--primary { background: var(--signal); color: var(--signal-fg); border: 1px solid var(--signal); }
|
|
371
|
+
.rz-admin .rz-btn--secondary, .rz-admin a.rz-btn--secondary { background: var(--surface-1); color: var(--text-primary); border: 1px solid var(--border); }
|
|
372
|
+
.rz-admin .rz-btn--danger, .rz-admin a.rz-btn--danger { background: transparent; color: var(--status-down); border: 1px solid var(--status-down); }
|
|
373
|
+
.rz-admin .rz-btn--ghost, .rz-admin a.rz-btn--ghost { background: var(--surface-1); color: var(--text-primary); border: 1px solid var(--border); }
|
|
374
|
+
.rz-admin .rz-btn:disabled { opacity: .55; cursor: default; }
|
|
375
|
+
.rz-arrow { width: 1.1em; height: 1.1em; flex-shrink: 0; transition: transform .15s ease-out; }
|
|
376
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
377
|
+
.rz-btn:hover .rz-arrow, a:hover .rz-arrow { transform: translate(2px, -2px); }
|
|
378
|
+
.rz-btn:hover .rz-arrow--fwd, a:hover .rz-arrow--fwd { transform: translateX(4px); }
|
|
379
|
+
}
|
|
380
|
+
.rz-card { display: flex; flex-direction: column; background: var(--surface-1); border: 1px solid var(--border); border-radius: 8px; padding: 18px 20px; }
|
|
381
|
+
.rz-card--narrow { max-width: 760px; }
|
|
382
|
+
.rz-card__title { font-size: .9375rem; font-weight: 600; margin-bottom: .35rem; }
|
|
383
|
+
.rz-card__meta { color: var(--text-muted); font-size: .8rem; }
|
|
384
|
+
.rz-card__actions { display: flex; gap: .5rem; margin-top: auto; padding-top: .9rem; flex-wrap: wrap; }
|
|
385
|
+
.rz-card__note { margin-top: .5rem; }
|
|
386
|
+
.rz-grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
|
|
387
|
+
.rz-pagehead { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 20px; flex-wrap: wrap; }
|
|
388
|
+
.rz-pagehead__eyebrow { font-family: var(--rz-font-mono); font-size: .6875rem; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); margin-bottom: .3rem; }
|
|
389
|
+
.rz-pagehead h1 { font-size: 1.375rem; font-weight: 600; }
|
|
390
|
+
.rz-pagehead__sub { color: var(--text-secondary); font-size: .85rem; margin-top: .25rem; }
|
|
391
|
+
.rz-pill { display: inline-flex; align-items: center; gap: .4rem; white-space: nowrap; font-family: var(--rz-font-mono); font-size: .6875rem; text-transform: uppercase; letter-spacing: .06em; padding: .15rem .5rem; border: 1px solid var(--border); border-radius: 999px; color: var(--text-secondary); }
|
|
392
|
+
.rz-pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--status-neutral); }
|
|
393
|
+
.rz-pill--up::before { background: var(--status-up); } .rz-pill--warn::before { background: var(--signal-warn); } .rz-pill--down::before { background: var(--status-down); }
|
|
394
|
+
.rz-pill--plain::before { display: none; }
|
|
395
|
+
/* ---- steps tracker ------------------------------------------------- */
|
|
396
|
+
/* Dots on one rail, connectors dot-to-dot, label under each dot; left-aligned like the rest of the page. */
|
|
397
|
+
.rz-steps { list-style: none; margin: 4px 0 22px; padding: 0 0 22px; display: flex; border-bottom: 1px solid var(--separator); }
|
|
398
|
+
.rz-step { flex: 1; position: relative; display: flex; flex-direction: column; align-items: flex-start; text-align: left; gap: 8px; min-width: 0; }
|
|
399
|
+
.rz-step:last-child { flex: 0 0 auto; }
|
|
400
|
+
.rz-step:not(:last-child)::after { content: ""; position: absolute; top: 12px; left: 26px; right: 0; height: 2px; background: var(--border); }
|
|
401
|
+
.rz-step--done:not(:last-child)::after { background: var(--status-up); }
|
|
402
|
+
.rz-step__dot { width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: .75rem; font-weight: 600; flex-shrink: 0; border: 2px solid var(--border); color: var(--text-muted); background: var(--surface-1); }
|
|
403
|
+
.rz-step--done .rz-step__dot { background: var(--status-up); border-color: var(--status-up); color: #fff; }
|
|
404
|
+
.rz-step--active .rz-step__dot { position: relative; border-color: color-mix(in srgb, var(--signal) 30%, transparent); color: var(--signal); }
|
|
405
|
+
/* A spinning arc over the ring: the step is working, not waiting on you. */
|
|
406
|
+
.rz-step--active .rz-step__dot::after { content: ""; position: absolute; inset: -2px; border-radius: 50%; border: 2px solid transparent; border-top-color: var(--signal); animation: rz-spin .9s linear infinite; }
|
|
407
|
+
@media (prefers-reduced-motion: reduce) { .rz-step--active .rz-step__dot::after { animation: none; border-color: var(--signal); } }
|
|
408
|
+
@keyframes rz-spin { to { transform: rotate(360deg); } }
|
|
409
|
+
.rz-step--failed .rz-step__dot { background: var(--status-down); border-color: var(--status-down); color: #fff; }
|
|
410
|
+
.rz-step__text { display: flex; flex-direction: column; gap: 1px; padding-right: 16px; }
|
|
411
|
+
.rz-step__label { font-size: .875rem; font-weight: 500; color: var(--text-secondary); }
|
|
412
|
+
.rz-step--active .rz-step__label, .rz-step--done .rz-step__label { color: var(--text-primary); }
|
|
413
|
+
.rz-step--failed .rz-step__label { color: var(--status-down); }
|
|
414
|
+
.rz-step__note { font-size: .75rem; color: var(--text-muted); }
|
|
415
|
+
@keyframes rz-pulse { 0%, 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--signal) 18%, transparent); } 50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--signal) 8%, transparent); } }
|
|
416
|
+
.rz-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 20px; }
|
|
417
|
+
.rz-actions__hint { font-size: .8rem; color: var(--text-muted); }
|
|
418
|
+
.rz-notice { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface-1); margin-bottom: 18px; font-size: .9rem; }
|
|
419
|
+
.rz-notice__text { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
420
|
+
.rz-status-link { text-decoration: none; }
|
|
421
|
+
.rz-discard { margin-top: 22px; padding-top: 14px; border-top: 1px solid var(--separator); display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: .85rem; color: var(--text-secondary); }
|
|
422
|
+
.rz-discard__link { background: none; border: 0; padding: 0; font: inherit; font-size: .85rem; color: var(--text-muted); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
|
|
423
|
+
.rz-discard__link:hover { color: var(--status-down); }
|
|
424
|
+
.rz-nowrap { white-space: nowrap; }
|
|
425
|
+
.rz-table td.rz-fit, .rz-table th.rz-fit { width: 1%; white-space: nowrap; }
|
|
426
|
+
.rz-empty { border: 1px dashed var(--border); border-radius: 8px; padding: 32px; text-align: center; color: var(--text-secondary); }
|
|
427
|
+
.rz-empty h3 { font-size: 1rem; margin-bottom: .4rem; }
|
|
428
|
+
.rz-table-wrap { width: 100%; }
|
|
429
|
+
.rz-table { width: 100%; border-collapse: collapse; background: var(--surface-1); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|
430
|
+
.rz-table th, .rz-table td { text-align: left; padding: 10px 14px; border-bottom: 1px solid var(--separator); font-size: .875rem; vertical-align: top; }
|
|
431
|
+
.rz-table th { font-family: var(--rz-font-mono); font-size: .6875rem; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); background: var(--surface-2); }
|
|
432
|
+
.rz-table tr:last-child td { border-bottom: 0; }
|
|
433
|
+
.rz-muted { color: var(--text-muted); }
|
|
434
|
+
.rz-alert { border: 1px solid var(--status-down); color: var(--status-down); border-radius: var(--rz-radius); padding: 12px 14px; margin: 0 0 16px; }
|
|
435
|
+
.rz-ok { border: 1px solid var(--status-up); color: var(--signal-dim); border-radius: var(--rz-radius); padding: 12px 14px; margin: 0 0 16px; }
|
|
436
|
+
|
|
437
|
+
/* ---- account menu --------------------------------------------------- */
|
|
438
|
+
.rz-avatar__img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; display: block; }
|
|
439
|
+
.rz-tabs { display: flex; gap: .25rem; border-bottom: 1px solid var(--border); margin: 0 0 1.5rem; flex-wrap: wrap; }
|
|
440
|
+
.rz-tabs__tab { padding: .55rem .9rem; font: inherit; font-size: .88rem; color: var(--text-secondary); background: none; border: 0; border-bottom: 2px solid transparent; margin-bottom: -1px; cursor: pointer; }
|
|
441
|
+
.rz-tabs__tab:hover { color: var(--text-primary); }
|
|
442
|
+
.rz-tabs__tab[aria-selected="true"] { color: var(--text-primary); border-bottom-color: var(--signal); font-weight: 600; }
|
|
443
|
+
.rz-tabs__tab:focus-visible { outline: 2px solid var(--signal); outline-offset: -2px; border-radius: 4px; }
|
|
444
|
+
.rz-avatar { width: 2rem; height: 2rem; border-radius: 50%; background: var(--surface-3); border: 1px solid var(--border); color: var(--text-primary); font-size: .6875rem; font-weight: 600; letter-spacing: .04em; display: flex; align-items: center; justify-content: center; cursor: pointer; font-family: inherit; padding: 0; overflow: hidden; }
|
|
445
|
+
.rz-menu { position: absolute; right: 0; top: calc(100% + 8px); min-width: 15rem; background: var(--surface-1); border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 8px 24px rgba(14,23,38,.14); overflow: hidden; z-index: 50; }
|
|
446
|
+
.rz-menu__who { padding: .75rem .9rem; border-bottom: 1px solid var(--border); margin-bottom: .3rem; }
|
|
447
|
+
.rz-menu__name { font-weight: 600; font-size: .875rem; }
|
|
448
|
+
.rz-menu__email { color: var(--text-muted); font-size: .75rem; word-break: break-all; }
|
|
449
|
+
.rz-menu__sep { height: 1px; background: var(--border); margin: .3rem 0; }
|
|
450
|
+
.rz-menu__item { display: flex; align-items: center; gap: .55rem; width: 100%; padding: .45rem .9rem; font: inherit; font-size: .8125rem; color: var(--text-secondary); background: none; border: 0; text-align: left; cursor: pointer; text-decoration: none; }
|
|
451
|
+
.rz-menu__item:hover { background: var(--surface-2); color: var(--text-primary); }
|
|
452
|
+
.rz-menu__item svg { flex-shrink: 0; }
|
|
453
|
+
.rz-menu__item--static { cursor: default; }
|
|
454
|
+
.rz-menu__item--static:hover { background: none; color: var(--text-secondary); }
|
|
455
|
+
.rz-menu__item .rz-menu__end { margin-left: auto; }
|
|
456
|
+
.rz-menu__signout { margin: 0 0 .3rem; }
|
|
457
|
+
.rz-seg { display: inline-flex; border: 1px solid var(--border); border-radius: var(--rz-radius); overflow: hidden; }
|
|
458
|
+
.rz-seg button { font: inherit; font-size: .75rem; padding: .15rem .55rem; line-height: 1.3; background: transparent; color: var(--text-secondary); border: 0; cursor: pointer; }
|
|
459
|
+
.rz-seg button[aria-pressed="true"] { background: var(--surface-3); color: var(--text-primary); }
|
|
460
|
+
|
|
461
|
+
/* ---- mobile: sidebar becomes a strip under the top bar --------------- */
|
|
462
|
+
@media (max-width: 720px) {
|
|
463
|
+
.rz-body { flex-direction: column; }
|
|
464
|
+
.rz-sidebar, .rz-sidebar--collapsed { width: 100%; border-right: 0; border-bottom: 1px solid var(--border); flex-direction: row; }
|
|
465
|
+
.rz-sidebar__nav { display: flex; padding: .35rem .25rem; overflow-x: auto; }
|
|
466
|
+
.rz-sidebar__item { flex-direction: column; gap: .2rem; font-size: .6875rem; padding: .4rem .6rem; border-radius: 6px; }
|
|
467
|
+
.rz-sidebar--collapsed .rz-sidebar__label { display: block; }
|
|
468
|
+
.rz-sidebar__collapse { display: none; }
|
|
469
|
+
.rz-sidebar__pinned { display: flex; border-top: 0; padding: .35rem .25rem; }
|
|
470
|
+
.rz-topbar__greeting { display: none; }
|
|
471
|
+
.rz-content { padding: 18px 14px 60px; }
|
|
472
|
+
/* The sidebar is a top strip now, so it must not be sticky-height; the topbar stays pinned. */
|
|
473
|
+
.rz-sidebar, .rz-sidebar--collapsed { position: static; height: auto; }
|
|
474
|
+
.rz-sidebar__item--disabled { display: none; }
|
|
475
|
+
/* Save bar: bleed matches the narrower content padding; stack status above the buttons. */
|
|
476
|
+
.rz-content .realiiz-form__bar { margin: 18px -14px -60px; padding: 10px 14px; flex-wrap: wrap; }
|
|
477
|
+
.rz-content .realiiz-form__status { flex-basis: 100%; margin-right: 0; font-size: .8rem; }
|
|
478
|
+
/* Editor toolbar wraps; the Insert group drops its label and left rule to save width. */
|
|
479
|
+
.rz-admin .realiiz-md__head { padding: 5px; }
|
|
480
|
+
.rz-admin .realiiz-md__insert { border-left: 0; padding-left: 0; margin-left: 0; }
|
|
481
|
+
.rz-admin .realiiz-md__lab { display: none; }
|
|
482
|
+
.rz-admin .realiiz-md--fs { padding: 8px; }
|
|
483
|
+
/* Tracker: notes hide, labels shrink, so three steps fit a phone. */
|
|
484
|
+
.rz-step__note { display: none; }
|
|
485
|
+
.rz-step__label { font-size: .8rem; }
|
|
486
|
+
.rz-step:not(:last-child)::after { right: 6px; }
|
|
487
|
+
/* Tables scroll sideways inside their own box instead of stretching the page. */
|
|
488
|
+
.rz-table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
489
|
+
.rz-pagehead { align-items: flex-start; flex-direction: column; }
|
|
490
|
+
.rz-notice { flex-direction: column; align-items: flex-start; }
|
|
491
|
+
.rz-card--narrow { max-width: none; }
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* ---- forms extras: markdown editor, chips, gauges, preview prose --------- */
|
|
495
|
+
.rz-admin .realiiz-md { --_md-head: var(--surface-2); --_md-hover: var(--surface-2); --_md-hover-line: var(--signal-glow); --_md-on: color-mix(in srgb, var(--signal) 12%, var(--surface-1)); --_md-tip: var(--ink); }
|
|
496
|
+
.rz-admin .realiiz-md__mode { background: var(--surface-2); }
|
|
497
|
+
.rz-admin .realiiz-md__mode button { color: var(--text-secondary); }
|
|
498
|
+
.rz-admin .realiiz-md__mode button[aria-selected="true"] { background: var(--surface-1); color: var(--text-primary); box-shadow: 0 1px 2px rgba(14,23,38,.12); }
|
|
499
|
+
.rz-admin .realiiz-md__panel, .rz-admin .realiiz-md__tgrid, .rz-admin .realiiz-md--fs, .rz-admin .realiiz-md .realiiz-md__ta, .rz-admin .realiiz-md__preview, .rz-admin .realiiz-md__head button { background: var(--surface-1); }
|
|
500
|
+
.rz-admin .realiiz-md__head { background: var(--surface-2); }
|
|
501
|
+
.rz-admin .realiiz-md__head button { color: var(--text-primary); }
|
|
502
|
+
.rz-admin .realiiz-md__insert button { background: var(--surface-2); color: var(--text-secondary); }
|
|
503
|
+
.rz-admin .realiiz-md__tgcells i { background: var(--surface-1); }
|
|
504
|
+
.rz-admin .realiiz-md__tgcells i.hot { background: color-mix(in srgb, var(--signal) 18%, var(--surface-1)); }
|
|
505
|
+
.rz-admin .realiiz-md__loading { color: var(--text-muted); font-size: .8rem; }
|
|
506
|
+
.rz-admin .realiiz-prose { max-width: 68ch; font-size: 1rem; line-height: 1.65; color: var(--text-primary); }
|
|
507
|
+
.rz-admin .realiiz-prose h1, .rz-admin .realiiz-prose h2, .rz-admin .realiiz-prose h3 { font-family: var(--rz-font-display); line-height: 1.25; margin: 1.4em 0 .5em; }
|
|
508
|
+
.rz-admin .realiiz-prose h2 { font-size: 1.35rem; } .rz-admin .realiiz-prose h3 { font-size: 1.1rem; }
|
|
509
|
+
.rz-admin .realiiz-prose p, .rz-admin .realiiz-prose ul, .rz-admin .realiiz-prose ol, .rz-admin .realiiz-prose blockquote { margin: 0 0 1em; }
|
|
510
|
+
.rz-admin .realiiz-prose a { color: var(--signal); }
|
|
511
|
+
/* The site's prose class picks a slate body colour for a light page; on the dark surface that reads as disabled. */
|
|
512
|
+
.rz-admin[data-theme="dark"] .realiiz-prose, .rz-admin[data-theme="dark"] .realiiz-prose :is(p, li, td, th, dd, dt, figcaption, strong, b, em, i, h1, h2, h3, h4, h5, h6) { color: var(--text-primary); }
|
|
513
|
+
.rz-admin[data-theme="dark"] .realiiz-prose :is(code, kbd) { color: var(--text-primary); background: var(--surface-3); }
|
|
514
|
+
.rz-admin[data-theme="dark"] .realiiz-prose a { color: var(--signal-dim); }
|
|
515
|
+
.rz-admin[data-theme="dark"] .realiiz-prose hr { border-color: var(--border); }
|
|
516
|
+
.rz-admin[data-theme="dark"] .realiiz-prose blockquote { color: var(--text-secondary); }
|
|
517
|
+
.rz-admin .realiiz-prose blockquote { border-left: 3px solid var(--border); padding-left: 1em; color: var(--text-secondary); }
|
|
518
|
+
.rz-admin .realiiz-prose code { background: var(--surface-2); padding: 0 4px; border-radius: 4px; font-family: var(--rz-font-mono); font-size: .9em; }
|
|
519
|
+
.rz-admin .realiiz-prose pre { background: var(--surface-2); padding: 12px 14px; border-radius: 6px; overflow-x: auto; }
|
|
520
|
+
.rz-admin .realiiz-prose img { max-width: 100%; height: auto; border-radius: 6px; }
|
|
521
|
+
.rz-admin .realiiz-chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; min-height: 40px; padding: 5px 8px; background: var(--surface-0); border: 1px solid var(--border); border-radius: var(--rz-radius); }
|
|
522
|
+
.rz-admin .realiiz-chips:focus-within { border-color: var(--signal); box-shadow: var(--realiiz-form-focus-ring); }
|
|
523
|
+
.rz-admin .realiiz-chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 4px 2px 9px; background: var(--surface-3); border: 1px solid var(--border); border-radius: 999px; font-size: .8rem; color: var(--text-primary); }
|
|
524
|
+
.rz-admin .realiiz-chip button { font: inherit; line-height: 1; width: 18px; height: 18px; border-radius: 50%; border: 0; background: transparent; color: var(--text-muted); cursor: pointer; }
|
|
525
|
+
.rz-admin .realiiz-chip button:hover { background: var(--status-down); color: #fff; }
|
|
526
|
+
.rz-admin .realiiz-chips__input { flex: 1; min-width: 120px; border: 0; background: transparent; font: inherit; color: inherit; outline: none; padding: 4px; }
|
|
527
|
+
.rz-admin .realiiz-chips__count { margin-left: auto; color: var(--text-muted); font-size: .75rem; font-variant-numeric: tabular-nums; }
|
|
528
|
+
.rz-admin .realiiz-gauge { color: var(--text-muted); }
|
|
529
|
+
.rz-admin .realiiz-gauge__bar { background: var(--surface-3); }
|
|
530
|
+
.rz-admin .realiiz-gauge__zone { background: color-mix(in srgb, var(--status-up) 28%, transparent); }
|
|
531
|
+
.rz-admin .realiiz-gauge__fill { background: var(--text-muted); }
|
|
532
|
+
.rz-admin .realiiz-gauge--ok .realiiz-gauge__fill { background: var(--status-up); } .rz-admin .realiiz-gauge--ok { color: var(--status-up); }
|
|
533
|
+
.rz-admin .realiiz-gauge--over .realiiz-gauge__fill { background: var(--status-down); } .rz-admin .realiiz-gauge--over { color: var(--status-down); }
|
|
534
|
+
`;
|
|
535
|
+
function AdminShell({
|
|
536
|
+
siteName,
|
|
537
|
+
productName = "Studio",
|
|
538
|
+
logoUrl,
|
|
539
|
+
user,
|
|
540
|
+
role,
|
|
541
|
+
nav,
|
|
542
|
+
pinnedNav,
|
|
543
|
+
activeHref,
|
|
544
|
+
basePath = "/admin",
|
|
545
|
+
signOutPath,
|
|
546
|
+
accountHref,
|
|
547
|
+
helpHref,
|
|
548
|
+
branding,
|
|
549
|
+
defaultTheme = "light",
|
|
550
|
+
children
|
|
551
|
+
}) {
|
|
552
|
+
const [theme, setTheme] = useTheme(defaultTheme);
|
|
553
|
+
const showNav = Boolean(user && role);
|
|
554
|
+
const shownName = branding?.name?.trim() || siteName;
|
|
555
|
+
const shownLogo = branding?.logoUrl || logoUrl;
|
|
556
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-admin", "data-theme": theme, children: [
|
|
557
|
+
/* @__PURE__ */ jsxRuntime.jsx("style", { children: SHELL_CSS }),
|
|
558
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
559
|
+
TopBar,
|
|
560
|
+
{
|
|
561
|
+
siteName: shownName,
|
|
562
|
+
productName,
|
|
563
|
+
logoUrl: shownLogo,
|
|
564
|
+
homeHref: basePath,
|
|
565
|
+
user,
|
|
566
|
+
role,
|
|
567
|
+
theme,
|
|
568
|
+
onTheme: setTheme,
|
|
569
|
+
signOutPath: signOutPath ?? `${basePath}/auth/signout`,
|
|
570
|
+
accountHref: accountHref === null ? void 0 : accountHref ?? `${basePath}/account`,
|
|
571
|
+
helpHref: helpHref === null ? void 0 : helpHref ?? `${basePath}/help`
|
|
572
|
+
}
|
|
573
|
+
),
|
|
574
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-body", children: [
|
|
575
|
+
showNav && /* @__PURE__ */ jsxRuntime.jsx(Sidebar, { items: nav, pinned: pinnedNav, activeHref, basePath }),
|
|
576
|
+
/* @__PURE__ */ jsxRuntime.jsx("main", { className: "rz-main", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-content", children }) })
|
|
577
|
+
] })
|
|
578
|
+
] });
|
|
579
|
+
}
|
|
580
|
+
function buttonClass(variant = "secondary", size = "md", extra) {
|
|
581
|
+
return ["rz-btn", `rz-btn--${variant}`, `rz-btn--${size}`, extra].filter(Boolean).join(" ");
|
|
582
|
+
}
|
|
583
|
+
function Button({ variant = "secondary", size = "md", className, type = "button", ...rest }) {
|
|
584
|
+
return /* @__PURE__ */ jsxRuntime.jsx("button", { type, className: buttonClass(variant, size, className), ...rest });
|
|
585
|
+
}
|
|
586
|
+
function LinkButton({ variant = "secondary", size = "md", className, external, forward, children, ...rest }) {
|
|
587
|
+
const extra = external ? { target: "_blank", rel: "noreferrer", ...rest } : rest;
|
|
588
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("a", { className: buttonClass(variant, size, className), ...extra, children: [
|
|
589
|
+
children,
|
|
590
|
+
external && /* @__PURE__ */ jsxRuntime.jsx(OutboundArrow, {}),
|
|
591
|
+
forward && !external && /* @__PURE__ */ jsxRuntime.jsx(ForwardArrow, {})
|
|
592
|
+
] });
|
|
593
|
+
}
|
|
594
|
+
function ForwardArrow() {
|
|
595
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "rz-arrow rz-arrow--fwd", children: [
|
|
596
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 12h14" }),
|
|
597
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m13 6 6 6-6 6" })
|
|
598
|
+
] });
|
|
599
|
+
}
|
|
600
|
+
function OutboundArrow() {
|
|
601
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "rz-arrow", children: [
|
|
602
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7 17 17 7" }),
|
|
603
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7 7h10v10" })
|
|
604
|
+
] });
|
|
605
|
+
}
|
|
606
|
+
function Card({ title, meta, note, children, actions, className }) {
|
|
607
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["rz-card", className].filter(Boolean).join(" "), children: [
|
|
608
|
+
title && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-card__title", children: title }),
|
|
609
|
+
meta && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-card__meta", children: meta }),
|
|
610
|
+
note && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-card__note", children: note }),
|
|
611
|
+
children,
|
|
612
|
+
actions && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-card__actions", children: actions })
|
|
613
|
+
] });
|
|
614
|
+
}
|
|
615
|
+
function PageHeader({ eyebrow, title, subtitle, actions }) {
|
|
616
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-pagehead", children: [
|
|
617
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
618
|
+
eyebrow && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-pagehead__eyebrow", children: eyebrow }),
|
|
619
|
+
/* @__PURE__ */ jsxRuntime.jsx("h1", { children: title }),
|
|
620
|
+
subtitle && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-pagehead__sub", children: subtitle })
|
|
621
|
+
] }),
|
|
622
|
+
actions && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-card__actions", style: { marginTop: 0 }, children: actions })
|
|
623
|
+
] });
|
|
624
|
+
}
|
|
625
|
+
function StatusPill({ tone = "neutral", children, title }) {
|
|
626
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `rz-pill rz-pill--${tone}`, title, children });
|
|
627
|
+
}
|
|
628
|
+
function EmptyState({ title, children, action }) {
|
|
629
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-empty", children: [
|
|
630
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { children: title }),
|
|
631
|
+
children && /* @__PURE__ */ jsxRuntime.jsx("div", { children }),
|
|
632
|
+
action && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginTop: 14 }, children: action })
|
|
633
|
+
] });
|
|
634
|
+
}
|
|
635
|
+
function Steps({ steps }) {
|
|
636
|
+
return /* @__PURE__ */ jsxRuntime.jsx("ol", { className: "rz-steps", "aria-label": "Progress", children: steps.map((st, i) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: `rz-step rz-step--${st.state}`, "aria-current": st.state === "active" ? "step" : void 0, children: [
|
|
637
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-step__dot", "aria-hidden": "true", children: st.state === "done" ? "\u2713" : st.state === "failed" ? "!" : i + 1 }),
|
|
638
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "rz-step__text", children: [
|
|
639
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-step__label", children: st.label }),
|
|
640
|
+
st.note && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-step__note", children: st.note })
|
|
641
|
+
] })
|
|
642
|
+
] }, i)) });
|
|
643
|
+
}
|
|
644
|
+
function Tabs({ options, value, onChange, label = "Sections" }) {
|
|
645
|
+
const list = react.useRef(null);
|
|
646
|
+
const onKey = (e) => {
|
|
647
|
+
const i = options.findIndex((o) => o.value === value);
|
|
648
|
+
if (i < 0) return;
|
|
649
|
+
let next = i;
|
|
650
|
+
if (e.key === "ArrowRight") next = (i + 1) % options.length;
|
|
651
|
+
else if (e.key === "ArrowLeft") next = (i - 1 + options.length) % options.length;
|
|
652
|
+
else if (e.key === "Home") next = 0;
|
|
653
|
+
else if (e.key === "End") next = options.length - 1;
|
|
654
|
+
else return;
|
|
655
|
+
e.preventDefault();
|
|
656
|
+
onChange(options[next].value);
|
|
657
|
+
list.current?.children[next]?.focus();
|
|
658
|
+
};
|
|
659
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: list, className: "rz-tabs", role: "tablist", "aria-label": label, onKeyDown: onKey, children: options.map((o) => {
|
|
660
|
+
const active = o.value === value;
|
|
661
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
662
|
+
"button",
|
|
663
|
+
{
|
|
664
|
+
type: "button",
|
|
665
|
+
role: "tab",
|
|
666
|
+
className: "rz-tabs__tab",
|
|
667
|
+
"aria-selected": active,
|
|
668
|
+
tabIndex: active ? 0 : -1,
|
|
669
|
+
onClick: () => onChange(o.value),
|
|
670
|
+
children: [
|
|
671
|
+
o.label,
|
|
672
|
+
o.suffix && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "rz-muted", children: [
|
|
673
|
+
" ",
|
|
674
|
+
o.suffix
|
|
675
|
+
] })
|
|
676
|
+
]
|
|
677
|
+
},
|
|
678
|
+
o.value
|
|
679
|
+
);
|
|
680
|
+
}) });
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// src/studio-ui/css.ts
|
|
684
|
+
var STUDIO_CSS = `
|
|
685
|
+
/*
|
|
686
|
+
* /admin \u2014 overrides only. Layout, colour and components come from
|
|
687
|
+
* @realiizlabs/admin/shell (SHELL_CSS, injected by AdminShell). The Realiiz
|
|
688
|
+
* palette is the package default, so this file just maps the shell's font
|
|
689
|
+
* hooks onto the site's loaded fonts. To rebrand, redeclare tokens here:
|
|
690
|
+
* .rz-admin { --signal: #0a58ca; }
|
|
691
|
+
*/
|
|
692
|
+
.rz-admin {
|
|
693
|
+
--rz-font: var(--font-inter), system-ui, sans-serif;
|
|
694
|
+
--rz-font-display: var(--font-space-grotesk), system-ui, sans-serif;
|
|
695
|
+
--rz-font-mono: var(--font-jetbrains-mono), ui-monospace, monospace;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/* Posts table: one wide column for the post (title, slug beneath in small mono) and
|
|
699
|
+
tight fixed columns for the known-width bits. Nothing wraps; the title and slug end in
|
|
700
|
+
an ellipsis with the full text as tooltip. The title opens the live post; the outbound
|
|
701
|
+
arrow sits inline after it and appears on hover (the shell's a:hover .rz-arrow motion). */
|
|
702
|
+
.rz-admin .rz-table--posts { table-layout: fixed; width: 100%; }
|
|
703
|
+
.rz-admin .rz-table--posts td, .rz-admin .rz-table--posts th { white-space: nowrap; overflow: hidden; }
|
|
704
|
+
.rz-admin .rz-col-title { width: auto; }
|
|
705
|
+
.rz-admin .rz-col-date { width: 7.5rem; }
|
|
706
|
+
.rz-admin .rz-col-status { width: 11.5rem; }
|
|
707
|
+
.rz-admin .rz-col-actions { width: 8.5rem; }
|
|
708
|
+
.rz-admin .rz-title-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
709
|
+
.rz-admin .rz-slug { font-family: var(--rz-font-mono); font-size: .78rem; margin-top: .2rem; }
|
|
710
|
+
.rz-admin [data-slugs="off"] .rz-slug { display: none; }
|
|
711
|
+
.rz-admin .rz-toolbar { display: flex; justify-content: flex-end; margin: 0 0 .6rem; }
|
|
712
|
+
.rz-admin .rz-switch { display: inline-flex; align-items: center; gap: .55rem; font-size: .8rem; color: var(--text-secondary); cursor: pointer; user-select: none; }
|
|
713
|
+
.rz-admin .rz-switch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
|
|
714
|
+
.rz-admin .rz-switch__track { position: relative; width: 30px; height: 18px; border-radius: 999px; background: var(--surface-3); transition: background .15s; }
|
|
715
|
+
.rz-admin .rz-switch__thumb { position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.25); transition: transform .15s; }
|
|
716
|
+
.rz-admin .rz-switch input:checked + .rz-switch__track { background: var(--signal); }
|
|
717
|
+
.rz-admin .rz-switch input:checked + .rz-switch__track .rz-switch__thumb { transform: translateX(12px); }
|
|
718
|
+
.rz-admin .rz-switch input:focus-visible + .rz-switch__track { outline: 2px solid var(--signal); outline-offset: 2px; }
|
|
719
|
+
.rz-admin .rz-switch:hover .rz-switch__label { color: var(--text-primary); }
|
|
720
|
+
.rz-admin .rz-title-link { display: flex; align-items: center; gap: .35em; max-width: 100%; color: inherit; text-decoration: none; }
|
|
721
|
+
.rz-admin .rz-title-link .rz-arrow { opacity: 0; }
|
|
722
|
+
.rz-admin .rz-title-link:hover { color: var(--signal); }
|
|
723
|
+
.rz-admin .rz-title-link:hover .rz-arrow { opacity: 1; }
|
|
724
|
+
@media (max-width: 720px) {
|
|
725
|
+
.rz-admin .rz-col-status { width: 9rem; }
|
|
726
|
+
.rz-admin .rz-col-actions { width: 6rem; }
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/* Dashboard card for a section that isn't built yet: the real card, faded, buttons disabled. */
|
|
730
|
+
/* Account page (05b): tracker-style \u2014 H1, email, underlined tabs, stacked form cards. */
|
|
731
|
+
.rz-admin .acct-h1 { font-size: 1.25rem; font-weight: 600; margin: 0 0 .25rem; color: var(--text-primary); }
|
|
732
|
+
.rz-admin .acct-email { font-size: .875rem; color: var(--text-muted); margin: 0 0 1.25rem; }
|
|
733
|
+
.rz-admin .acct-stack { display: flex; flex-direction: column; gap: 1rem; max-width: 560px; }
|
|
734
|
+
.rz-admin .acct-stack:has(.acct-team) { max-width: none; }
|
|
735
|
+
.rz-admin .acct-card { display: flex; flex-direction: column; gap: .9rem; }
|
|
736
|
+
.rz-admin .acct-title { font-size: 1rem; font-weight: 600; margin: 0; color: var(--text-primary); }
|
|
737
|
+
.rz-admin .acct-blurb { font-size: .875rem; color: var(--text-secondary); margin: -.4rem 0 0; line-height: 1.5; }
|
|
738
|
+
.rz-admin .acct-field { display: flex; flex-direction: column; gap: .35rem; }
|
|
739
|
+
.rz-admin .acct-label { font-size: .85rem; font-weight: 500; color: var(--text-primary); }
|
|
740
|
+
.rz-admin .acct-hint { font-size: .78rem; color: var(--text-muted); }
|
|
741
|
+
.rz-admin .acct-input { font: inherit; font-size: .9rem; color: var(--text-primary); background: var(--surface-0); border: 1px solid var(--border); border-radius: 6px; padding: 9px 11px; min-height: 40px; width: 100%; }
|
|
742
|
+
.rz-admin .acct-input:read-only { color: var(--text-secondary); background: var(--surface-2); }
|
|
743
|
+
.rz-admin .acct-input:focus-visible { outline: 2px solid var(--signal); outline-offset: 1px; }
|
|
744
|
+
.rz-admin .acct-select { width: auto; padding-right: 2rem; }
|
|
745
|
+
.rz-admin .acct-select--sm { min-height: 32px; padding: 4px 28px 4px 8px; font-size: .8rem; }
|
|
746
|
+
.rz-admin .acct-actions { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
|
|
747
|
+
.rz-admin .acct-ok { font-size: .85rem; color: var(--signal); }
|
|
748
|
+
.rz-admin .acct-link { font: inherit; font-size: .82rem; background: none; border: 0; padding: 0; color: var(--text-secondary); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
|
|
749
|
+
.rz-admin .acct-link:hover { color: var(--signal); }
|
|
750
|
+
.rz-admin .acct-link--danger:hover { color: #c0392b; }
|
|
751
|
+
.rz-admin .acct-link:disabled { opacity: .5; cursor: default; }
|
|
752
|
+
.rz-admin .acct-team-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
|
|
753
|
+
.rz-admin .acct-invite__row { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; }
|
|
754
|
+
.rz-admin .acct-invite__row .acct-input[type="email"] { flex: 1 1 220px; width: auto; }
|
|
755
|
+
.rz-admin .acct-person { display: flex; align-items: center; gap: .7rem; }
|
|
756
|
+
.rz-admin .acct-person__avatar { width: 32px; height: 32px; border-radius: 50%; background: var(--surface-3); border: 1px solid var(--border); display: inline-flex; align-items: center; justify-content: center; font-size: .7rem; font-weight: 600; overflow: hidden; flex-shrink: 0; }
|
|
757
|
+
.rz-admin .acct-person__avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
758
|
+
.rz-admin .acct-person__text { display: flex; flex-direction: column; line-height: 1.25; }
|
|
759
|
+
.rz-admin .acct-person__email { font-size: .78rem; }
|
|
760
|
+
.rz-admin .acct-row-actions { display: flex; gap: .8rem; align-items: center; white-space: nowrap; min-height: 34px; }
|
|
761
|
+
.rz-admin .acct-help-list { margin: 0; padding-left: 1.2rem; color: var(--text-secondary); line-height: 1.7; }
|
|
762
|
+
|
|
763
|
+
/* Sign-in: the password option under the magic-link form. */
|
|
764
|
+
.rz-admin .pwsi-toggle { margin: 1.1rem 0 0; text-align: center; }
|
|
765
|
+
.rz-admin .pwsi { display: flex; flex-direction: column; gap: .9rem; margin-top: 1.25rem; padding-top: 1.25rem; border-top: 1px solid var(--separator); }
|
|
766
|
+
|
|
767
|
+
/* Password boxes: eye toggle + the requirement checklist that lights up as each rule passes. */
|
|
768
|
+
.rz-admin .acct-pw { position: relative; }
|
|
769
|
+
.rz-admin .acct-pw .acct-input { padding-right: 2.6rem; }
|
|
770
|
+
.rz-admin .acct-pw__eye { position: absolute; right: .5rem; top: 50%; transform: translateY(-50%); background: none; border: 0; padding: .35rem; color: var(--text-muted); cursor: pointer; display: inline-flex; border-radius: 4px; }
|
|
771
|
+
.rz-admin .acct-pw__eye:hover, .rz-admin .acct-pw__eye[aria-pressed="true"] { color: var(--text-secondary); }
|
|
772
|
+
.rz-admin .acct-rules { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .5rem; }
|
|
773
|
+
.rz-admin .acct-rule { display: flex; align-items: center; gap: .5rem; font-size: .8125rem; color: var(--text-muted); transition: color .15s; }
|
|
774
|
+
.rz-admin .acct-rule--met { color: var(--signal); }
|
|
775
|
+
|
|
776
|
+
/* Picture uploader (avatar, logo, favicon): one grid everywhere \u2014 preview | buttons row, hint under the buttons. */
|
|
777
|
+
.rz-admin .acct-pic { border: 1px dashed var(--border); border-radius: 8px; padding: 16px 18px; background: var(--surface-0); outline: none; transition: border-color .15s; }
|
|
778
|
+
.rz-admin .acct-pic:focus-visible, .rz-admin .acct-pic--over { border-color: var(--signal); border-style: solid; }
|
|
779
|
+
.rz-admin .acct-pic--busy { opacity: .7; }
|
|
780
|
+
.rz-admin .acct-pic__file { display: none; }
|
|
781
|
+
.rz-admin .acct-pic__empty { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; color: var(--text-muted); }
|
|
782
|
+
.rz-admin .acct-pic__fallback { width: 44px; height: 44px; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; border-radius: 8px; background: var(--surface-3); color: var(--text-muted); font-size: .9rem; font-weight: 600; letter-spacing: .04em; }
|
|
783
|
+
.rz-admin .acct-pic__fallback--circle { border-radius: 50%; }
|
|
784
|
+
.rz-admin .acct-pic__or { font-size: .8125rem; }
|
|
785
|
+
.rz-admin .acct-pic__has { display: flex; align-items: flex-start; gap: 16px; }
|
|
786
|
+
.rz-admin .acct-pic__thumb { display: block; width: 96px; height: 96px; object-fit: cover; border-radius: 8px; border: 1px solid var(--border); background: var(--surface-3); flex-shrink: 0; }
|
|
787
|
+
.rz-admin .acct-pic__thumb--circle { border-radius: 50%; }
|
|
788
|
+
.rz-admin .acct-pic__meta { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
|
|
789
|
+
.rz-admin .acct-pic__actions { display: flex; align-items: center; gap: 14px; }
|
|
790
|
+
.rz-admin .acct-pic__remove { font: inherit; font-size: .8125rem; background: none; border: 0; padding: 0; color: var(--text-muted); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
|
|
791
|
+
.rz-admin .acct-pic__remove:hover { color: #c0392b; }
|
|
792
|
+
.rz-admin .acct-pic__remove:disabled { cursor: default; opacity: .6; }
|
|
793
|
+
.rz-admin .acct-pic__hint { margin: 10px 0 0; font-size: .75rem; color: var(--text-muted); line-height: 1.4; }
|
|
794
|
+
.rz-admin .acct-pic__has .acct-pic__hint { margin-top: 0; }
|
|
795
|
+
.rz-admin .acct-pic__note { margin: 8px 0 0; }
|
|
796
|
+
.rz-admin .acct-pic__error { margin: 8px 0 0; font-size: .8rem; color: #c0392b; }
|
|
797
|
+
|
|
798
|
+
/* Branded dropdown (role pickers): a bordered button that opens the shell's menu, checkmark on the current choice. */
|
|
799
|
+
.rz-admin .acct-dd { position: relative; display: inline-block; }
|
|
800
|
+
.rz-admin .acct-dd__btn { display: inline-flex; align-items: center; gap: .5rem; font: inherit; font-size: .85rem; color: var(--text-primary); background: var(--surface-0); border: 1px solid var(--border); border-radius: 6px; padding: 0 .7rem; height: 34px; cursor: pointer; }
|
|
801
|
+
.rz-admin .acct-dd__btn:hover { border-color: var(--text-muted); }
|
|
802
|
+
.rz-admin .acct-dd__btn:focus-visible { outline: 2px solid var(--signal); outline-offset: 1px; }
|
|
803
|
+
.rz-admin .acct-dd__btn:disabled { opacity: .6; cursor: default; }
|
|
804
|
+
.rz-admin .acct-dd__menu { left: 0; right: auto; min-width: 15rem; padding: .3rem 0; }
|
|
805
|
+
.rz-admin .acct-dd__item { align-items: flex-start; }
|
|
806
|
+
.rz-admin .acct-dd__check { width: 1rem; flex-shrink: 0; color: var(--signal); font-weight: 700; }
|
|
807
|
+
.rz-admin .acct-dd__text { display: flex; flex-direction: column; line-height: 1.3; }
|
|
808
|
+
.rz-admin .acct-dd__hint { font-size: .74rem; color: var(--text-muted); }
|
|
809
|
+
/* Team table: everything on one centre line, and the role menu may overflow the table (the shell
|
|
810
|
+
clips .rz-table for rounded corners, so round the header cells by hand instead). */
|
|
811
|
+
.rz-admin .acct-team { overflow: visible; }
|
|
812
|
+
.rz-admin .acct-team thead th:first-child { border-top-left-radius: 8px; }
|
|
813
|
+
.rz-admin .acct-team thead th:last-child { border-top-right-radius: 8px; }
|
|
814
|
+
.rz-admin .acct-team tbody tr:last-child td { border-bottom: 0; }
|
|
815
|
+
.rz-admin .acct-team td { vertical-align: middle; }
|
|
816
|
+
.rz-admin .acct-team td > * { vertical-align: middle; }
|
|
817
|
+
.rz-admin .acct-team .rz-pill { position: relative; top: 0; }
|
|
818
|
+
.rz-admin .acct-dd__menu { z-index: 60; }
|
|
819
|
+
.rz-admin .acct-invite__row .acct-dd__btn { height: 40px; }
|
|
820
|
+
|
|
821
|
+
.rz-admin .rz-term { position: relative; display: inline; }
|
|
822
|
+
.rz-admin .rz-term__word { cursor: help; text-decoration: underline dotted; text-underline-offset: 4px; text-decoration-color: color-mix(in srgb, currentColor 50%, transparent); }
|
|
823
|
+
.rz-admin .rz-term__word:hover, .rz-admin .rz-term__word:focus-visible { text-decoration-color: var(--signal); outline: none; }
|
|
824
|
+
.rz-admin .rz-term__tip { position: absolute; left: 0; top: calc(100% + 8px); z-index: 70; width: 272px; max-width: 80vw; padding: 12px 14px; border-radius: 8px; background: var(--text-primary); color: var(--surface-0); font-size: .8125rem; line-height: 1.4; font-weight: 400; box-shadow: 0 8px 24px rgba(14,23,38,.18); opacity: 0; pointer-events: none; transition: opacity .15s; }
|
|
825
|
+
.rz-admin .rz-term__tip--open { opacity: 1; }
|
|
826
|
+
`;
|
|
827
|
+
function StudioShellFrame({ siteName, logoUrl, user, role, nav, pinnedNav, branding, children }) {
|
|
828
|
+
const pathname = navigation.usePathname() ?? "/admin";
|
|
829
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
830
|
+
/* @__PURE__ */ jsxRuntime.jsx("style", { children: STUDIO_CSS }),
|
|
831
|
+
/* @__PURE__ */ jsxRuntime.jsx(AdminShell, { siteName, logoUrl: logoUrl ?? void 0, user, role, nav, pinnedNav, branding, accountHref: null, activeHref: pathname, children })
|
|
832
|
+
] });
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/forms-ui/styles.ts
|
|
836
|
+
var FORM_CSS = `
|
|
837
|
+
.realiiz-form{
|
|
838
|
+
--_fg:var(--realiiz-form-fg,#111);
|
|
839
|
+
--_muted:var(--realiiz-form-muted,#666);
|
|
840
|
+
--_border:var(--realiiz-form-border,#cfcfcf);
|
|
841
|
+
--_accent:var(--realiiz-form-accent,#111);
|
|
842
|
+
--_accent-fg:var(--realiiz-form-accent-fg,#fff);
|
|
843
|
+
--_danger:var(--realiiz-form-danger,#b3261e);
|
|
844
|
+
--_radius:var(--realiiz-form-radius,6px);
|
|
845
|
+
--_font:var(--realiiz-form-font,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif);
|
|
846
|
+
--_ring:var(--realiiz-form-focus-ring,0 0 0 3px rgba(0,0,0,.15));
|
|
847
|
+
box-sizing:border-box;max-width:100%;color:var(--_fg);font-family:var(--_font);font-size:15px;line-height:1.4;
|
|
848
|
+
}
|
|
849
|
+
.realiiz-form *,.realiiz-form *::before,.realiiz-form *::after{box-sizing:inherit}
|
|
850
|
+
.realiiz-form fieldset{border:0;margin:0 0 20px;padding:0;min-width:0}
|
|
851
|
+
.realiiz-form legend{font-weight:600;font-size:13px;letter-spacing:.02em;text-transform:uppercase;color:var(--_muted);margin-bottom:12px;padding:0}
|
|
852
|
+
.realiiz-form__field{display:block;margin:0 0 16px}
|
|
853
|
+
.realiiz-form__label{display:block;font-weight:500;margin-bottom:6px}
|
|
854
|
+
.realiiz-form__required{color:var(--_danger);margin-left:3px}
|
|
855
|
+
.realiiz-form__control{display:block;width:100%;max-width:100%;font:inherit;color:inherit;background:#fff;border:1px solid var(--_border);border-radius:var(--_radius);padding:9px 11px;min-height:40px}
|
|
856
|
+
.realiiz-form__control:focus{outline:none;border-color:var(--_accent);box-shadow:var(--_ring)}
|
|
857
|
+
.realiiz-form__control--textarea{min-height:110px;resize:vertical}
|
|
858
|
+
.realiiz-form__control--body{min-height:280px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:14px}
|
|
859
|
+
.realiiz-form__control--invalid{border-color:var(--_danger)}
|
|
860
|
+
.realiiz-select{position:relative}
|
|
861
|
+
.realiiz-select__btn{display:flex;align-items:center;justify-content:space-between;gap:10px;text-align:left;cursor:pointer;background:#fff}
|
|
862
|
+
.realiiz-select__btn svg{flex-shrink:0;color:var(--_muted);transition:transform .15s}
|
|
863
|
+
.realiiz-select__btn--open{border-color:var(--_accent);box-shadow:var(--_ring)}
|
|
864
|
+
.realiiz-select__btn--open svg{transform:rotate(180deg)}
|
|
865
|
+
.realiiz-select__btn:disabled{opacity:.6;cursor:default}
|
|
866
|
+
.realiiz-select__placeholder{color:var(--_muted)}
|
|
867
|
+
.realiiz-select__menu{position:absolute;left:0;right:0;top:calc(100% + 6px);margin:0;padding:6px 0;list-style:none;background:#fff;border:1px solid var(--_border);border-radius:var(--_radius);box-shadow:0 8px 24px rgba(14,23,38,.14);max-height:260px;overflow:auto;z-index:40}
|
|
868
|
+
.realiiz-select__item{display:flex;align-items:center;gap:8px;padding:8px 12px;font-size:14px;color:var(--_fg);cursor:pointer}
|
|
869
|
+
.realiiz-select__item--active{background:var(--realiiz-form-hover,rgba(0,0,0,.05))}
|
|
870
|
+
.realiiz-select__item--selected{font-weight:600}
|
|
871
|
+
.realiiz-select__check{width:1em;flex-shrink:0;color:var(--_accent);font-weight:700}
|
|
872
|
+
.realiiz-form__check{display:flex;align-items:center;gap:8px;font-weight:500}
|
|
873
|
+
.realiiz-form__check input{width:18px;height:18px;margin:0}
|
|
874
|
+
.realiiz-form__helper{display:flex;justify-content:space-between;gap:12px;font-size:12px;color:var(--_muted);margin-top:4px;min-height:1em}
|
|
875
|
+
.realiiz-form__count{margin-left:auto;font-variant-numeric:tabular-nums}
|
|
876
|
+
.realiiz-form__error{color:var(--_danger);font-size:13px;margin-top:4px}
|
|
877
|
+
.realiiz-image{border:1px dashed var(--_border);border-radius:var(--_radius);padding:16px 18px;background:#fff;outline:none;transition:border-color .15s}
|
|
878
|
+
.realiiz-image--over{border-color:var(--_accent);border-style:solid}
|
|
879
|
+
.realiiz-image--invalid{border-color:var(--_danger)}
|
|
880
|
+
.realiiz-image--busy{opacity:.7}
|
|
881
|
+
.realiiz-image__empty{display:flex;align-items:center;gap:14px;flex-wrap:wrap;color:var(--_muted)}
|
|
882
|
+
.realiiz-image__icon{flex-shrink:0;color:var(--_muted)}
|
|
883
|
+
.realiiz-image__or{font-size:13px}
|
|
884
|
+
.realiiz-image__has{display:flex;align-items:flex-start;gap:16px}
|
|
885
|
+
.realiiz-image__thumb{display:block;max-height:160px;max-width:min(320px,55%);width:auto;object-fit:cover;border-radius:8px;border:1px solid var(--_border);background:#f6f8fa}
|
|
886
|
+
.realiiz-image__meta{display:flex;flex-direction:column;gap:10px;min-width:0}
|
|
887
|
+
.realiiz-image__detail{font-size:13px;color:var(--_muted);word-break:break-all}
|
|
888
|
+
.realiiz-image__actions{display:flex;align-items:center;gap:14px}
|
|
889
|
+
.realiiz-image__remove{font:inherit;font-size:13px;background:none;border:0;padding:0;color:var(--_muted);text-decoration:underline;text-underline-offset:3px;cursor:pointer}
|
|
890
|
+
.realiiz-image__remove:hover{color:var(--_danger)}
|
|
891
|
+
.realiiz-image__hint{font-size:12px;color:var(--_muted);margin-top:10px}
|
|
892
|
+
.realiiz-image .realiiz-form__error{margin-top:8px}
|
|
893
|
+
@media (max-width:720px){.realiiz-image__has{flex-direction:column}.realiiz-image__thumb{max-width:100%}}
|
|
894
|
+
.realiiz-form__alert{border:1px solid var(--_danger);border-radius:var(--_radius);padding:12px 14px;margin:0 0 20px;color:var(--_danger)}
|
|
895
|
+
.realiiz-form__alert ul{margin:6px 0 0;padding-left:18px}
|
|
896
|
+
.realiiz-form__actions{display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap;margin-top:8px}
|
|
897
|
+
.realiiz-form__help{text-decoration:underline dotted;text-decoration-color:color-mix(in srgb,currentColor 45%,transparent);text-underline-offset:4px;cursor:help}
|
|
898
|
+
.realiiz-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
899
|
+
.realiiz-form__footer{max-width:920px}
|
|
900
|
+
.realiiz-form__bar{display:flex;align-items:center;gap:10px;margin-top:16px;padding:12px 0;border-top:1px solid var(--_border);background:#fff}
|
|
901
|
+
.realiiz-form__status{margin-right:auto;font-size:.85rem;color:var(--_muted)}
|
|
902
|
+
.realiiz-form__bar[data-dirty] .realiiz-form__status{color:var(--_fg)}
|
|
903
|
+
.realiiz-form__button{font:inherit;font-weight:600;border-radius:var(--_radius);padding:10px 16px;border:1px solid var(--_border);background:#fff;color:var(--_fg);cursor:pointer;min-height:40px}
|
|
904
|
+
.realiiz-form__button--primary{background:var(--_accent);color:var(--_accent-fg);border-color:var(--_accent)}
|
|
905
|
+
.realiiz-form__button:disabled{opacity:.6;cursor:default}
|
|
906
|
+
.realiiz-md{--_md-head:#fafbfd;--_md-hover:#f4f8fd;--_md-hover-line:#bcd0ea;--_md-on:#eef4fb;--_md-tip:#1f2933;position:relative}
|
|
907
|
+
.realiiz-md__mode{display:inline-flex;align-self:flex-start;width:max-content;gap:2px;padding:3px;background:color-mix(in srgb,var(--_border) 40%,transparent);border-radius:8px;margin-bottom:8px}
|
|
908
|
+
.realiiz-md__mode button{font:inherit;font-size:.8rem;padding:5px 13px;border:0;border-radius:6px;background:transparent;color:var(--_muted);cursor:pointer}
|
|
909
|
+
.realiiz-md__mode button[aria-selected="true"]{background:#fff;color:var(--_fg);box-shadow:0 1px 2px rgba(0,0,0,.12)}
|
|
910
|
+
.realiiz-md__panel{position:relative;z-index:0;display:flex;flex-direction:column;border:1px solid var(--_border);border-radius:10px;background:#fff;overflow:hidden}
|
|
911
|
+
.realiiz-md__panel:focus-within{border-color:var(--_md-hover-line)}
|
|
912
|
+
.realiiz-md__head{position:relative;z-index:5;display:flex;align-items:center;gap:2px;flex-wrap:wrap;padding:5px 7px;background:var(--_md-head);border-bottom:1px solid var(--_border);flex-shrink:0}
|
|
913
|
+
.realiiz-md__tools{display:flex;align-items:center;gap:3px;flex-wrap:wrap}
|
|
914
|
+
.realiiz-md__spacer{flex:1}
|
|
915
|
+
.realiiz-md__head button{position:relative;min-width:31px;height:28px;padding:0 9px;display:inline-flex;align-items:center;justify-content:center;background:#fff;border:1px solid var(--_border);border-radius:6px;font-family:inherit;font-size:.82rem;color:var(--_fg);cursor:pointer;line-height:1}
|
|
916
|
+
.realiiz-md__head button:hover:not(:disabled){border-color:var(--_md-hover-line);background:var(--_md-hover)}
|
|
917
|
+
.realiiz-md__head button:active{background:var(--_md-on)}
|
|
918
|
+
.realiiz-md__head button.on{background:var(--_md-on);border-color:var(--_accent);color:var(--_accent);font-weight:600}
|
|
919
|
+
.realiiz-md__ico{width:15px;height:15px;display:block}
|
|
920
|
+
.realiiz-md__dot{font-size:1.05rem;line-height:1}
|
|
921
|
+
.realiiz-md__sep{width:1px;height:17px;background:var(--_border);margin:0 5px}
|
|
922
|
+
.realiiz-md__insert{display:inline-flex;align-items:center;gap:3px;border-left:1px solid var(--_border);padding-left:8px;margin-left:6px}
|
|
923
|
+
.realiiz-md__insert button{background:#f6f8fa;color:var(--_muted);padding:0;width:31px}
|
|
924
|
+
.realiiz-md__insert button:hover{color:var(--_accent)}
|
|
925
|
+
.realiiz-md__lab{font-size:.74rem;color:var(--_muted);margin-right:3px}
|
|
926
|
+
.realiiz-md__head button[data-tip]::after{content:attr(data-tip);position:absolute;top:calc(100% + 6px);left:0;background:var(--_md-tip);color:#fff;font-size:.72rem;padding:4px 8px;border-radius:5px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .12s;z-index:20}
|
|
927
|
+
.realiiz-md__head button[data-tip]:hover::after,.realiiz-md__head button[data-tip]:focus-visible::after{opacity:1}
|
|
928
|
+
.realiiz-md__expand[data-tip]::after{left:auto;right:0}
|
|
929
|
+
.realiiz-md__tgwrap{position:relative;display:inline-flex}
|
|
930
|
+
.realiiz-md__tgrid{position:absolute;top:calc(100% + 6px);left:0;background:#fff;border:1px solid var(--_border);border-radius:9px;padding:9px;box-shadow:0 10px 28px rgba(0,0,0,.14);z-index:7;display:block}
|
|
931
|
+
.realiiz-md__tgcells{display:grid;grid-template-columns:repeat(6,17px);gap:3px}
|
|
932
|
+
.realiiz-md__tgcells i{width:17px;height:15px;border:1px solid var(--_border);border-radius:2px;background:#fff;cursor:pointer;display:block}
|
|
933
|
+
.realiiz-md__tgcells i.hot{background:#dbe9fb;border-color:var(--_accent)}
|
|
934
|
+
.realiiz-md__tglab{display:block;margin-top:8px;font-size:.74rem;color:var(--_muted);text-align:center;white-space:nowrap}
|
|
935
|
+
.realiiz-md .realiiz-md__ta{flex:1 1 auto;border:0;border-radius:0;resize:vertical;background:#fff;max-width:none;padding:16px 18px;line-height:1.65}
|
|
936
|
+
.realiiz-md .realiiz-md__ta:focus{outline:none;box-shadow:none}
|
|
937
|
+
.realiiz-md__preview{padding:20px 24px;background:#fff}
|
|
938
|
+
.realiiz-md__note{padding:4px 2px}
|
|
939
|
+
.realiiz-md--fs{position:fixed;inset:0;z-index:60;background:#fff;padding:14px 22px 22px;overflow:auto;display:flex;flex-direction:column}
|
|
940
|
+
.realiiz-md--fs .realiiz-md__panel{flex:1;min-height:0}
|
|
941
|
+
.realiiz-md--fs .realiiz-md__ta,.realiiz-md--fs .realiiz-md__preview{flex:1;min-height:0;overflow:auto;resize:none}
|
|
942
|
+
.realiiz-prose{max-width:68ch;line-height:1.65}
|
|
943
|
+
.realiiz-chips{display:flex;flex-wrap:wrap;gap:6px;align-items:center;min-height:40px;padding:5px 8px;background:#fff;border:1px solid var(--_border);border-radius:var(--_radius)}
|
|
944
|
+
.realiiz-chips:focus-within{border-color:var(--_accent);box-shadow:var(--_ring)}
|
|
945
|
+
.realiiz-chip{display:inline-flex;align-items:center;gap:4px;padding:2px 4px 2px 9px;border:1px solid var(--_border);border-radius:999px;font-size:.8rem}
|
|
946
|
+
.realiiz-chip button{font:inherit;line-height:1;width:18px;height:18px;border-radius:50%;border:0;background:transparent;color:var(--_muted);cursor:pointer}
|
|
947
|
+
.realiiz-chips__input{flex:1;min-width:120px;border:0;background:transparent;font:inherit;color:inherit;outline:none;padding:4px}
|
|
948
|
+
.realiiz-chips__count{margin-left:auto;color:var(--_muted);font-size:.75rem}
|
|
949
|
+
.realiiz-gauge{display:inline-flex;align-items:center;gap:10px;font-size:.75rem;color:var(--_muted)}
|
|
950
|
+
.realiiz-gauge__bar{position:relative;width:140px;height:6px;border-radius:3px;background:var(--_border);overflow:hidden}
|
|
951
|
+
.realiiz-gauge__zone{position:absolute;top:0;bottom:0;background:rgba(21,128,61,.28)}
|
|
952
|
+
.realiiz-gauge__fill{position:absolute;top:0;bottom:0;left:0;background:var(--_muted);border-radius:3px;transition:width .12s}
|
|
953
|
+
.realiiz-gauge__text b{font-weight:600;color:inherit}
|
|
954
|
+
.realiiz-gauge--ok{color:#15803d}.realiiz-gauge--ok .realiiz-gauge__fill{background:#15803d}
|
|
955
|
+
.realiiz-gauge--over{color:var(--_danger)}.realiiz-gauge--over .realiiz-gauge__fill{background:var(--_danger)}
|
|
956
|
+
`;
|
|
957
|
+
var UNINVITED_MESSAGE = "This email address isn't set up for this site yet. Ask the site owner to send you an invite.";
|
|
958
|
+
function isUninvitedError(message) {
|
|
959
|
+
return /signups? not allowed|not allowed|user not found|invalid login/i.test(message);
|
|
960
|
+
}
|
|
961
|
+
function MagicLinkForm({ url, anonKey, redirectTo, siteName, className }) {
|
|
962
|
+
const supabase = react.useMemo(() => ssr.createBrowserClient(url, anonKey), [url, anonKey]);
|
|
963
|
+
const [email, setEmail] = react.useState("");
|
|
964
|
+
const [state, setState] = react.useState({ kind: "idle" });
|
|
965
|
+
const submit = async (e) => {
|
|
966
|
+
e.preventDefault();
|
|
967
|
+
if (state.kind === "sending") return;
|
|
968
|
+
const address = email.trim().toLowerCase();
|
|
969
|
+
setState({ kind: "sending" });
|
|
970
|
+
const { error } = await supabase.auth.signInWithOtp({
|
|
971
|
+
email: address,
|
|
972
|
+
options: { emailRedirectTo: redirectTo, shouldCreateUser: false }
|
|
973
|
+
});
|
|
974
|
+
if (error) {
|
|
975
|
+
const uninvited = isUninvitedError(error.message);
|
|
976
|
+
setState({ kind: "error", message: uninvited ? UNINVITED_MESSAGE : error.message, uninvited });
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
setState({ kind: "sent", email: address });
|
|
980
|
+
};
|
|
981
|
+
const root = ["realiiz-form", "realiiz-signin", className].filter(Boolean).join(" ");
|
|
982
|
+
if (state.kind === "sent") {
|
|
983
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: root, "data-realiiz-signin": "sent", children: [
|
|
984
|
+
/* @__PURE__ */ jsxRuntime.jsx("style", { children: FORM_CSS }),
|
|
985
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "realiiz-signin__heading", children: "Check your email" }),
|
|
986
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
987
|
+
"We sent a sign-in link to ",
|
|
988
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: state.email }),
|
|
989
|
+
". It expires in an hour."
|
|
990
|
+
] }),
|
|
991
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "realiiz-form__button", onClick: () => setState({ kind: "idle" }), children: "Use a different address" })
|
|
992
|
+
] });
|
|
993
|
+
}
|
|
994
|
+
const sending = state.kind === "sending";
|
|
995
|
+
const fieldError = state.kind === "error" && !state.uninvited ? state.message : null;
|
|
996
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("form", { className: root, onSubmit: submit, noValidate: true, "data-realiiz-signin": "form", children: [
|
|
997
|
+
/* @__PURE__ */ jsxRuntime.jsx("style", { children: FORM_CSS }),
|
|
998
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h2", { className: "realiiz-signin__heading", children: [
|
|
999
|
+
"Sign in to ",
|
|
1000
|
+
siteName
|
|
1001
|
+
] }),
|
|
1002
|
+
state.kind === "error" && state.uninvited && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-form__alert", role: "alert", children: state.message }),
|
|
1003
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-form__field", children: [
|
|
1004
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "realiiz-form__label", htmlFor: "realiiz-signin-email", children: "Email address" }),
|
|
1005
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1006
|
+
"input",
|
|
1007
|
+
{
|
|
1008
|
+
id: "realiiz-signin-email",
|
|
1009
|
+
className: ["realiiz-form__control", fieldError && "realiiz-form__control--invalid"].filter(Boolean).join(" "),
|
|
1010
|
+
type: "email",
|
|
1011
|
+
name: "email",
|
|
1012
|
+
autoComplete: "email",
|
|
1013
|
+
required: true,
|
|
1014
|
+
value: email,
|
|
1015
|
+
onChange: (e) => setEmail(e.target.value),
|
|
1016
|
+
"aria-invalid": fieldError ? true : void 0,
|
|
1017
|
+
"aria-describedby": fieldError ? "realiiz-signin-email-error" : void 0
|
|
1018
|
+
}
|
|
1019
|
+
),
|
|
1020
|
+
fieldError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-form__error", id: "realiiz-signin-email-error", children: fieldError })
|
|
1021
|
+
] }),
|
|
1022
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-form__actions", children: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: "realiiz-form__button realiiz-form__button--primary", disabled: sending || email.trim() === "", children: sending ? "Sending\u2026" : "Send me a sign-in link" }) })
|
|
1023
|
+
] });
|
|
1024
|
+
}
|
|
1025
|
+
function SignOutButton({ actionPath = "/admin/auth/signout", className, label = "Sign out" }) {
|
|
1026
|
+
return /* @__PURE__ */ jsxRuntime.jsx("form", { method: "post", action: actionPath, className: ["realiiz-signout", className].filter(Boolean).join(" "), children: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: "realiiz-form__button", children: label }) });
|
|
1027
|
+
}
|
|
1028
|
+
function StudioNotConfigured() {
|
|
1029
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1030
|
+
/* @__PURE__ */ jsxRuntime.jsx(PageHeader, { eyebrow: "Setup", title: "Studio isn't configured on this deployment" }),
|
|
1031
|
+
/* @__PURE__ */ jsxRuntime.jsx(Card, { children: /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { margin: 0 }, children: [
|
|
1032
|
+
"Set the variables listed in ",
|
|
1033
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { children: ".env.example" }),
|
|
1034
|
+
" under \u201CStudio\u201D. Nothing else on the site is affected."
|
|
1035
|
+
] }) })
|
|
1036
|
+
] });
|
|
1037
|
+
}
|
|
1038
|
+
function StudioNoAccess({ actual }) {
|
|
1039
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1040
|
+
/* @__PURE__ */ jsxRuntime.jsx(PageHeader, { eyebrow: "Access", title: "No access to this site" }),
|
|
1041
|
+
/* @__PURE__ */ jsxRuntime.jsx(Card, { actions: /* @__PURE__ */ jsxRuntime.jsx(SignOutButton, {}), children: /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { margin: 0 }, children: [
|
|
1042
|
+
"You're signed in, but this account isn't set up to publish here",
|
|
1043
|
+
actual ? ` (current role: ${actual})` : "",
|
|
1044
|
+
". Ask the site owner for an invite."
|
|
1045
|
+
] }) })
|
|
1046
|
+
] });
|
|
1047
|
+
}
|
|
1048
|
+
function StudioTerm({ children, definition }) {
|
|
1049
|
+
const [open, setOpen] = react.useState(false);
|
|
1050
|
+
const id = react.useId();
|
|
1051
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "rz-term", onMouseEnter: () => setOpen(true), onMouseLeave: () => setOpen(false), children: [
|
|
1052
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1053
|
+
"span",
|
|
1054
|
+
{
|
|
1055
|
+
role: "button",
|
|
1056
|
+
tabIndex: 0,
|
|
1057
|
+
"aria-describedby": id,
|
|
1058
|
+
"aria-expanded": open,
|
|
1059
|
+
className: "rz-term__word",
|
|
1060
|
+
onClick: () => setOpen((o) => !o),
|
|
1061
|
+
onKeyDown: (e) => {
|
|
1062
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1063
|
+
e.preventDefault();
|
|
1064
|
+
setOpen((o) => !o);
|
|
1065
|
+
}
|
|
1066
|
+
if (e.key === "Escape") setOpen(false);
|
|
1067
|
+
},
|
|
1068
|
+
onFocus: () => setOpen(true),
|
|
1069
|
+
onBlur: () => setOpen(false),
|
|
1070
|
+
children
|
|
1071
|
+
}
|
|
1072
|
+
),
|
|
1073
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id, className: open ? "rz-term__tip rz-term__tip--open" : "rz-term__tip", role: "tooltip", children: definition })
|
|
1074
|
+
] });
|
|
1075
|
+
}
|
|
1076
|
+
var GLOSSARY = {
|
|
1077
|
+
pullRequest: "The safety step between you and the live website. When you save, your edit is set aside on GitHub (the service that stores your site) and the site is test-built with it first. Only when that passes can you press Publish and make it live. GitHub calls this a pull request \u2014 you never need to go there yourself.",
|
|
1078
|
+
repository: "Where your website actually lives: a private, backed-up folder on GitHub that keeps every version of every page. This list is read straight from it, so it's always current \u2014 even for items added outside Studio.",
|
|
1079
|
+
change: "Your edit while it's on its way to the live site. You save it, it gets checked, then you publish it. An item can have one change in progress at a time, and you can come back to it whenever you like.",
|
|
1080
|
+
checks: "A practice run. The whole website is rebuilt with your edit in it, to make sure nothing broke \u2014 a missing field, a picture that isn't there. If something's wrong you find out here, not on the live site.",
|
|
1081
|
+
publish: "Makes your checked edit live for everyone. Takes about two minutes to appear on the website."
|
|
1082
|
+
};
|
|
1083
|
+
function Jargon({ term, children }) {
|
|
1084
|
+
return /* @__PURE__ */ jsxRuntime.jsx(StudioTerm, { definition: GLOSSARY[term], children });
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// src/studio-ui/copy.ts
|
|
1088
|
+
function copyFor(entry) {
|
|
1089
|
+
const singular = entry.singular ?? entry.label;
|
|
1090
|
+
if (entry.id === "posts") {
|
|
1091
|
+
return {
|
|
1092
|
+
newItem: "New post",
|
|
1093
|
+
empty: "No posts yet",
|
|
1094
|
+
first: "Write the first one",
|
|
1095
|
+
removeLink: "Remove this post from the website",
|
|
1096
|
+
removeConfirm: "Take this post off the website? You\u2019ll get a change to review first \u2014 nothing disappears until you publish it.",
|
|
1097
|
+
removing: "This post is being removed, so it can\u2019t be edited right now. Cancel the removal from the change page if you\u2019ve changed your mind.",
|
|
1098
|
+
newSubtitle: "The web address fills itself from the title; change it if you like before you save."
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
const lower = singular.toLowerCase();
|
|
1102
|
+
return {
|
|
1103
|
+
newItem: `New ${lower}`,
|
|
1104
|
+
empty: `No ${entry.label.toLowerCase()} yet`,
|
|
1105
|
+
first: "Add the first one",
|
|
1106
|
+
removeLink: `Remove this ${lower} from the website`,
|
|
1107
|
+
removeConfirm: `Take this ${lower} off the website? You\u2019ll get a change to review first \u2014 nothing disappears until you publish it.`,
|
|
1108
|
+
removing: `This ${lower} is being removed, so it can\u2019t be edited right now. Cancel the removal from the change page if you\u2019ve changed your mind.`,
|
|
1109
|
+
newSubtitle: "The web address fills itself from the title; change it if you like before you save."
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// src/forms/internals.ts
|
|
1114
|
+
function asNode(schema, what = "schema") {
|
|
1115
|
+
const n = schema;
|
|
1116
|
+
if (!n || typeof n !== "object" || !n._zod?.def?.type) {
|
|
1117
|
+
throw new TypeError(`walkSchema: ${what} is not a zod 4 schema`);
|
|
1118
|
+
}
|
|
1119
|
+
return n;
|
|
1120
|
+
}
|
|
1121
|
+
function checkMessage(def) {
|
|
1122
|
+
const e = def.error;
|
|
1123
|
+
if (!e) return void 0;
|
|
1124
|
+
if (typeof e === "string") return e;
|
|
1125
|
+
const out = e({});
|
|
1126
|
+
if (typeof out === "string") return out;
|
|
1127
|
+
return out?.message;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// src/forms/checks.ts
|
|
1131
|
+
function readChecks(node) {
|
|
1132
|
+
const constraints = {};
|
|
1133
|
+
const messages = {};
|
|
1134
|
+
for (const c of node._zod.def.checks ?? []) {
|
|
1135
|
+
const d = c._zod.def;
|
|
1136
|
+
const msg = checkMessage(d);
|
|
1137
|
+
let key;
|
|
1138
|
+
switch (d.check) {
|
|
1139
|
+
case "min_length":
|
|
1140
|
+
constraints.min = d.minimum;
|
|
1141
|
+
key = "min_length";
|
|
1142
|
+
break;
|
|
1143
|
+
case "max_length":
|
|
1144
|
+
constraints.max = d.maximum;
|
|
1145
|
+
key = "max_length";
|
|
1146
|
+
break;
|
|
1147
|
+
case "length_equals":
|
|
1148
|
+
constraints.exact = d.length;
|
|
1149
|
+
key = "length_equals";
|
|
1150
|
+
break;
|
|
1151
|
+
case "greater_than":
|
|
1152
|
+
constraints.min = d.inclusive ? d.value : (d.value ?? 0) + 1;
|
|
1153
|
+
key = "greater_than";
|
|
1154
|
+
break;
|
|
1155
|
+
case "less_than":
|
|
1156
|
+
constraints.max = d.inclusive ? d.value : (d.value ?? 0) - 1;
|
|
1157
|
+
key = "less_than";
|
|
1158
|
+
break;
|
|
1159
|
+
case "string_format":
|
|
1160
|
+
if (d.format === "regex" && d.pattern) {
|
|
1161
|
+
constraints.pattern = d.pattern.source;
|
|
1162
|
+
constraints.format = "regex";
|
|
1163
|
+
key = "regex";
|
|
1164
|
+
} else if (d.format === "url" || d.format === "email") {
|
|
1165
|
+
constraints.format = d.format;
|
|
1166
|
+
key = d.format;
|
|
1167
|
+
} else {
|
|
1168
|
+
key = d.format;
|
|
1169
|
+
}
|
|
1170
|
+
break;
|
|
1171
|
+
case "number_format":
|
|
1172
|
+
if (d.format === "safeint" || d.format === "int32") {
|
|
1173
|
+
constraints.format = "int";
|
|
1174
|
+
key = "int";
|
|
1175
|
+
}
|
|
1176
|
+
break;
|
|
1177
|
+
case "custom":
|
|
1178
|
+
key = "custom";
|
|
1179
|
+
break;
|
|
1180
|
+
default:
|
|
1181
|
+
key = d.check;
|
|
1182
|
+
}
|
|
1183
|
+
if (key && msg) messages[key] = msg;
|
|
1184
|
+
}
|
|
1185
|
+
return { constraints, messages };
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// src/forms/unwrap.ts
|
|
1189
|
+
function unwrap(start) {
|
|
1190
|
+
let node = start;
|
|
1191
|
+
let required = true;
|
|
1192
|
+
for (let i = 0; i < 16; i++) {
|
|
1193
|
+
const def = node._zod.def;
|
|
1194
|
+
if (def.type === "optional" || def.type === "nullable" || def.type === "default") {
|
|
1195
|
+
required = false;
|
|
1196
|
+
if (!def.innerType) break;
|
|
1197
|
+
node = def.innerType;
|
|
1198
|
+
} else if (def.type === "pipe") {
|
|
1199
|
+
const next = def.out ?? def.in;
|
|
1200
|
+
if (!next) break;
|
|
1201
|
+
node = next;
|
|
1202
|
+
} else {
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return { node, type: node._zod.def.type, required };
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// src/forms/inputs.ts
|
|
1210
|
+
var TEXTAREA_MIN_MAX = 120;
|
|
1211
|
+
function inputFor(node) {
|
|
1212
|
+
const def = node._zod.def;
|
|
1213
|
+
const { constraints, messages } = readChecks(node);
|
|
1214
|
+
switch (def.type) {
|
|
1215
|
+
case "string": {
|
|
1216
|
+
if (constraints.format === "url") return { input: "url", constraints, messages };
|
|
1217
|
+
const long = (constraints.max ?? 0) > TEXTAREA_MIN_MAX;
|
|
1218
|
+
return { input: long ? "textarea" : "text", constraints, messages };
|
|
1219
|
+
}
|
|
1220
|
+
case "date":
|
|
1221
|
+
return { input: "date", constraints, messages };
|
|
1222
|
+
case "number":
|
|
1223
|
+
case "int":
|
|
1224
|
+
return { input: "number", constraints, messages };
|
|
1225
|
+
case "boolean":
|
|
1226
|
+
return { input: "boolean", constraints, messages };
|
|
1227
|
+
case "enum":
|
|
1228
|
+
case "literal": {
|
|
1229
|
+
const options = Object.values(def.entries ?? {}).map(String);
|
|
1230
|
+
return { input: "select", constraints, messages, options };
|
|
1231
|
+
}
|
|
1232
|
+
case "array": {
|
|
1233
|
+
if (def.element) {
|
|
1234
|
+
const item = unwrap(def.element);
|
|
1235
|
+
const inner = readChecks(item.node);
|
|
1236
|
+
constraints.item = inner;
|
|
1237
|
+
}
|
|
1238
|
+
return { input: "tags", constraints, messages };
|
|
1239
|
+
}
|
|
1240
|
+
default:
|
|
1241
|
+
return { input: "text", constraints, messages };
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/forms/labels.ts
|
|
1246
|
+
function readMeta(schema) {
|
|
1247
|
+
const s = schema;
|
|
1248
|
+
const raw = typeof s.meta === "function" ? s.meta() ?? {} : {};
|
|
1249
|
+
const pick = (k) => typeof raw[k] === "string" ? raw[k] : void 0;
|
|
1250
|
+
const rec = raw.recommended;
|
|
1251
|
+
const recommended = Array.isArray(rec) && rec.length === 2 && rec.every((n) => typeof n === "number") ? [rec[0], rec[1]] : void 0;
|
|
1252
|
+
const INPUTS = ["text", "textarea", "date", "number", "boolean", "select", "tags", "image", "url", "time"];
|
|
1253
|
+
const input = typeof raw.input === "string" && INPUTS.includes(raw.input) ? raw.input : void 0;
|
|
1254
|
+
return {
|
|
1255
|
+
label: pick("label"),
|
|
1256
|
+
placeholder: pick("placeholder"),
|
|
1257
|
+
description: pick("description") ?? (typeof s.description === "string" ? s.description : void 0),
|
|
1258
|
+
recommended,
|
|
1259
|
+
suffix: pick("suffix"),
|
|
1260
|
+
help: pick("help"),
|
|
1261
|
+
input
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function inputOverrideFor(name, schema, entry) {
|
|
1265
|
+
return entry?.inputs?.[name] ?? readMeta(schema).input;
|
|
1266
|
+
}
|
|
1267
|
+
function recommendedFor(name, schema, entry) {
|
|
1268
|
+
return entry?.recommended?.[name] ?? readMeta(schema).recommended;
|
|
1269
|
+
}
|
|
1270
|
+
function titlecase(name) {
|
|
1271
|
+
const words = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase().trim();
|
|
1272
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
1273
|
+
}
|
|
1274
|
+
function labelFor(name, schema, entry) {
|
|
1275
|
+
const fromRegistry = entry?.labels?.[name];
|
|
1276
|
+
if (fromRegistry) return fromRegistry;
|
|
1277
|
+
const meta = readMeta(schema);
|
|
1278
|
+
return meta.label ?? meta.description ?? titlecase(name);
|
|
1279
|
+
}
|
|
1280
|
+
function placeholderFor(name, schema, entry) {
|
|
1281
|
+
return entry?.placeholders?.[name] ?? readMeta(schema).placeholder;
|
|
1282
|
+
}
|
|
1283
|
+
function helpFor(name, schema, entry) {
|
|
1284
|
+
return entry?.help?.[name] ?? readMeta(schema).help;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// src/forms/walk.ts
|
|
1288
|
+
function walkSchema(schema, entry = {}) {
|
|
1289
|
+
const root = asNode(schema);
|
|
1290
|
+
const shape = shapeOf(root);
|
|
1291
|
+
const names = Object.keys(shape);
|
|
1292
|
+
const specs = names.map((name) => {
|
|
1293
|
+
const raw = shape[name];
|
|
1294
|
+
const { node, required } = unwrap(raw);
|
|
1295
|
+
const info = inputFor(node);
|
|
1296
|
+
const role = roleFor(name, entry);
|
|
1297
|
+
return {
|
|
1298
|
+
name,
|
|
1299
|
+
label: labelFor(name, raw, entry),
|
|
1300
|
+
input: role === "image" ? "image" : inputOverrideFor(name, raw, entry) ?? info.input,
|
|
1301
|
+
required,
|
|
1302
|
+
constraints: info.constraints,
|
|
1303
|
+
messages: info.messages,
|
|
1304
|
+
placeholder: placeholderFor(name, raw, entry),
|
|
1305
|
+
recommended: recommendedFor(name, raw, entry),
|
|
1306
|
+
suffix: readMeta(raw).suffix,
|
|
1307
|
+
help: helpFor(name, raw, entry),
|
|
1308
|
+
order: 0,
|
|
1309
|
+
role,
|
|
1310
|
+
options: entry.options?.[name] ?? info.options
|
|
1311
|
+
};
|
|
1312
|
+
});
|
|
1313
|
+
return applyOrder(specs, entry.order ?? []);
|
|
1314
|
+
}
|
|
1315
|
+
function shapeOf(root) {
|
|
1316
|
+
let node = root;
|
|
1317
|
+
for (let i = 0; i < 8 && !node._zod.def.shape; i++) {
|
|
1318
|
+
const def = node._zod.def;
|
|
1319
|
+
const next = def.innerType ?? def.in;
|
|
1320
|
+
if (!next) break;
|
|
1321
|
+
node = next;
|
|
1322
|
+
}
|
|
1323
|
+
const shape = node._zod.def.shape;
|
|
1324
|
+
if (!shape) throw new TypeError(`walkSchema: expected an object schema, got "${root._zod.def.type}"`);
|
|
1325
|
+
return shape;
|
|
1326
|
+
}
|
|
1327
|
+
function roleFor(name, entry) {
|
|
1328
|
+
if (entry.hidden?.includes(name)) return "hidden";
|
|
1329
|
+
if (entry.body === name) return "body";
|
|
1330
|
+
if (entry.images?.includes(name)) return "image";
|
|
1331
|
+
return "field";
|
|
1332
|
+
}
|
|
1333
|
+
function applyOrder(specs, order) {
|
|
1334
|
+
const rank = new Map(order.map((n, i) => [n, i]));
|
|
1335
|
+
const named = specs.filter((s) => rank.has(s.name)).sort((a, b) => rank.get(a.name) - rank.get(b.name));
|
|
1336
|
+
const rest = specs.filter((s) => !rank.has(s.name));
|
|
1337
|
+
return [...named, ...rest].map((s, i) => ({ ...s, order: i }));
|
|
1338
|
+
}
|
|
1339
|
+
function ChipsInput({ id, name, value, onChange, onBlur, placeholder, invalid, max }) {
|
|
1340
|
+
const [draft, setDraft] = react.useState("");
|
|
1341
|
+
const commit = () => {
|
|
1342
|
+
const parts = draft.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1343
|
+
if (parts.length) onChange([...value, ...parts.filter((p) => !value.includes(p))]);
|
|
1344
|
+
setDraft("");
|
|
1345
|
+
};
|
|
1346
|
+
const onKey = (e) => {
|
|
1347
|
+
if (e.key === "Enter" || e.key === ",") {
|
|
1348
|
+
e.preventDefault();
|
|
1349
|
+
commit();
|
|
1350
|
+
} else if (e.key === "Backspace" && draft === "" && value.length) {
|
|
1351
|
+
onChange(value.slice(0, -1));
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["realiiz-chips", invalid && "realiiz-form__control--invalid"].filter(Boolean).join(" "), "data-name": name, children: [
|
|
1355
|
+
value.map((tag) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-chip", children: [
|
|
1356
|
+
tag,
|
|
1357
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": `Remove ${tag}`, onClick: () => onChange(value.filter((t) => t !== tag)), children: "\xD7" })
|
|
1358
|
+
] }, tag)),
|
|
1359
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1360
|
+
"input",
|
|
1361
|
+
{
|
|
1362
|
+
id,
|
|
1363
|
+
type: "text",
|
|
1364
|
+
className: "realiiz-chips__input",
|
|
1365
|
+
value: draft,
|
|
1366
|
+
placeholder: value.length ? "" : placeholder,
|
|
1367
|
+
onChange: (e) => setDraft(e.target.value),
|
|
1368
|
+
onKeyDown: onKey,
|
|
1369
|
+
onBlur: () => {
|
|
1370
|
+
commit();
|
|
1371
|
+
onBlur();
|
|
1372
|
+
},
|
|
1373
|
+
"aria-invalid": invalid || void 0
|
|
1374
|
+
}
|
|
1375
|
+
),
|
|
1376
|
+
typeof max === "number" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-chips__count", children: [
|
|
1377
|
+
value.length,
|
|
1378
|
+
" of ",
|
|
1379
|
+
max
|
|
1380
|
+
] })
|
|
1381
|
+
] });
|
|
1382
|
+
}
|
|
1383
|
+
function labelFor2(value, labels) {
|
|
1384
|
+
if (labels?.[value]) return labels[value];
|
|
1385
|
+
return value ? titlecase(value) : "\u2014";
|
|
1386
|
+
}
|
|
1387
|
+
function SelectInput(p) {
|
|
1388
|
+
const [open, setOpen] = react.useState(false);
|
|
1389
|
+
const [active, setActive] = react.useState(0);
|
|
1390
|
+
const wrap = react.useRef(null);
|
|
1391
|
+
const list = react.useRef(null);
|
|
1392
|
+
const listId = react.useId();
|
|
1393
|
+
const choices = p.allowEmpty ? ["", ...p.options] : p.options;
|
|
1394
|
+
const latest = react.useRef({ choices, value: p.value });
|
|
1395
|
+
latest.current = { choices, value: p.value };
|
|
1396
|
+
react.useEffect(() => {
|
|
1397
|
+
if (!open) return;
|
|
1398
|
+
setActive(Math.max(0, latest.current.choices.indexOf(latest.current.value)));
|
|
1399
|
+
const onDoc = (e) => {
|
|
1400
|
+
if (!wrap.current?.contains(e.target)) close();
|
|
1401
|
+
};
|
|
1402
|
+
document.addEventListener("mousedown", onDoc);
|
|
1403
|
+
return () => document.removeEventListener("mousedown", onDoc);
|
|
1404
|
+
}, [open]);
|
|
1405
|
+
react.useEffect(() => {
|
|
1406
|
+
if (!open) return;
|
|
1407
|
+
list.current?.querySelectorAll("[role=option]")[active]?.scrollIntoView?.({ block: "nearest" });
|
|
1408
|
+
}, [open, active]);
|
|
1409
|
+
const close = () => {
|
|
1410
|
+
setOpen(false);
|
|
1411
|
+
p.onBlur?.();
|
|
1412
|
+
};
|
|
1413
|
+
const pick = (v) => {
|
|
1414
|
+
if (v !== p.value) p.onChange(v);
|
|
1415
|
+
close();
|
|
1416
|
+
};
|
|
1417
|
+
const onKey = (e) => {
|
|
1418
|
+
if (!open) {
|
|
1419
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Enter" || e.key === " ") {
|
|
1420
|
+
e.preventDefault();
|
|
1421
|
+
setOpen(true);
|
|
1422
|
+
}
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
switch (e.key) {
|
|
1426
|
+
case "ArrowDown":
|
|
1427
|
+
e.preventDefault();
|
|
1428
|
+
setActive((i) => Math.min(choices.length - 1, i + 1));
|
|
1429
|
+
break;
|
|
1430
|
+
case "ArrowUp":
|
|
1431
|
+
e.preventDefault();
|
|
1432
|
+
setActive((i) => Math.max(0, i - 1));
|
|
1433
|
+
break;
|
|
1434
|
+
case "Home":
|
|
1435
|
+
e.preventDefault();
|
|
1436
|
+
setActive(0);
|
|
1437
|
+
break;
|
|
1438
|
+
case "End":
|
|
1439
|
+
e.preventDefault();
|
|
1440
|
+
setActive(choices.length - 1);
|
|
1441
|
+
break;
|
|
1442
|
+
case "Enter":
|
|
1443
|
+
case " ":
|
|
1444
|
+
e.preventDefault();
|
|
1445
|
+
pick(choices[active] ?? "");
|
|
1446
|
+
break;
|
|
1447
|
+
case "Escape":
|
|
1448
|
+
e.preventDefault();
|
|
1449
|
+
close();
|
|
1450
|
+
break;
|
|
1451
|
+
case "Tab":
|
|
1452
|
+
close();
|
|
1453
|
+
break;
|
|
1454
|
+
default: {
|
|
1455
|
+
if (e.key.length === 1 && /\S/.test(e.key)) {
|
|
1456
|
+
const k = e.key.toLowerCase();
|
|
1457
|
+
const idx = choices.findIndex((c) => labelFor2(c, p.labels).toLowerCase().startsWith(k));
|
|
1458
|
+
if (idx >= 0) setActive(idx);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
const cls = ["realiiz-form__control", "realiiz-select__btn", p.invalid && "realiiz-form__control--invalid", open && "realiiz-select__btn--open"].filter(Boolean).join(" ");
|
|
1464
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: wrap, className: "realiiz-select", onKeyDown: onKey, children: [
|
|
1465
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name: p.name, value: p.value, readOnly: true }),
|
|
1466
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1467
|
+
"button",
|
|
1468
|
+
{
|
|
1469
|
+
type: "button",
|
|
1470
|
+
id: p.id,
|
|
1471
|
+
className: cls,
|
|
1472
|
+
role: "combobox",
|
|
1473
|
+
"aria-haspopup": "listbox",
|
|
1474
|
+
"aria-expanded": open,
|
|
1475
|
+
"aria-controls": listId,
|
|
1476
|
+
"aria-invalid": p.invalid || void 0,
|
|
1477
|
+
"aria-describedby": p.invalid ? `${p.id}-error` : void 0,
|
|
1478
|
+
disabled: p.disabled,
|
|
1479
|
+
onClick: () => open ? close() : setOpen(true),
|
|
1480
|
+
children: [
|
|
1481
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: p.value ? void 0 : "realiiz-select__placeholder", children: labelFor2(p.value, p.labels) }),
|
|
1482
|
+
/* @__PURE__ */ jsxRuntime.jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 9l6 6 6-6" }) })
|
|
1483
|
+
]
|
|
1484
|
+
}
|
|
1485
|
+
),
|
|
1486
|
+
open && /* @__PURE__ */ jsxRuntime.jsx("ul", { ref: list, id: listId, className: "realiiz-select__menu", role: "listbox", "aria-labelledby": p.id, tabIndex: -1, children: choices.map((c, i) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1487
|
+
"li",
|
|
1488
|
+
{
|
|
1489
|
+
role: "option",
|
|
1490
|
+
"aria-selected": c === p.value,
|
|
1491
|
+
"data-value": c,
|
|
1492
|
+
className: ["realiiz-select__item", i === active && "realiiz-select__item--active", c === p.value && "realiiz-select__item--selected"].filter(Boolean).join(" "),
|
|
1493
|
+
onMouseEnter: () => setActive(i),
|
|
1494
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
1495
|
+
onClick: () => pick(c),
|
|
1496
|
+
children: [
|
|
1497
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-select__check", "aria-hidden": "true", children: c === p.value ? "\u2713" : "" }),
|
|
1498
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: labelFor2(c, p.labels) })
|
|
1499
|
+
]
|
|
1500
|
+
},
|
|
1501
|
+
c || "__empty"
|
|
1502
|
+
)) })
|
|
1503
|
+
] });
|
|
1504
|
+
}
|
|
1505
|
+
function Control(p) {
|
|
1506
|
+
const cls = ["realiiz-form__control", p.invalid && "realiiz-form__control--invalid"].filter(Boolean).join(" ");
|
|
1507
|
+
const common = {
|
|
1508
|
+
id: p.id,
|
|
1509
|
+
name: p.spec.name,
|
|
1510
|
+
"aria-invalid": p.invalid || void 0,
|
|
1511
|
+
"aria-describedby": p.invalid ? `${p.id}-error` : void 0,
|
|
1512
|
+
onBlur: p.onBlur
|
|
1513
|
+
};
|
|
1514
|
+
const str = typeof p.value === "string" ? p.value : "";
|
|
1515
|
+
if (p.options === "loading") {
|
|
1516
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cls, "aria-busy": "true", style: { color: "var(--_muted)" }, children: "Loading options\u2026" });
|
|
1517
|
+
}
|
|
1518
|
+
if (p.options || p.spec.input === "select") {
|
|
1519
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1520
|
+
SelectInput,
|
|
1521
|
+
{
|
|
1522
|
+
id: p.id,
|
|
1523
|
+
name: p.spec.name,
|
|
1524
|
+
value: str,
|
|
1525
|
+
options: p.options ?? [],
|
|
1526
|
+
allowEmpty: !p.spec.required,
|
|
1527
|
+
invalid: p.invalid,
|
|
1528
|
+
onChange: p.onChange,
|
|
1529
|
+
onBlur: p.onBlur
|
|
1530
|
+
}
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
switch (p.spec.input) {
|
|
1534
|
+
case "textarea":
|
|
1535
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1536
|
+
"textarea",
|
|
1537
|
+
{
|
|
1538
|
+
...common,
|
|
1539
|
+
className: `${cls} realiiz-form__control--textarea`,
|
|
1540
|
+
value: str,
|
|
1541
|
+
placeholder: p.spec.placeholder,
|
|
1542
|
+
onChange: (e) => p.onChange(e.target.value)
|
|
1543
|
+
}
|
|
1544
|
+
);
|
|
1545
|
+
case "boolean":
|
|
1546
|
+
return /* @__PURE__ */ jsxRuntime.jsx("input", { ...common, type: "checkbox", checked: p.value === true, onChange: (e) => p.onChange(e.target.checked) });
|
|
1547
|
+
case "image":
|
|
1548
|
+
return /* @__PURE__ */ jsxRuntime.jsx("input", { ...common, type: "text", className: cls, value: str, placeholder: p.spec.placeholder ?? "/images/picture.webp", onChange: (e) => p.onChange(e.target.value) });
|
|
1549
|
+
case "date":
|
|
1550
|
+
return /* @__PURE__ */ jsxRuntime.jsx("input", { ...common, type: "date", className: cls, value: str, onChange: (e) => p.onChange(e.target.value) });
|
|
1551
|
+
case "number":
|
|
1552
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1553
|
+
"input",
|
|
1554
|
+
{
|
|
1555
|
+
...common,
|
|
1556
|
+
type: "number",
|
|
1557
|
+
inputMode: "numeric",
|
|
1558
|
+
className: cls,
|
|
1559
|
+
value: str,
|
|
1560
|
+
min: p.spec.constraints.min,
|
|
1561
|
+
max: p.spec.constraints.max,
|
|
1562
|
+
step: p.spec.constraints.format === "int" ? 1 : void 0,
|
|
1563
|
+
placeholder: p.spec.placeholder,
|
|
1564
|
+
onChange: (e) => p.onChange(e.target.value)
|
|
1565
|
+
}
|
|
1566
|
+
);
|
|
1567
|
+
case "time":
|
|
1568
|
+
return /* @__PURE__ */ jsxRuntime.jsx("input", { ...common, type: "time", className: cls, value: str, onChange: (e) => p.onChange(e.target.value) });
|
|
1569
|
+
case "url":
|
|
1570
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1571
|
+
"input",
|
|
1572
|
+
{
|
|
1573
|
+
...common,
|
|
1574
|
+
type: "url",
|
|
1575
|
+
className: cls,
|
|
1576
|
+
value: str,
|
|
1577
|
+
placeholder: p.spec.placeholder ?? "https://",
|
|
1578
|
+
onChange: (e) => p.onChange(e.target.value)
|
|
1579
|
+
}
|
|
1580
|
+
);
|
|
1581
|
+
default:
|
|
1582
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1583
|
+
"input",
|
|
1584
|
+
{
|
|
1585
|
+
...common,
|
|
1586
|
+
type: "text",
|
|
1587
|
+
className: cls,
|
|
1588
|
+
value: str,
|
|
1589
|
+
placeholder: p.spec.placeholder,
|
|
1590
|
+
maxLength: p.spec.constraints.max,
|
|
1591
|
+
onChange: (e) => p.onChange(e.target.value)
|
|
1592
|
+
}
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
function useImages() {
|
|
1597
|
+
const [images, setImages] = react.useState({});
|
|
1598
|
+
const latest = react.useRef(images);
|
|
1599
|
+
latest.current = images;
|
|
1600
|
+
const urls = react.useRef(/* @__PURE__ */ new Set());
|
|
1601
|
+
react.useEffect(() => {
|
|
1602
|
+
const set = urls.current;
|
|
1603
|
+
return () => {
|
|
1604
|
+
for (const u of set) URL.revokeObjectURL(u);
|
|
1605
|
+
};
|
|
1606
|
+
}, []);
|
|
1607
|
+
const keep = react.useCallback((field, r, previous) => {
|
|
1608
|
+
const previewUrl = r.previewUrl ?? URL.createObjectURL(r.blob);
|
|
1609
|
+
if (!r.previewUrl) urls.current.add(previewUrl);
|
|
1610
|
+
const picked = { path: r.path, blob: r.blob, previewUrl, width: r.width, height: r.height, previous };
|
|
1611
|
+
setImages((m) => ({ ...m, [field]: picked }));
|
|
1612
|
+
return picked;
|
|
1613
|
+
}, []);
|
|
1614
|
+
const remove = react.useCallback((field) => {
|
|
1615
|
+
const previous = latest.current[field]?.previous;
|
|
1616
|
+
setImages((m) => {
|
|
1617
|
+
const rest = { ...m };
|
|
1618
|
+
delete rest[field];
|
|
1619
|
+
return rest;
|
|
1620
|
+
});
|
|
1621
|
+
return previous;
|
|
1622
|
+
}, []);
|
|
1623
|
+
const toSubmission = react.useCallback(() => {
|
|
1624
|
+
return Object.fromEntries(Object.entries(images).map(([k, v]) => [k, { path: v.path, blob: v.blob }]));
|
|
1625
|
+
}, [images]);
|
|
1626
|
+
return { images, keep, remove, toSubmission };
|
|
1627
|
+
}
|
|
1628
|
+
function formatBytes(n) {
|
|
1629
|
+
if (n < 1024 * 1024) return `${Math.max(1, Math.round(n / 1024))} KB`;
|
|
1630
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// src/forms-ui/md/upload.ts
|
|
1634
|
+
var IMAGE_ACCEPT = "image/jpeg,image/png,image/webp,image/gif";
|
|
1635
|
+
function pickFile(accept = IMAGE_ACCEPT) {
|
|
1636
|
+
return new Promise((resolve) => {
|
|
1637
|
+
const input = document.createElement("input");
|
|
1638
|
+
input.type = "file";
|
|
1639
|
+
input.accept = accept;
|
|
1640
|
+
input.style.display = "none";
|
|
1641
|
+
document.body.appendChild(input);
|
|
1642
|
+
const done = () => {
|
|
1643
|
+
resolve(input.files?.[0]);
|
|
1644
|
+
input.remove();
|
|
1645
|
+
};
|
|
1646
|
+
input.addEventListener("change", done, { once: true });
|
|
1647
|
+
window.addEventListener("focus", () => setTimeout(() => {
|
|
1648
|
+
if (document.body.contains(input)) done();
|
|
1649
|
+
}, 400), { once: true });
|
|
1650
|
+
input.click();
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
function imageFromDrop(e) {
|
|
1654
|
+
return Array.from(e.dataTransfer?.files ?? []).find((f) => f.type.startsWith("image/"));
|
|
1655
|
+
}
|
|
1656
|
+
function imageFromPaste(e) {
|
|
1657
|
+
const item = Array.from(e.clipboardData?.items ?? []).find((i) => i.kind === "file" && i.type.startsWith("image/"));
|
|
1658
|
+
return item?.getAsFile() ?? void 0;
|
|
1659
|
+
}
|
|
1660
|
+
function messageOf(err) {
|
|
1661
|
+
if (err && typeof err === "object" && err.name === "MediaError") return err.message;
|
|
1662
|
+
return "That picture couldn\u2019t be prepared. Try a different file.";
|
|
1663
|
+
}
|
|
1664
|
+
var IMAGE_HINT = "JPG, PNG or WebP, up to 10MB. We shrink it for the web.";
|
|
1665
|
+
function ImageInput(p) {
|
|
1666
|
+
const [busy, setBusy] = react.useState(false);
|
|
1667
|
+
const [error, setError] = react.useState(null);
|
|
1668
|
+
const [over, setOver] = react.useState(false);
|
|
1669
|
+
const zone = react.useRef(null);
|
|
1670
|
+
const take = async (file) => {
|
|
1671
|
+
if (!file || busy || p.disabled) return;
|
|
1672
|
+
setBusy(true);
|
|
1673
|
+
setError(null);
|
|
1674
|
+
try {
|
|
1675
|
+
await p.onPick(file);
|
|
1676
|
+
} catch (err) {
|
|
1677
|
+
setError(messageOf(err));
|
|
1678
|
+
} finally {
|
|
1679
|
+
setBusy(false);
|
|
1680
|
+
}
|
|
1681
|
+
};
|
|
1682
|
+
const choose = async () => take(await pickFile());
|
|
1683
|
+
const onDrop = (e) => {
|
|
1684
|
+
e.preventDefault();
|
|
1685
|
+
setOver(false);
|
|
1686
|
+
void take(imageFromDrop(e));
|
|
1687
|
+
};
|
|
1688
|
+
const onPaste = (e) => {
|
|
1689
|
+
const f = imageFromPaste(e);
|
|
1690
|
+
if (f) {
|
|
1691
|
+
e.preventDefault();
|
|
1692
|
+
void take(f);
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
const src = p.picked?.previewUrl ?? (p.value || null);
|
|
1696
|
+
const detail = p.picked ? [p.picked.width && p.picked.height ? `${p.picked.width} \xD7 ${p.picked.height}` : null, formatBytes(p.picked.blob.size)].filter(Boolean).join(" \xB7 ") : p.value;
|
|
1697
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1698
|
+
"div",
|
|
1699
|
+
{
|
|
1700
|
+
ref: zone,
|
|
1701
|
+
className: ["realiiz-image", over && "realiiz-image--over", p.invalid && "realiiz-image--invalid", busy && "realiiz-image--busy"].filter(Boolean).join(" "),
|
|
1702
|
+
"data-field": p.name,
|
|
1703
|
+
onDragOver: (e) => {
|
|
1704
|
+
e.preventDefault();
|
|
1705
|
+
if (!over) setOver(true);
|
|
1706
|
+
},
|
|
1707
|
+
onDragLeave: () => setOver(false),
|
|
1708
|
+
onDrop,
|
|
1709
|
+
onPaste,
|
|
1710
|
+
onBlur: p.onBlur,
|
|
1711
|
+
tabIndex: -1,
|
|
1712
|
+
children: [
|
|
1713
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { id: p.id, name: p.name, type: "text", value: p.value, readOnly: true, className: "realiiz-sr-only", "aria-busy": busy || void 0 }),
|
|
1714
|
+
src ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-image__has", children: [
|
|
1715
|
+
/* @__PURE__ */ jsxRuntime.jsx("img", { src, alt: "", className: "realiiz-image__thumb", "data-testid": "realiiz-image-thumb" }),
|
|
1716
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-image__meta", children: [
|
|
1717
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-image__detail", children: busy ? "Shrinking\u2026" : detail }),
|
|
1718
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-image__actions", children: [
|
|
1719
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "realiiz-form__button", onClick: choose, disabled: busy || p.disabled, children: "Replace" }),
|
|
1720
|
+
p.picked && p.onRemove && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "realiiz-image__remove", onClick: p.onRemove, disabled: busy, children: "Remove" })
|
|
1721
|
+
] })
|
|
1722
|
+
] })
|
|
1723
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-image__empty", children: [
|
|
1724
|
+
/* @__PURE__ */ jsxRuntime.jsx(PictureIcon, {}),
|
|
1725
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "realiiz-form__button", onClick: choose, disabled: busy || p.disabled, children: busy ? "Shrinking\u2026" : "Choose a picture" }),
|
|
1726
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-image__or", children: "or drag one here" })
|
|
1727
|
+
] }),
|
|
1728
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-image__hint", children: IMAGE_HINT }),
|
|
1729
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-form__error", role: "alert", children: error })
|
|
1730
|
+
]
|
|
1731
|
+
}
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
function PictureIcon() {
|
|
1735
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: "realiiz-image__icon", children: [
|
|
1736
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
|
|
1737
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "10", r: "1.6" }),
|
|
1738
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 16l-5-5-8 8" })
|
|
1739
|
+
] });
|
|
1740
|
+
}
|
|
1741
|
+
var defaultLabel = (label, help) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-form__help", title: help, children: label });
|
|
1742
|
+
function LengthGauge({ length: raw, range, suffix }) {
|
|
1743
|
+
const [min, max] = range;
|
|
1744
|
+
const length = raw === 0 ? 0 : raw + (suffix?.length ?? 0);
|
|
1745
|
+
const state = length === 0 ? "empty" : length < min ? "short" : length <= max ? "ok" : "over";
|
|
1746
|
+
const scale = Math.round(max * 1.3);
|
|
1747
|
+
const pct = (n) => `${Math.min(100, n / scale * 100)}%`;
|
|
1748
|
+
const verdict = state === "ok" ? "good length" : state === "over" ? `over ${max} \u2014 may be cut off` : `aim for ${min}\u2013${max}`;
|
|
1749
|
+
const incl = suffix && length > 0 ? ` incl. \u201C${suffix.trim()}\u201D` : "";
|
|
1750
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: `realiiz-gauge realiiz-gauge--${state}`, title: `Recommended ${min}\u2013${max} characters${suffix ? ` including \u201C${suffix.trim()}\u201D` : ""}`, children: [
|
|
1751
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-gauge__bar", "aria-hidden": "true", children: [
|
|
1752
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-gauge__zone", style: { left: pct(min), width: `calc(${pct(max)} - ${pct(min)})` } }),
|
|
1753
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-gauge__fill", style: { width: pct(length) } })
|
|
1754
|
+
] }),
|
|
1755
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-gauge__text", children: [
|
|
1756
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { children: length }),
|
|
1757
|
+
" characters",
|
|
1758
|
+
incl,
|
|
1759
|
+
" \xB7 ",
|
|
1760
|
+
verdict
|
|
1761
|
+
] })
|
|
1762
|
+
] });
|
|
1763
|
+
}
|
|
1764
|
+
function Field(p) {
|
|
1765
|
+
const id = `${p.formId}-${p.spec.name}`;
|
|
1766
|
+
const invalid = p.issues.length > 0;
|
|
1767
|
+
const max = p.spec.constraints.max;
|
|
1768
|
+
const isText = p.spec.input === "text" || p.spec.input === "textarea";
|
|
1769
|
+
const len = typeof p.value === "string" ? p.value.length : 0;
|
|
1770
|
+
const isCheck = p.spec.input === "boolean";
|
|
1771
|
+
const isTags = p.spec.input === "tags";
|
|
1772
|
+
const labelText = p.spec.help ? (p.renderLabel ?? defaultLabel)(p.spec.label, p.spec.help) : p.spec.label;
|
|
1773
|
+
const control = p.spec.input === "image" && p.onPickImage ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1774
|
+
ImageInput,
|
|
1775
|
+
{
|
|
1776
|
+
id,
|
|
1777
|
+
name: p.spec.name,
|
|
1778
|
+
value: typeof p.value === "string" ? p.value : "",
|
|
1779
|
+
picked: p.picked,
|
|
1780
|
+
invalid,
|
|
1781
|
+
onPick: p.onPickImage,
|
|
1782
|
+
onRemove: p.onRemoveImage,
|
|
1783
|
+
onBlur: p.onBlur
|
|
1784
|
+
}
|
|
1785
|
+
) : isTags ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1786
|
+
ChipsInput,
|
|
1787
|
+
{
|
|
1788
|
+
id,
|
|
1789
|
+
name: p.spec.name,
|
|
1790
|
+
value: Array.isArray(p.value) ? p.value : [],
|
|
1791
|
+
onChange: (v) => p.onChange(v),
|
|
1792
|
+
onBlur: p.onBlur,
|
|
1793
|
+
placeholder: p.spec.placeholder,
|
|
1794
|
+
invalid,
|
|
1795
|
+
max: p.spec.constraints.exact ?? p.spec.constraints.max
|
|
1796
|
+
}
|
|
1797
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(Control, { id, spec: p.spec, value: p.value, invalid, options: p.options, onChange: p.onChange, onBlur: p.onBlur });
|
|
1798
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `realiiz-form__field realiiz-form__field--${p.spec.input}`, "data-field": p.spec.name, children: [
|
|
1799
|
+
isCheck ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "realiiz-form__check", htmlFor: id, children: [
|
|
1800
|
+
control,
|
|
1801
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
1802
|
+
labelText,
|
|
1803
|
+
p.spec.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-form__required", "aria-hidden": "true", children: "*" })
|
|
1804
|
+
] })
|
|
1805
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1806
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "realiiz-form__label", htmlFor: id, children: [
|
|
1807
|
+
labelText,
|
|
1808
|
+
p.spec.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-form__required", "aria-hidden": "true", children: "*" })
|
|
1809
|
+
] }),
|
|
1810
|
+
control
|
|
1811
|
+
] }),
|
|
1812
|
+
isText && (p.spec.recommended || typeof max === "number") && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-form__helper", children: [
|
|
1813
|
+
p.spec.recommended ? /* @__PURE__ */ jsxRuntime.jsx(LengthGauge, { length: len, range: p.spec.recommended, suffix: p.spec.suffix }) : /* @__PURE__ */ jsxRuntime.jsx("span", {}),
|
|
1814
|
+
typeof max === "number" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-form__count", children: [
|
|
1815
|
+
len,
|
|
1816
|
+
" / ",
|
|
1817
|
+
max
|
|
1818
|
+
] })
|
|
1819
|
+
] }),
|
|
1820
|
+
invalid && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-form__error", id: `${id}-error`, children: p.issues.map((i, n) => /* @__PURE__ */ jsxRuntime.jsx("div", { children: i.message }, n)) })
|
|
1821
|
+
] });
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// src/forms-ui/md/table.ts
|
|
1825
|
+
var ROW = /^\s*\|.*\|\s*$/;
|
|
1826
|
+
var SEP = /^\s*\|[\s:|-]+\|\s*$/;
|
|
1827
|
+
function tableCells(line) {
|
|
1828
|
+
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split(/(?<!\\)\|/).map((c) => c.trim());
|
|
1829
|
+
}
|
|
1830
|
+
function tableAt(v, pos) {
|
|
1831
|
+
const lines = v.split("\n");
|
|
1832
|
+
let i = 0, off = 0;
|
|
1833
|
+
while (i < lines.length && pos > off + lines[i].length) {
|
|
1834
|
+
off += lines[i].length + 1;
|
|
1835
|
+
i++;
|
|
1836
|
+
}
|
|
1837
|
+
if (i >= lines.length || !ROW.test(lines[i])) return null;
|
|
1838
|
+
let s = i, e = i;
|
|
1839
|
+
while (s > 0 && ROW.test(lines[s - 1])) s--;
|
|
1840
|
+
while (e < lines.length - 1 && ROW.test(lines[e + 1])) e++;
|
|
1841
|
+
if (e - s < 1 || !SEP.test(lines[s + 1])) return null;
|
|
1842
|
+
const start = lines.slice(0, s).reduce((n, l) => n + l.length + 1, 0);
|
|
1843
|
+
const end = start + lines.slice(s, e + 1).join("\n").length;
|
|
1844
|
+
const align = tableCells(lines[s + 1]).map((c) => c.startsWith(":") && c.endsWith(":") ? "c" : c.endsWith(":") ? "r" : c.startsWith(":") ? "L" : "l");
|
|
1845
|
+
const rows = lines.slice(s, e + 1).filter((_, n) => n !== 1).map(tableCells);
|
|
1846
|
+
const col = Math.max(0, (lines[i].slice(0, pos - off).match(/(?<!\\)\|/g) || []).length - 1);
|
|
1847
|
+
const row = i - s <= 1 ? 0 : i - s - 1;
|
|
1848
|
+
return { start, end, rows, align, row, col };
|
|
1849
|
+
}
|
|
1850
|
+
function renderTable(rows, align) {
|
|
1851
|
+
const cols = Math.max(...rows.map((r) => r.length));
|
|
1852
|
+
const w = [];
|
|
1853
|
+
for (let c = 0; c < cols; c++) w[c] = Math.max(3, ...rows.map((r) => (r[c] || "").length));
|
|
1854
|
+
const pad = (t, c) => (t || "").padEnd(w[c]);
|
|
1855
|
+
const line = (r) => "| " + Array.from({ length: cols }, (_, c) => pad(r[c], c)).join(" | ") + " |";
|
|
1856
|
+
const sep = "|" + Array.from({ length: cols }, (_, c) => align[c] === "c" ? ":" + "-".repeat(w[c]) + ":" : align[c] === "r" ? " " + "-".repeat(w[c] - 1) + ": " : align[c] === "L" ? ":" + "-".repeat(w[c] - 1) + " " : " " + "-".repeat(w[c]) + " ").join("|") + "|";
|
|
1857
|
+
return [line(rows[0]), sep, ...rows.slice(1).map(line)].join("\n");
|
|
1858
|
+
}
|
|
1859
|
+
function newTable(rows, cols) {
|
|
1860
|
+
const head = Array.from({ length: cols }, (_, i) => `Column ${i + 1}`);
|
|
1861
|
+
const grid = [head, ...Array.from({ length: rows }, () => Array(cols).fill(""))];
|
|
1862
|
+
return { text: renderTable(grid, Array(cols).fill("l")), firstHeader: head[0] };
|
|
1863
|
+
}
|
|
1864
|
+
function applyTableOp(v, pos, op) {
|
|
1865
|
+
const t = tableAt(v, pos);
|
|
1866
|
+
if (!t) return null;
|
|
1867
|
+
const rows = t.rows.map((r) => r.slice());
|
|
1868
|
+
const cols = Math.max(...rows.map((r) => r.length));
|
|
1869
|
+
const align = t.align.slice();
|
|
1870
|
+
if (op === "rowAfter") {
|
|
1871
|
+
rows.splice(Math.max(1, t.row + 1), 0, Array(cols).fill(""));
|
|
1872
|
+
} else if (op === "rowDel") {
|
|
1873
|
+
if (t.row === 0 || rows.length <= 2) return { error: "A table needs its header row and at least one row below it." };
|
|
1874
|
+
rows.splice(t.row, 1);
|
|
1875
|
+
} else if (op === "colAfter") {
|
|
1876
|
+
rows.forEach((r) => {
|
|
1877
|
+
while (r.length < cols) r.push("");
|
|
1878
|
+
r.splice(t.col + 1, 0, "");
|
|
1879
|
+
});
|
|
1880
|
+
align.splice(t.col + 1, 0, "l");
|
|
1881
|
+
} else if (op === "colDel") {
|
|
1882
|
+
if (cols <= 1) return { error: "That's the last column \u2014 delete the table instead." };
|
|
1883
|
+
rows.forEach((r) => r.splice(t.col, 1));
|
|
1884
|
+
align.splice(t.col, 1);
|
|
1885
|
+
}
|
|
1886
|
+
const out = renderTable(rows, align);
|
|
1887
|
+
return { text: v.slice(0, t.start) + out + v.slice(t.end), a: t.start, b: t.start + out.length };
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// src/forms-ui/md/edits.ts
|
|
1891
|
+
var toggleWrap = (mark, placeholder = "text") => (s, a, b) => {
|
|
1892
|
+
const sel = s.slice(a, b);
|
|
1893
|
+
const n = mark.length;
|
|
1894
|
+
if (sel.length >= 2 * n && sel.startsWith(mark) && sel.endsWith(mark)) {
|
|
1895
|
+
const inner = sel.slice(n, sel.length - n);
|
|
1896
|
+
return { text: s.slice(0, a) + inner + s.slice(b), a, b: a + inner.length };
|
|
1897
|
+
}
|
|
1898
|
+
if (s.slice(a - n, a) === mark && s.slice(b, b + n) === mark) {
|
|
1899
|
+
return { text: s.slice(0, a - n) + sel + s.slice(b + n), a: a - n, b: a - n + sel.length };
|
|
1900
|
+
}
|
|
1901
|
+
const body = sel || placeholder;
|
|
1902
|
+
return { text: s.slice(0, a) + mark + body + mark + s.slice(b), a: a + n, b: a + n + body.length };
|
|
1903
|
+
};
|
|
1904
|
+
var PREFIX_RE = /^(#{1,6}\s+|[-*+]\s+|\d+[.)]\s+|>\s?)/;
|
|
1905
|
+
var lineSpan = (s, a, b) => {
|
|
1906
|
+
const start = s.lastIndexOf("\n", a - 1) + 1;
|
|
1907
|
+
const endIdx = s.indexOf("\n", Math.max(b, a));
|
|
1908
|
+
return { start, end: endIdx === -1 ? s.length : endIdx };
|
|
1909
|
+
};
|
|
1910
|
+
var toggleLines = (prefix, matches) => (s, a, b) => {
|
|
1911
|
+
const { start, end } = lineSpan(s, a, b);
|
|
1912
|
+
const lines = s.slice(start, end).split("\n");
|
|
1913
|
+
const allOn = lines.every((l) => l.trim() === "" || matches(l));
|
|
1914
|
+
const out = lines.map((l, i) => {
|
|
1915
|
+
const bare = l.replace(PREFIX_RE, "");
|
|
1916
|
+
return allOn ? bare : (typeof prefix === "string" ? prefix : prefix(i)) + bare;
|
|
1917
|
+
});
|
|
1918
|
+
const replaced = out.join("\n");
|
|
1919
|
+
return { text: s.slice(0, start) + replaced + s.slice(end), a: start, b: start + replaced.length };
|
|
1920
|
+
};
|
|
1921
|
+
var LINK_RE = /\[([^\]\n]*)\]\(([^)\n]*)\)/g;
|
|
1922
|
+
function linkAt(s, a, b) {
|
|
1923
|
+
for (const m of s.matchAll(LINK_RE)) {
|
|
1924
|
+
if (s[m.index - 1] === "!") continue;
|
|
1925
|
+
const from = m.index, to = from + m[0].length;
|
|
1926
|
+
if (a >= from && b <= to) return { from, to, label: m[1] };
|
|
1927
|
+
if (from > b) break;
|
|
1928
|
+
}
|
|
1929
|
+
return null;
|
|
1930
|
+
}
|
|
1931
|
+
var toggleLink = (s, a, b) => {
|
|
1932
|
+
const hit = linkAt(s, a, b);
|
|
1933
|
+
if (hit) return { text: s.slice(0, hit.from) + hit.label + s.slice(hit.to), a: hit.from, b: hit.from + hit.label.length };
|
|
1934
|
+
const label = s.slice(a, b) || "link text";
|
|
1935
|
+
const urlStart = a + label.length + 3;
|
|
1936
|
+
return { text: s.slice(0, a) + `[${label}](https://)` + s.slice(b), a: urlStart, b: urlStart + 8 };
|
|
1937
|
+
};
|
|
1938
|
+
var insertImage = (s, a, b) => {
|
|
1939
|
+
const alt = s.slice(a, b) || "description";
|
|
1940
|
+
const urlStart = a + alt.length + 4;
|
|
1941
|
+
return { text: s.slice(0, a) + `` + s.slice(b), a: urlStart, b: urlStart + 8 };
|
|
1942
|
+
};
|
|
1943
|
+
var insertDot = (s, a, b) => {
|
|
1944
|
+
const t = " \xB7 ";
|
|
1945
|
+
return { text: s.slice(0, a) + t + s.slice(b), a: a + t.length, b: a + t.length };
|
|
1946
|
+
};
|
|
1947
|
+
var blockBefore = (s, a) => a === 0 || s.slice(0, a).endsWith("\n\n") ? "" : s.slice(0, a).endsWith("\n") ? "\n" : "\n\n";
|
|
1948
|
+
var insertRule = (s, a, b) => {
|
|
1949
|
+
const before = blockBefore(s, a);
|
|
1950
|
+
const t = before + "---\n\n";
|
|
1951
|
+
return { text: s.slice(0, a) + t + s.slice(b), a: a + t.length, b: a + t.length };
|
|
1952
|
+
};
|
|
1953
|
+
var insertTable = (rows, cols) => (s, a, b) => {
|
|
1954
|
+
const { text, firstHeader } = newTable(rows, cols);
|
|
1955
|
+
const before = blockBefore(s, a);
|
|
1956
|
+
const t = before + text + "\n";
|
|
1957
|
+
const at = a + before.length + text.indexOf(firstHeader);
|
|
1958
|
+
return { text: s.slice(0, a) + t + s.slice(b), a: at, b: at + firstHeader.length };
|
|
1959
|
+
};
|
|
1960
|
+
var toggleTerm = (component = "Term", attr = "definition") => (s, a, b) => {
|
|
1961
|
+
const re = new RegExp(`<${component}\\s+${attr}="([^"]*)"\\s*>([\\s\\S]*?)</${component}>`, "g");
|
|
1962
|
+
for (const m of s.matchAll(re)) {
|
|
1963
|
+
const from = m.index, to = from + m[0].length;
|
|
1964
|
+
if (a >= from && b <= to) return { text: s.slice(0, from) + m[2] + s.slice(to), a: from, b: from + m[2].length };
|
|
1965
|
+
if (from > b) break;
|
|
1966
|
+
}
|
|
1967
|
+
const word = s.slice(a, b) || "term";
|
|
1968
|
+
const open = `<${component} ${attr}="`;
|
|
1969
|
+
const text = s.slice(0, a) + open + `">${word}</${component}>` + s.slice(b);
|
|
1970
|
+
return { text, a: a + open.length, b: a + open.length };
|
|
1971
|
+
};
|
|
1972
|
+
var TOOLS = [
|
|
1973
|
+
{ id: "bold", title: "Bold \u2014 \u2318B", apply: toggleWrap("**") },
|
|
1974
|
+
{ id: "italic", title: "Italic \u2014 \u2318I", apply: toggleWrap("_") },
|
|
1975
|
+
{ id: "h2", title: "Section heading", apply: toggleLines("## ", (l) => /^##\s/.test(l)) },
|
|
1976
|
+
{ id: "h3", title: "Sub-heading", apply: toggleLines("### ", (l) => /^###\s/.test(l)) },
|
|
1977
|
+
{ id: "ul", title: "Bulleted list", apply: toggleLines("- ", (l) => /^[-*+]\s/.test(l)) },
|
|
1978
|
+
{ id: "ol", title: "Numbered list", apply: toggleLines((i) => `${i + 1}. `, (l) => /^\d+[.)]\s/.test(l)) },
|
|
1979
|
+
{ id: "quote", title: "Quote", apply: toggleLines("> ", (l) => /^>/.test(l)) },
|
|
1980
|
+
{ id: "link", title: "Link", apply: toggleLink },
|
|
1981
|
+
{ id: "code", title: "Inline code", apply: toggleWrap("`", "code") },
|
|
1982
|
+
{ id: "image", title: "Image", apply: insertImage },
|
|
1983
|
+
{ id: "dot", title: "Separator between details ( \xB7 )", apply: insertDot },
|
|
1984
|
+
{ id: "rule", title: "Divider line across the page", apply: insertRule }
|
|
1985
|
+
];
|
|
1986
|
+
var toolById = (id) => TOOLS.find((t) => t.id === id);
|
|
1987
|
+
function activeTools(s, a, b) {
|
|
1988
|
+
const on = /* @__PURE__ */ new Set();
|
|
1989
|
+
const sel = s.slice(a, b);
|
|
1990
|
+
const wrapped = (m) => s.slice(a - m.length, a) === m && s.slice(b, b + m.length) === m || sel.length >= 2 * m.length && sel.startsWith(m) && sel.endsWith(m);
|
|
1991
|
+
if (wrapped("**") || spanAt(s, a, b, /\*\*[^*\n]+\*\*/g, 2)) on.add("bold");
|
|
1992
|
+
if (wrapped("_") || spanAt(s, a, b, /(?<![\w_])_[^_\n]+_(?![\w_])/g, 1)) on.add("italic");
|
|
1993
|
+
if (wrapped("`") || spanAt(s, a, b, /`[^`\n]+`/g, 1)) on.add("code");
|
|
1994
|
+
if (linkAt(s, a, b)) on.add("link");
|
|
1995
|
+
if (spanAt(s, a, b, /<([A-Z][\w]*)\s[^>]*>[\s\S]*?<\/\1>/g, 0)) on.add("term");
|
|
1996
|
+
const { start, end } = lineSpan(s, a, a);
|
|
1997
|
+
const line = s.slice(start, end);
|
|
1998
|
+
if (/^##\s/.test(line)) on.add("h2");
|
|
1999
|
+
else if (/^###\s/.test(line)) on.add("h3");
|
|
2000
|
+
else if (/^[-*+]\s/.test(line)) on.add("ul");
|
|
2001
|
+
else if (/^\d+[.)]\s/.test(line)) on.add("ol");
|
|
2002
|
+
else if (/^>/.test(line)) on.add("quote");
|
|
2003
|
+
return on;
|
|
2004
|
+
}
|
|
2005
|
+
function spanAt(s, a, b, re, pad) {
|
|
2006
|
+
for (const m of s.matchAll(re)) {
|
|
2007
|
+
const from = m.index, to = from + m[0].length;
|
|
2008
|
+
if (a >= from + pad && b <= to - pad) return true;
|
|
2009
|
+
if (from > b) break;
|
|
2010
|
+
}
|
|
2011
|
+
return false;
|
|
2012
|
+
}
|
|
2013
|
+
var Svg = ({ children, fill = "none" }) => /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "realiiz-md__ico", viewBox: "0 0 24 24", fill, stroke: "currentColor", strokeWidth: "1.9", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children });
|
|
2014
|
+
var MD_ICONS = {
|
|
2015
|
+
bold: /* @__PURE__ */ jsxRuntime.jsx("b", { children: "B" }),
|
|
2016
|
+
italic: /* @__PURE__ */ jsxRuntime.jsx("i", { children: "I" }),
|
|
2017
|
+
h2: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
|
|
2018
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 5v14" }),
|
|
2019
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 5v14" }),
|
|
2020
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 12h8" }),
|
|
2021
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M17.5 19v-6.5l-1.6 1.2" })
|
|
2022
|
+
] }),
|
|
2023
|
+
h3: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
|
|
2024
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 5v14" }),
|
|
2025
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 5v14" }),
|
|
2026
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 12h7" }),
|
|
2027
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 12.5h3.5l-2 2.2a1.9 1.9 0 1 1-1.5 3.2" })
|
|
2028
|
+
] }),
|
|
2029
|
+
ul: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
|
|
2030
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "6.5", x2: "20", y2: "6.5" }),
|
|
2031
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "12", x2: "20", y2: "12" }),
|
|
2032
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "17.5", x2: "20", y2: "17.5" }),
|
|
2033
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.6", cy: "6.5", r: "1.3", fill: "currentColor", stroke: "none" }),
|
|
2034
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.6", cy: "12", r: "1.3", fill: "currentColor", stroke: "none" }),
|
|
2035
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.6", cy: "17.5", r: "1.3", fill: "currentColor", stroke: "none" })
|
|
2036
|
+
] }),
|
|
2037
|
+
ol: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
|
|
2038
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "10", y1: "6.5", x2: "20", y2: "6.5" }),
|
|
2039
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "10", y1: "12", x2: "20", y2: "12" }),
|
|
2040
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "10", y1: "17.5", x2: "20", y2: "17.5" }),
|
|
2041
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 5.6 5.2 5v3.4" }),
|
|
2042
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.6 11.2h1.8L3.6 13.7h1.9" }),
|
|
2043
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.7 16.3h1.7v1.2H4.2h1.2v1.2H3.7" })
|
|
2044
|
+
] }),
|
|
2045
|
+
quote: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
|
|
2046
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 7c-2.4 0-4 1.7-4 4 0 1.9 1.3 3.2 3 3.2.4 0 .8-.1 1-.2-.3 1.4-1.5 2.6-3 3l.7 1.5c2.8-.8 4.8-3.3 4.8-6.6C11.5 8.7 10.5 7 9 7z", fill: "currentColor", stroke: "none" }),
|
|
2047
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18 7c-2.4 0-4 1.7-4 4 0 1.9 1.3 3.2 3 3.2.4 0 .8-.1 1-.2-.3 1.4-1.5 2.6-3 3l.7 1.5c2.8-.8 4.8-3.3 4.8-6.6C20.5 8.7 19.5 7 18 7z", fill: "currentColor", stroke: "none" })
|
|
2048
|
+
] }),
|
|
2049
|
+
link: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
|
|
2050
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10 13a5 5 0 0 0 7.5.5l2.5-2.5a5 5 0 0 0-7-7l-1.5 1.4" }),
|
|
2051
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 11a5 5 0 0 0-7.5-.5L4 13a5 5 0 0 0 7 7l1.4-1.4" })
|
|
2052
|
+
] }),
|
|
2053
|
+
code: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
|
|
2054
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m8 7-5 5 5 5" }),
|
|
2055
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m16 7 5 5-5 5" }),
|
|
2056
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m14 4-4 16" })
|
|
2057
|
+
] }),
|
|
2058
|
+
// A tooltip: a tall speech bubble with an "i" — what the reader will actually see on the page.
|
|
2059
|
+
term: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
|
|
2060
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 2.5h14a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2h-4.5L12 21.5 9.5 17.5H5a2 2 0 0 1-2-2v-11a2 2 0 0 1 2-2z" }),
|
|
2061
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 9v4.5" }),
|
|
2062
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "6.2", r: ".9", fill: "currentColor", stroke: "none" })
|
|
2063
|
+
] }),
|
|
2064
|
+
image: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
|
|
2065
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
|
|
2066
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "8.5", cy: "8.5", r: "1.6" }),
|
|
2067
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m21 15-5-5L5 21" })
|
|
2068
|
+
] }),
|
|
2069
|
+
dot: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-md__dot", children: "\xB7" }),
|
|
2070
|
+
rule: /* @__PURE__ */ jsxRuntime.jsx(Svg, { children: /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12" }) }),
|
|
2071
|
+
table: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
|
|
2072
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }),
|
|
2073
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "9.5", x2: "21", y2: "9.5" }),
|
|
2074
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "15", x2: "21", y2: "15" }),
|
|
2075
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "9.5", x2: "12", y2: "20" })
|
|
2076
|
+
] }),
|
|
2077
|
+
expand: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
|
|
2078
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 3H3v6" }),
|
|
2079
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15 21h6v-6" }),
|
|
2080
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3l7 7" }),
|
|
2081
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 21l-7-7" })
|
|
2082
|
+
] }),
|
|
2083
|
+
contract: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
|
|
2084
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 10h6V4" }),
|
|
2085
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M20 14h-6v6" }),
|
|
2086
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3l7 7" }),
|
|
2087
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 21l-7-7" })
|
|
2088
|
+
] })
|
|
2089
|
+
};
|
|
2090
|
+
var MAX = 6;
|
|
2091
|
+
function TableGrid({ onPick }) {
|
|
2092
|
+
const [open, setOpen] = react.useState(false);
|
|
2093
|
+
const [hot, setHot] = react.useState([0, 0]);
|
|
2094
|
+
const wrap = react.useRef(null);
|
|
2095
|
+
react.useEffect(() => {
|
|
2096
|
+
if (!open) return;
|
|
2097
|
+
const away = (e) => {
|
|
2098
|
+
if (!wrap.current?.contains(e.target)) setOpen(false);
|
|
2099
|
+
};
|
|
2100
|
+
const esc = (e) => {
|
|
2101
|
+
if (e.key === "Escape") setOpen(false);
|
|
2102
|
+
};
|
|
2103
|
+
document.addEventListener("mousedown", away);
|
|
2104
|
+
document.addEventListener("keydown", esc);
|
|
2105
|
+
return () => {
|
|
2106
|
+
document.removeEventListener("mousedown", away);
|
|
2107
|
+
document.removeEventListener("keydown", esc);
|
|
2108
|
+
};
|
|
2109
|
+
}, [open]);
|
|
2110
|
+
const [r, c] = hot;
|
|
2111
|
+
const label = r && c ? `${c} column${c === 1 ? "" : "s"} \xD7 ${r} row${r === 1 ? "" : "s"}` : "Pick a size";
|
|
2112
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: ["realiiz-md__tgwrap", open && "is-open"].filter(Boolean).join(" "), ref: wrap, children: [
|
|
2113
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-tool": "table", "data-tip": "New table \u2014 pick a size", "aria-label": "Insert a table", "aria-expanded": open, onClick: () => setOpen((o) => !o), children: MD_ICONS.table }),
|
|
2114
|
+
open && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-md__tgrid", role: "dialog", "aria-label": "Table size", onMouseLeave: () => setHot([0, 0]), children: [
|
|
2115
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-md__tgcells", children: Array.from({ length: MAX * MAX }, (_, n) => {
|
|
2116
|
+
const rr = Math.floor(n / MAX) + 1, cc = n % MAX + 1;
|
|
2117
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2118
|
+
"i",
|
|
2119
|
+
{
|
|
2120
|
+
className: rr <= r && cc <= c ? "hot" : void 0,
|
|
2121
|
+
onMouseEnter: () => setHot([rr, cc]),
|
|
2122
|
+
onClick: () => {
|
|
2123
|
+
setOpen(false);
|
|
2124
|
+
onPick(rr, cc);
|
|
2125
|
+
},
|
|
2126
|
+
"aria-label": `${cc} \xD7 ${rr}`,
|
|
2127
|
+
role: "button"
|
|
2128
|
+
},
|
|
2129
|
+
n
|
|
2130
|
+
);
|
|
2131
|
+
}) }),
|
|
2132
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-md__tglab", children: label })
|
|
2133
|
+
] })
|
|
2134
|
+
] });
|
|
2135
|
+
}
|
|
2136
|
+
var FORMAT = ["bold", "italic"];
|
|
2137
|
+
var BLOCK = ["h2", "h3", "ul", "ol", "quote"];
|
|
2138
|
+
var INLINE = ["link", "code"];
|
|
2139
|
+
var INSERT = ["image", "dot", "rule"];
|
|
2140
|
+
var TABLE_OPS = [
|
|
2141
|
+
{ op: "rowAfter", label: "+ Row", tip: "Insert a row below this one" },
|
|
2142
|
+
{ op: "rowDel", label: "\u2212 Row", tip: "Delete this row" },
|
|
2143
|
+
{ op: "colAfter", label: "+ Col", tip: "Insert a column to the right" },
|
|
2144
|
+
{ op: "colDel", label: "\u2212 Col", tip: "Delete this column" }
|
|
2145
|
+
];
|
|
2146
|
+
function MarkdownEditor({ id, name = "body", value, onChange, placeholder, renderPreview, previewClassName, minHeight = 320, termComponent, onUploadImage, previewSrc }) {
|
|
2147
|
+
const [mode, setMode] = react.useState(() => renderPreview && value.trim() ? "preview" : "edit");
|
|
2148
|
+
const [preview, setPreview] = react.useState(null);
|
|
2149
|
+
const [loading, setLoading] = react.useState(false);
|
|
2150
|
+
const [expanded, setExpanded] = react.useState(false);
|
|
2151
|
+
const [on, setOn] = react.useState(() => /* @__PURE__ */ new Set());
|
|
2152
|
+
const [inTable, setInTable] = react.useState(false);
|
|
2153
|
+
const [note, setNote] = react.useState(null);
|
|
2154
|
+
const [uploading, setUploading] = react.useState(false);
|
|
2155
|
+
const ta = react.useRef(null);
|
|
2156
|
+
const sync = react.useCallback(() => {
|
|
2157
|
+
const el = ta.current;
|
|
2158
|
+
if (!el) return;
|
|
2159
|
+
setOn(activeTools(el.value, el.selectionStart, el.selectionEnd));
|
|
2160
|
+
setInTable(Boolean(tableAt(el.value, el.selectionStart)));
|
|
2161
|
+
}, []);
|
|
2162
|
+
const commit = (edit) => {
|
|
2163
|
+
const el = ta.current;
|
|
2164
|
+
if (!el) return;
|
|
2165
|
+
const { text, a, b } = edit;
|
|
2166
|
+
const from = commonPrefix(el.value, text);
|
|
2167
|
+
const oldTail = el.value.length - from, newTail = text.length - from;
|
|
2168
|
+
const suffix = commonSuffix(el.value.slice(from), text.slice(from));
|
|
2169
|
+
const oldEnd = from + oldTail - suffix, newEnd = from + newTail - suffix;
|
|
2170
|
+
let ok = false;
|
|
2171
|
+
el.focus();
|
|
2172
|
+
el.setSelectionRange(from, oldEnd);
|
|
2173
|
+
try {
|
|
2174
|
+
ok = document.execCommand("insertText", false, text.slice(from, newEnd));
|
|
2175
|
+
} catch {
|
|
2176
|
+
ok = false;
|
|
2177
|
+
}
|
|
2178
|
+
if (!ok || el.value !== text) onChange(text);
|
|
2179
|
+
requestAnimationFrame(() => {
|
|
2180
|
+
el.setSelectionRange(a, b);
|
|
2181
|
+
sync();
|
|
2182
|
+
});
|
|
2183
|
+
};
|
|
2184
|
+
const run = (apply) => {
|
|
2185
|
+
const el = ta.current;
|
|
2186
|
+
if (!el) return;
|
|
2187
|
+
commit(apply(value, el.selectionStart, el.selectionEnd));
|
|
2188
|
+
};
|
|
2189
|
+
const runTable = (op) => {
|
|
2190
|
+
const el = ta.current;
|
|
2191
|
+
if (!el) return;
|
|
2192
|
+
const r = applyTableOp(value, el.selectionStart, op);
|
|
2193
|
+
if (!r) return;
|
|
2194
|
+
if ("error" in r) {
|
|
2195
|
+
setNote(r.error);
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
setNote(null);
|
|
2199
|
+
commit(r);
|
|
2200
|
+
};
|
|
2201
|
+
const upload = async (file) => {
|
|
2202
|
+
const el = ta.current;
|
|
2203
|
+
if (!file || !onUploadImage || !el || uploading) return;
|
|
2204
|
+
setUploading(true);
|
|
2205
|
+
setNote(null);
|
|
2206
|
+
const a = el.selectionStart, b = el.selectionEnd;
|
|
2207
|
+
try {
|
|
2208
|
+
const { path } = await onUploadImage(file);
|
|
2209
|
+
const alt = value.slice(a, b) || "description";
|
|
2210
|
+
const text = value.slice(0, a) + `` + value.slice(b);
|
|
2211
|
+
commit({ text, a: a + 2, b: a + 2 + alt.length });
|
|
2212
|
+
} catch (err) {
|
|
2213
|
+
setNote(messageOf(err));
|
|
2214
|
+
} finally {
|
|
2215
|
+
setUploading(false);
|
|
2216
|
+
}
|
|
2217
|
+
};
|
|
2218
|
+
const onDrop = (e) => {
|
|
2219
|
+
const f = imageFromDrop(e);
|
|
2220
|
+
if (f && onUploadImage) {
|
|
2221
|
+
e.preventDefault();
|
|
2222
|
+
void upload(f);
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
const onPaste = (e) => {
|
|
2226
|
+
const f = imageFromPaste(e);
|
|
2227
|
+
if (f && onUploadImage) {
|
|
2228
|
+
e.preventDefault();
|
|
2229
|
+
void upload(f);
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2232
|
+
const onKey = (e) => {
|
|
2233
|
+
if (e.key === "Escape" && expanded) {
|
|
2234
|
+
e.preventDefault();
|
|
2235
|
+
setExpanded(false);
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
if (!(e.metaKey || e.ctrlKey)) return;
|
|
2239
|
+
if (e.key === "b") {
|
|
2240
|
+
e.preventDefault();
|
|
2241
|
+
run(toolById("bold").apply);
|
|
2242
|
+
}
|
|
2243
|
+
if (e.key === "i") {
|
|
2244
|
+
e.preventDefault();
|
|
2245
|
+
run(toolById("italic").apply);
|
|
2246
|
+
}
|
|
2247
|
+
if (e.key === "k") {
|
|
2248
|
+
e.preventDefault();
|
|
2249
|
+
run(toolById("link").apply);
|
|
2250
|
+
}
|
|
2251
|
+
};
|
|
2252
|
+
react.useEffect(() => {
|
|
2253
|
+
if (!expanded) return;
|
|
2254
|
+
const prev = document.body.style.overflow;
|
|
2255
|
+
document.body.style.overflow = "hidden";
|
|
2256
|
+
const esc = (e) => {
|
|
2257
|
+
if (e.key === "Escape") setExpanded(false);
|
|
2258
|
+
};
|
|
2259
|
+
document.addEventListener("keydown", esc);
|
|
2260
|
+
return () => {
|
|
2261
|
+
document.body.style.overflow = prev;
|
|
2262
|
+
document.removeEventListener("keydown", esc);
|
|
2263
|
+
};
|
|
2264
|
+
}, [expanded]);
|
|
2265
|
+
react.useEffect(() => {
|
|
2266
|
+
if (mode !== "preview" || !renderPreview) return;
|
|
2267
|
+
let live = true;
|
|
2268
|
+
setLoading(true);
|
|
2269
|
+
renderPreview(value).then((r) => {
|
|
2270
|
+
if (live) setPreview("html" in r ? { html: swapPreviewSrc(r.html, previewSrc) } : r);
|
|
2271
|
+
}).catch((err) => {
|
|
2272
|
+
if (live) setPreview({ error: err instanceof Error ? err.message : String(err) });
|
|
2273
|
+
}).finally(() => {
|
|
2274
|
+
if (live) setLoading(false);
|
|
2275
|
+
});
|
|
2276
|
+
return () => {
|
|
2277
|
+
live = false;
|
|
2278
|
+
};
|
|
2279
|
+
}, [mode, value, renderPreview, previewSrc]);
|
|
2280
|
+
const termTool = termComponent ? { id: "term", title: `Tooltip \u2014 wraps the selection in <${termComponent}>`, apply: toggleTerm(termComponent) } : null;
|
|
2281
|
+
const btn = (toolId) => {
|
|
2282
|
+
const t = toolId === "term" && termTool ? termTool : toolById(toolId);
|
|
2283
|
+
const uploads = t.id === "image" && Boolean(onUploadImage);
|
|
2284
|
+
const title = uploads ? uploading ? "Shrinking\u2026" : "Picture \u2014 choose a file, or drop/paste one into the text" : t.title;
|
|
2285
|
+
return /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-tool": t.id, "data-tip": title, "aria-label": title, "aria-pressed": on.has(t.id) || void 0, "aria-busy": uploads && uploading ? true : void 0, className: on.has(t.id) ? "on" : void 0, disabled: uploads && uploading, onMouseDown: (e) => e.preventDefault(), onClick: () => uploads ? pickFile().then(upload) : run(t.apply), children: MD_ICONS[t.id] }, t.id);
|
|
2286
|
+
};
|
|
2287
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["realiiz-md", expanded && "realiiz-md--fs"].filter(Boolean).join(" "), children: [
|
|
2288
|
+
renderPreview && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-md__mode", role: "tablist", "aria-label": "Editor mode", children: [
|
|
2289
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", role: "tab", "aria-selected": mode === "edit", onClick: () => setMode("edit"), children: "Edit Markdown" }),
|
|
2290
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", role: "tab", "aria-selected": mode === "preview", onClick: () => setMode("preview"), children: "Preview" })
|
|
2291
|
+
] }),
|
|
2292
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-md__panel", children: [
|
|
2293
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-md__head", children: [
|
|
2294
|
+
mode === "edit" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-md__tools", role: "toolbar", "aria-label": "Formatting", children: [
|
|
2295
|
+
FORMAT.map(btn),
|
|
2296
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-md__sep" }),
|
|
2297
|
+
BLOCK.map(btn),
|
|
2298
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-md__sep" }),
|
|
2299
|
+
INLINE.map(btn),
|
|
2300
|
+
termTool && btn("term"),
|
|
2301
|
+
inTable && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2302
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-md__sep" }),
|
|
2303
|
+
TABLE_OPS.map((t) => /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-tbl-op": t.op, "data-tip": t.tip, "aria-label": t.tip, onMouseDown: (e) => e.preventDefault(), onClick: () => runTable(t.op), children: t.label }, t.op))
|
|
2304
|
+
] }),
|
|
2305
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-md__insert", children: [
|
|
2306
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-md__lab", children: "Insert" }),
|
|
2307
|
+
INSERT.map(btn),
|
|
2308
|
+
/* @__PURE__ */ jsxRuntime.jsx(TableGrid, { onPick: (r, c) => run(insertTable(r, c)) })
|
|
2309
|
+
] })
|
|
2310
|
+
] }),
|
|
2311
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-md__spacer" }),
|
|
2312
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2313
|
+
"button",
|
|
2314
|
+
{
|
|
2315
|
+
type: "button",
|
|
2316
|
+
className: "realiiz-md__expand",
|
|
2317
|
+
"data-tip": expanded ? "Back to the normal layout \u2014 or press Esc" : "Fill the window \u2014 Esc to come back",
|
|
2318
|
+
"aria-label": expanded ? "Exit" : "Expand",
|
|
2319
|
+
onClick: () => setExpanded((x) => !x),
|
|
2320
|
+
children: expanded ? MD_ICONS.contract : MD_ICONS.expand
|
|
2321
|
+
}
|
|
2322
|
+
)
|
|
2323
|
+
] }),
|
|
2324
|
+
mode === "edit" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2325
|
+
"textarea",
|
|
2326
|
+
{
|
|
2327
|
+
ref: ta,
|
|
2328
|
+
id,
|
|
2329
|
+
name,
|
|
2330
|
+
className: "realiiz-form__control realiiz-form__control--body realiiz-md__ta",
|
|
2331
|
+
style: expanded ? void 0 : { minHeight },
|
|
2332
|
+
value,
|
|
2333
|
+
placeholder,
|
|
2334
|
+
onChange: (e) => {
|
|
2335
|
+
onChange(e.target.value);
|
|
2336
|
+
sync();
|
|
2337
|
+
},
|
|
2338
|
+
onSelect: sync,
|
|
2339
|
+
onKeyUp: sync,
|
|
2340
|
+
onClick: sync,
|
|
2341
|
+
onKeyDown: onKey,
|
|
2342
|
+
onDrop,
|
|
2343
|
+
onPaste,
|
|
2344
|
+
spellCheck: true
|
|
2345
|
+
}
|
|
2346
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-md__preview", style: expanded ? void 0 : { minHeight }, "aria-live": "polite", "aria-busy": loading, children: [
|
|
2347
|
+
loading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-md__loading", children: "Rendering\u2026" }),
|
|
2348
|
+
!loading && preview && "error" in preview && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-form__alert", role: "alert", children: [
|
|
2349
|
+
"Couldn't render: ",
|
|
2350
|
+
preview.error
|
|
2351
|
+
] }),
|
|
2352
|
+
!loading && preview && "html" in preview && /* @__PURE__ */ jsxRuntime.jsx("div", { className: ["realiiz-prose", previewClassName].filter(Boolean).join(" "), dangerouslySetInnerHTML: { __html: preview.html } })
|
|
2353
|
+
] })
|
|
2354
|
+
] }),
|
|
2355
|
+
(note || uploading) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-form__helper realiiz-md__note", role: "status", children: uploading ? "Shrinking\u2026" : note })
|
|
2356
|
+
] });
|
|
2357
|
+
}
|
|
2358
|
+
function swapPreviewSrc(html, map) {
|
|
2359
|
+
if (!map || Object.keys(map).length === 0) return html;
|
|
2360
|
+
return html.replace(/(<img\b[^>]*\bsrc=")([^"]+)(")/g, (m, pre, src, post) => map[src] ? `${pre}${map[src]}${post}` : m);
|
|
2361
|
+
}
|
|
2362
|
+
function commonPrefix(a, b) {
|
|
2363
|
+
const n = Math.min(a.length, b.length);
|
|
2364
|
+
let i = 0;
|
|
2365
|
+
while (i < n && a[i] === b[i]) i++;
|
|
2366
|
+
return i;
|
|
2367
|
+
}
|
|
2368
|
+
function commonSuffix(a, b) {
|
|
2369
|
+
const n = Math.min(a.length, b.length);
|
|
2370
|
+
let i = 0;
|
|
2371
|
+
while (i < n && a[a.length - 1 - i] === b[b.length - 1 - i]) i++;
|
|
2372
|
+
return i;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
// src/forms-ui/issues.ts
|
|
2376
|
+
function mapIssues(issues, specs) {
|
|
2377
|
+
const byName = new Map(specs.map((s) => [s.name, s]));
|
|
2378
|
+
return issues.map((issue) => {
|
|
2379
|
+
const field = typeof issue.path[0] === "string" ? issue.path[0] : null;
|
|
2380
|
+
const spec = field ? byName.get(field) : void 0;
|
|
2381
|
+
const key = keyFor(issue, spec);
|
|
2382
|
+
const message = spec?.messages[key] ?? (issue.code === "unrecognized_keys" ? unknownKeysMessage(issue) : void 0) ?? issue.message;
|
|
2383
|
+
return { field, key, message };
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
function keyFor(issue, spec) {
|
|
2387
|
+
const numeric = issue.origin === "number" || issue.origin === "int";
|
|
2388
|
+
switch (issue.code) {
|
|
2389
|
+
case "too_small":
|
|
2390
|
+
if (spec?.constraints.exact !== void 0) return "length_equals";
|
|
2391
|
+
return numeric ? "greater_than" : "min_length";
|
|
2392
|
+
case "too_big":
|
|
2393
|
+
if (spec?.constraints.exact !== void 0) return "length_equals";
|
|
2394
|
+
return numeric ? "less_than" : "max_length";
|
|
2395
|
+
case "invalid_format":
|
|
2396
|
+
return issue.format ?? "format";
|
|
2397
|
+
case "invalid_type":
|
|
2398
|
+
return issue.format === "safeint" || issue.format === "int32" ? "int" : "invalid_type";
|
|
2399
|
+
default:
|
|
2400
|
+
return issue.code;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
function unknownKeysMessage(issue) {
|
|
2404
|
+
const keys = issue.keys ?? [];
|
|
2405
|
+
return `This file has ${keys.length === 1 ? "a field" : "fields"} the site does not recognise: ${keys.join(", ")}. It was probably edited by hand \u2014 remove or rename ${keys.length === 1 ? "it" : "them"} to continue.`;
|
|
2406
|
+
}
|
|
2407
|
+
function groupIssues(list) {
|
|
2408
|
+
const byField = /* @__PURE__ */ new Map();
|
|
2409
|
+
const formLevel = [];
|
|
2410
|
+
for (const i of list) {
|
|
2411
|
+
if (i.field === null) formLevel.push(i);
|
|
2412
|
+
else byField.set(i.field, [...byField.get(i.field) ?? [], i]);
|
|
2413
|
+
}
|
|
2414
|
+
return { byField, formLevel };
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
// src/forms-ui/values.ts
|
|
2418
|
+
function toControl(spec, raw) {
|
|
2419
|
+
if (raw === void 0 || raw === null) return spec.input === "boolean" ? false : "";
|
|
2420
|
+
switch (spec.input) {
|
|
2421
|
+
case "boolean":
|
|
2422
|
+
return Boolean(raw);
|
|
2423
|
+
case "date": {
|
|
2424
|
+
if (raw instanceof Date) return raw.toISOString().slice(0, 10);
|
|
2425
|
+
const s = String(raw);
|
|
2426
|
+
return /^\d{4}-\d{2}-\d{2}/.test(s) ? s.slice(0, 10) : s;
|
|
2427
|
+
}
|
|
2428
|
+
case "tags":
|
|
2429
|
+
return Array.isArray(raw) ? raw.map(String) : String(raw).split(",").map((t) => t.trim()).filter(Boolean);
|
|
2430
|
+
default:
|
|
2431
|
+
return String(raw);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
function toFrontmatter(specs, state, hiddenValues) {
|
|
2435
|
+
const frontmatter = { ...hiddenValues };
|
|
2436
|
+
for (const spec of specs) {
|
|
2437
|
+
if (spec.role === "hidden" || spec.role === "body") continue;
|
|
2438
|
+
const v = state[spec.name];
|
|
2439
|
+
if (spec.input === "boolean") {
|
|
2440
|
+
if (v === true) frontmatter[spec.name] = true;
|
|
2441
|
+
else if (spec.required) frontmatter[spec.name] = false;
|
|
2442
|
+
continue;
|
|
2443
|
+
}
|
|
2444
|
+
if (Array.isArray(v)) {
|
|
2445
|
+
if (v.length || spec.required) frontmatter[spec.name] = v;
|
|
2446
|
+
continue;
|
|
2447
|
+
}
|
|
2448
|
+
const s = typeof v === "string" ? v.trim() : "";
|
|
2449
|
+
if (s === "") {
|
|
2450
|
+
if (spec.required) frontmatter[spec.name] = spec.input === "tags" ? [] : "";
|
|
2451
|
+
continue;
|
|
2452
|
+
}
|
|
2453
|
+
switch (spec.input) {
|
|
2454
|
+
case "number": {
|
|
2455
|
+
const n = Number(s);
|
|
2456
|
+
frontmatter[spec.name] = Number.isNaN(n) ? s : n;
|
|
2457
|
+
break;
|
|
2458
|
+
}
|
|
2459
|
+
case "tags":
|
|
2460
|
+
frontmatter[spec.name] = s.split(",").map((t) => t.trim()).filter(Boolean);
|
|
2461
|
+
break;
|
|
2462
|
+
default:
|
|
2463
|
+
frontmatter[spec.name] = s;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return frontmatter;
|
|
2467
|
+
}
|
|
2468
|
+
var LEAVE_MESSAGE = "You have unsaved changes. Leave this page and lose them?";
|
|
2469
|
+
function slugify(s) {
|
|
2470
|
+
return s.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2471
|
+
}
|
|
2472
|
+
function ContentForm({ entry, initialValues = {}, initialBody = "", onSubmit, onCancel, submitLabel = "Save", className, showBody = true, bodyLabel = "Content", renderPreview, previewClassName, termComponent, onPickImage, onUploadImage, previewSrc, stickyBar = false, status, barExtra, footer, renderLabel, derive, guardUnsaved = true }) {
|
|
2473
|
+
const formId = react.useId();
|
|
2474
|
+
const specs = react.useMemo(() => walkSchema(entry.schema, entry), [entry]);
|
|
2475
|
+
const visible = react.useMemo(() => specs.filter((s) => s.role !== "hidden" && s.role !== "body"), [specs]);
|
|
2476
|
+
const bodySpec = specs.find((s) => s.role === "body");
|
|
2477
|
+
const sections = react.useMemo(() => {
|
|
2478
|
+
const groups = entry.groups ?? [];
|
|
2479
|
+
const grouped = new Set(groups.flatMap((g) => g.fields));
|
|
2480
|
+
const byName = new Map(visible.map((s) => [s.name, s]));
|
|
2481
|
+
const main = { label: entry.singular ?? entry.label, fields: visible.filter((s) => !grouped.has(s.name)) };
|
|
2482
|
+
const rest = groups.map((g) => ({ label: g.label, fields: g.fields.map((n) => byName.get(n)).filter((s) => Boolean(s)) })).filter((g) => g.fields.length > 0);
|
|
2483
|
+
return [main, ...rest];
|
|
2484
|
+
}, [visible, entry.groups, entry.label, entry.singular]);
|
|
2485
|
+
const initialState = react.useMemo(() => Object.fromEntries(specs.map((s) => [s.name, toControl(s, initialValues[s.name])])), [specs, initialValues]);
|
|
2486
|
+
const [state, setState] = react.useState(initialState);
|
|
2487
|
+
const [deriving, setDeriving] = react.useState(() => Boolean(derive && !initialState[derive.target]));
|
|
2488
|
+
const update = (name, v) => setState((st) => {
|
|
2489
|
+
const next = { ...st, [name]: v };
|
|
2490
|
+
if (derive && deriving && name === derive.source && typeof v === "string") {
|
|
2491
|
+
next[derive.target] = (derive.transform ?? slugify)(v);
|
|
2492
|
+
}
|
|
2493
|
+
return next;
|
|
2494
|
+
});
|
|
2495
|
+
const onFieldChange = (name, v) => {
|
|
2496
|
+
if (derive && deriving && name === derive.target) setDeriving(false);
|
|
2497
|
+
update(name, v);
|
|
2498
|
+
};
|
|
2499
|
+
const [body, setBody] = react.useState(initialBody);
|
|
2500
|
+
const pictures = useImages();
|
|
2501
|
+
const pickImage = onPickImage ? (field) => async (file) => {
|
|
2502
|
+
const r = await onPickImage(field, file);
|
|
2503
|
+
pictures.keep(field, r, typeof state[field] === "string" ? state[field] : "");
|
|
2504
|
+
onFieldChange(field, r.path);
|
|
2505
|
+
} : void 0;
|
|
2506
|
+
const removeImage = (field) => () => {
|
|
2507
|
+
const previous = pictures.remove(field);
|
|
2508
|
+
onFieldChange(field, previous ?? "");
|
|
2509
|
+
};
|
|
2510
|
+
const dirty = body !== initialBody || specs.some((s) => !sameValue(state[s.name], initialState[s.name]));
|
|
2511
|
+
const [issues, setIssues] = react.useState([]);
|
|
2512
|
+
const [attempted, setAttempted] = react.useState(false);
|
|
2513
|
+
const [submitting, setSubmitting] = react.useState(false);
|
|
2514
|
+
const [options, setOptions] = react.useState({});
|
|
2515
|
+
const hiddenValues = react.useMemo(() => {
|
|
2516
|
+
const shown = new Set(visible.map((s) => s.name));
|
|
2517
|
+
if (bodySpec) shown.add(bodySpec.name);
|
|
2518
|
+
return Object.fromEntries(Object.entries(initialValues).filter(([k]) => !shown.has(k)));
|
|
2519
|
+
}, [initialValues, visible, bodySpec]);
|
|
2520
|
+
react.useEffect(() => {
|
|
2521
|
+
let live = true;
|
|
2522
|
+
for (const s of visible) {
|
|
2523
|
+
if (typeof s.options !== "function") continue;
|
|
2524
|
+
setOptions((o) => ({ ...o, [s.name]: "loading" }));
|
|
2525
|
+
Promise.resolve(s.options()).then((list) => {
|
|
2526
|
+
if (live) setOptions((o) => ({ ...o, [s.name]: list }));
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
return () => {
|
|
2530
|
+
live = false;
|
|
2531
|
+
};
|
|
2532
|
+
}, [visible]);
|
|
2533
|
+
const leaving = react.useRef(false);
|
|
2534
|
+
react.useEffect(() => {
|
|
2535
|
+
if (!guardUnsaved || !dirty || submitting) return;
|
|
2536
|
+
const onUnload = (e) => {
|
|
2537
|
+
if (!leaving.current) {
|
|
2538
|
+
e.preventDefault();
|
|
2539
|
+
e.returnValue = LEAVE_MESSAGE;
|
|
2540
|
+
}
|
|
2541
|
+
};
|
|
2542
|
+
const onClick = (e) => {
|
|
2543
|
+
const a = e.target?.closest?.("a[href]");
|
|
2544
|
+
if (!a || a.target === "_blank" || e.defaultPrevented || e.metaKey || e.ctrlKey) return;
|
|
2545
|
+
if (a.origin !== window.location.origin) return;
|
|
2546
|
+
if (!window.confirm(LEAVE_MESSAGE)) {
|
|
2547
|
+
e.preventDefault();
|
|
2548
|
+
e.stopPropagation();
|
|
2549
|
+
} else leaving.current = true;
|
|
2550
|
+
};
|
|
2551
|
+
window.addEventListener("beforeunload", onUnload);
|
|
2552
|
+
document.addEventListener("click", onClick, true);
|
|
2553
|
+
return () => {
|
|
2554
|
+
window.removeEventListener("beforeunload", onUnload);
|
|
2555
|
+
document.removeEventListener("click", onClick, true);
|
|
2556
|
+
};
|
|
2557
|
+
}, [guardUnsaved, dirty, submitting]);
|
|
2558
|
+
const validate = react.useCallback(() => {
|
|
2559
|
+
const frontmatter = toFrontmatter(specs, state, hiddenValues);
|
|
2560
|
+
const result = entry.schema.safeParse(frontmatter);
|
|
2561
|
+
setIssues(result.success ? [] : mapIssues(result.error.issues, specs));
|
|
2562
|
+
return result;
|
|
2563
|
+
}, [entry.schema, specs, state, hiddenValues]);
|
|
2564
|
+
const handleSubmit = async (e) => {
|
|
2565
|
+
e.preventDefault();
|
|
2566
|
+
if (submitting) return;
|
|
2567
|
+
setAttempted(true);
|
|
2568
|
+
const result = validate();
|
|
2569
|
+
if (!result.success) return;
|
|
2570
|
+
setSubmitting(true);
|
|
2571
|
+
try {
|
|
2572
|
+
await onSubmit({ frontmatter: result.data, body, images: pictures.toSubmission() });
|
|
2573
|
+
} finally {
|
|
2574
|
+
setSubmitting(false);
|
|
2575
|
+
}
|
|
2576
|
+
};
|
|
2577
|
+
const { byField, formLevel } = groupIssues(issues);
|
|
2578
|
+
const optionsFor = (s) => options[s.name] ?? (Array.isArray(s.options) ? s.options : void 0);
|
|
2579
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("form", { className: ["realiiz-form", className].filter(Boolean).join(" "), onSubmit: handleSubmit, noValidate: true, "data-realiiz-content-form": entry.id, children: [
|
|
2580
|
+
/* @__PURE__ */ jsxRuntime.jsx("style", { children: FORM_CSS }),
|
|
2581
|
+
formLevel.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-form__alert", role: "alert", children: [
|
|
2582
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: "This can't be saved yet." }),
|
|
2583
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { children: formLevel.map((i, n) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: i.message }, n)) })
|
|
2584
|
+
] }),
|
|
2585
|
+
sections.map((sec) => /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
|
|
2586
|
+
/* @__PURE__ */ jsxRuntime.jsx("legend", { children: sec.label }),
|
|
2587
|
+
sec.fields.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2588
|
+
Field,
|
|
2589
|
+
{
|
|
2590
|
+
formId,
|
|
2591
|
+
spec: s,
|
|
2592
|
+
value: state[s.name],
|
|
2593
|
+
issues: byField.get(s.name) ?? [],
|
|
2594
|
+
options: optionsFor(s),
|
|
2595
|
+
renderLabel,
|
|
2596
|
+
picked: pictures.images[s.name],
|
|
2597
|
+
onPickImage: pickImage?.(s.name),
|
|
2598
|
+
onRemoveImage: removeImage(s.name),
|
|
2599
|
+
onChange: (v) => onFieldChange(s.name, v),
|
|
2600
|
+
onBlur: () => {
|
|
2601
|
+
if (attempted) validate();
|
|
2602
|
+
}
|
|
2603
|
+
},
|
|
2604
|
+
s.name
|
|
2605
|
+
))
|
|
2606
|
+
] }, sec.label)),
|
|
2607
|
+
(showBody || bodySpec) && /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
|
|
2608
|
+
/* @__PURE__ */ jsxRuntime.jsx("legend", { children: bodySpec?.label ?? bodyLabel }),
|
|
2609
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "realiiz-form__label realiiz-sr-only", htmlFor: `${formId}-body`, children: bodySpec?.label ?? bodyLabel }),
|
|
2610
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2611
|
+
MarkdownEditor,
|
|
2612
|
+
{
|
|
2613
|
+
id: `${formId}-body`,
|
|
2614
|
+
value: body,
|
|
2615
|
+
onChange: setBody,
|
|
2616
|
+
placeholder: bodySpec?.placeholder ?? "Write in Markdown. Headings with ##, links as [text](url).",
|
|
2617
|
+
renderPreview,
|
|
2618
|
+
previewClassName,
|
|
2619
|
+
termComponent,
|
|
2620
|
+
onUploadImage,
|
|
2621
|
+
previewSrc
|
|
2622
|
+
}
|
|
2623
|
+
)
|
|
2624
|
+
] }),
|
|
2625
|
+
footer && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-form__footer", children: footer }),
|
|
2626
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: stickyBar ? "realiiz-form__bar" : "realiiz-form__actions", "data-dirty": dirty || void 0, children: [
|
|
2627
|
+
stickyBar && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-form__status", role: "status", children: status ?? (dirty ? "Unsaved changes" : "No changes yet") }),
|
|
2628
|
+
stickyBar && barExtra,
|
|
2629
|
+
onCancel && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "realiiz-form__button", onClick: onCancel, children: "Cancel" }),
|
|
2630
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: "realiiz-form__button realiiz-form__button--primary", disabled: submitting, children: submitting ? `${submitLabel}\u2026` : submitLabel })
|
|
2631
|
+
] })
|
|
2632
|
+
] });
|
|
2633
|
+
}
|
|
2634
|
+
function sameValue(a, b) {
|
|
2635
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
2636
|
+
return a === b;
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
// src/media/errors.ts
|
|
2640
|
+
var MediaError = class extends Error {
|
|
2641
|
+
constructor(message) {
|
|
2642
|
+
super(message);
|
|
2643
|
+
this.name = "MediaError";
|
|
2644
|
+
}
|
|
2645
|
+
};
|
|
2646
|
+
var MESSAGES = {
|
|
2647
|
+
type: "That file type isn\u2019t supported \u2014 use a JPG, PNG or WebP.",
|
|
2648
|
+
size: "That picture is too big to send \u2014 pick one under 10MB.",
|
|
2649
|
+
encode: "This browser can\u2019t prepare pictures \u2014 try Chrome, Edge or Safari 16+."
|
|
2650
|
+
};
|
|
2651
|
+
|
|
2652
|
+
// src/media/resize.ts
|
|
2653
|
+
var ACCEPTED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"];
|
|
2654
|
+
var MAX_INPUT_BYTES = 10 * 1024 * 1024;
|
|
2655
|
+
function fitWithin(width, height, maxWidth) {
|
|
2656
|
+
if (width <= maxWidth) return { width, height };
|
|
2657
|
+
const ratio = maxWidth / width;
|
|
2658
|
+
return { width: Math.round(width * ratio), height: Math.max(1, Math.round(height * ratio)) };
|
|
2659
|
+
}
|
|
2660
|
+
function checkInput(file) {
|
|
2661
|
+
if (!ACCEPTED_TYPES.includes(file.type)) throw new MediaError(MESSAGES.type);
|
|
2662
|
+
if (file.size > MAX_INPUT_BYTES) throw new MediaError(MESSAGES.size);
|
|
2663
|
+
}
|
|
2664
|
+
async function resizeImage(file, opts = {}) {
|
|
2665
|
+
checkInput(file);
|
|
2666
|
+
const { maxWidth = 1600, quality = 0.82, maxBytes = 600 * 1024 } = opts;
|
|
2667
|
+
const bitmap = await decode(file);
|
|
2668
|
+
const { width, height } = fitWithin(bitmap.width, bitmap.height, maxWidth);
|
|
2669
|
+
const canvas = makeCanvas(width, height);
|
|
2670
|
+
const ctx = canvas.getContext("2d");
|
|
2671
|
+
if (!ctx) throw new MediaError(MESSAGES.encode);
|
|
2672
|
+
ctx.drawImage(bitmap, 0, 0, width, height);
|
|
2673
|
+
bitmap.close?.();
|
|
2674
|
+
let q = quality;
|
|
2675
|
+
let blob = await encode(canvas, q);
|
|
2676
|
+
while (blob.size > maxBytes && q - 0.08 >= 0.5) {
|
|
2677
|
+
q -= 0.08;
|
|
2678
|
+
blob = await encode(canvas, q);
|
|
2679
|
+
}
|
|
2680
|
+
return { blob, width, height };
|
|
2681
|
+
}
|
|
2682
|
+
async function decode(file) {
|
|
2683
|
+
try {
|
|
2684
|
+
return await createImageBitmap(file);
|
|
2685
|
+
} catch {
|
|
2686
|
+
throw new MediaError(MESSAGES.encode);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
function makeCanvas(width, height) {
|
|
2690
|
+
if (typeof OffscreenCanvas !== "undefined") return new OffscreenCanvas(width, height);
|
|
2691
|
+
const c = document.createElement("canvas");
|
|
2692
|
+
c.width = width;
|
|
2693
|
+
c.height = height;
|
|
2694
|
+
return c;
|
|
2695
|
+
}
|
|
2696
|
+
async function encode(canvas, quality) {
|
|
2697
|
+
const blob = "convertToBlob" in canvas ? await canvas.convertToBlob({ type: "image/webp", quality }).catch(() => null) : await new Promise((resolve) => canvas.toBlob(resolve, "image/webp", quality));
|
|
2698
|
+
if (!blob || blob.type !== "image/webp") throw new MediaError(MESSAGES.encode);
|
|
2699
|
+
return blob;
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
// src/media/paths.ts
|
|
2703
|
+
function slugifyName(name) {
|
|
2704
|
+
const base2 = name.replace(/\.[a-z0-9]+$/i, "");
|
|
2705
|
+
const s = base2.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2706
|
+
return s || "image";
|
|
2707
|
+
}
|
|
2708
|
+
function imagePathFor(kind, slug, originalName = "", opts = {}) {
|
|
2709
|
+
const dir = (opts.publicDir ?? "/blog").replace(/\/$/, "");
|
|
2710
|
+
switch (kind) {
|
|
2711
|
+
case "hero":
|
|
2712
|
+
return `${dir}/${slug}.webp`;
|
|
2713
|
+
case "social":
|
|
2714
|
+
return `${dir}/${slug}-social.webp`;
|
|
2715
|
+
case "body":
|
|
2716
|
+
return `${dir}/${slug}/${slugifyName(originalName)}.webp`;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
// src/studio-ui/useImages.ts
|
|
2721
|
+
var HERO_WIDTH = 1600;
|
|
2722
|
+
var BODY_WIDTH = 1400;
|
|
2723
|
+
var SLUG_FIRST = "Fill in the web address (slug) first \u2014 the picture is named after it.";
|
|
2724
|
+
function useImages2(getSlug, publicDir = "/blog/") {
|
|
2725
|
+
const dir = publicDir.replace(/\/$/, "");
|
|
2726
|
+
const [body, setBody] = react.useState({});
|
|
2727
|
+
const urls = react.useRef(/* @__PURE__ */ new Set());
|
|
2728
|
+
react.useEffect(() => {
|
|
2729
|
+
const set = urls.current;
|
|
2730
|
+
return () => {
|
|
2731
|
+
for (const u of set) URL.revokeObjectURL(u);
|
|
2732
|
+
};
|
|
2733
|
+
}, []);
|
|
2734
|
+
const latest = react.useRef({ getSlug, dir });
|
|
2735
|
+
latest.current = { getSlug, dir };
|
|
2736
|
+
const slugOrThrow = () => {
|
|
2737
|
+
const slug = latest.current.getSlug().trim();
|
|
2738
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) throw new MediaError(SLUG_FIRST);
|
|
2739
|
+
return slug;
|
|
2740
|
+
};
|
|
2741
|
+
const pickField = react.useCallback(async (field, file) => {
|
|
2742
|
+
const slug = slugOrThrow();
|
|
2743
|
+
const kind = field === "ogImage" ? "social" : "hero";
|
|
2744
|
+
const r = await resizeImage(file, { maxWidth: HERO_WIDTH });
|
|
2745
|
+
return { path: imagePathFor(kind, slug, void 0, { publicDir: latest.current.dir }), blob: r.blob, width: r.width, height: r.height };
|
|
2746
|
+
}, []);
|
|
2747
|
+
const uploadBody = react.useCallback(async (file) => {
|
|
2748
|
+
const slug = slugOrThrow();
|
|
2749
|
+
const r = await resizeImage(file, { maxWidth: BODY_WIDTH });
|
|
2750
|
+
const path = imagePathFor("body", slug, file.name, { publicDir: latest.current.dir });
|
|
2751
|
+
const previewUrl = URL.createObjectURL(r.blob);
|
|
2752
|
+
urls.current.add(previewUrl);
|
|
2753
|
+
setBody((m) => ({ ...m, [path]: { blob: r.blob, previewUrl } }));
|
|
2754
|
+
return { path, previewUrl };
|
|
2755
|
+
}, []);
|
|
2756
|
+
const previewSrc = Object.fromEntries(Object.entries(body).map(([p, v]) => [p, v.previewUrl]));
|
|
2757
|
+
const toPayload = react.useCallback(async (fields, markdown) => {
|
|
2758
|
+
const out = Object.values(fields);
|
|
2759
|
+
for (const [path, v] of Object.entries(body)) if (markdown.includes(path)) out.push({ path, blob: v.blob });
|
|
2760
|
+
return Promise.all(out.map(async ({ path, blob }) => ({ path, base64: await toBase64(blob) })));
|
|
2761
|
+
}, [body]);
|
|
2762
|
+
return { pickField, uploadBody, previewSrc, toPayload };
|
|
2763
|
+
}
|
|
2764
|
+
function toBase64(blob) {
|
|
2765
|
+
return new Promise((resolve, reject) => {
|
|
2766
|
+
const r = new FileReader();
|
|
2767
|
+
r.onload = () => resolve(String(r.result).split(",")[1] ?? "");
|
|
2768
|
+
r.onerror = () => reject(r.error);
|
|
2769
|
+
r.readAsDataURL(blob);
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
function ContentEditor({ typeId, initialValues, initialBody, existing = null, isNew = false, footer }) {
|
|
2773
|
+
const entry = useEntry(typeId);
|
|
2774
|
+
const { publishContent, previewBody } = useStudio().actions;
|
|
2775
|
+
const [error, setError] = react.useState(null);
|
|
2776
|
+
const [, startTransition] = react.useTransition();
|
|
2777
|
+
const pictures = useImages2(
|
|
2778
|
+
() => document.querySelector(`form[data-realiiz-content-form="${typeId}"] [name="slug"]`)?.value ?? "",
|
|
2779
|
+
entry ? publicDirOf(entry) : "/images/"
|
|
2780
|
+
);
|
|
2781
|
+
if (!entry) return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-alert", role: "alert", children: [
|
|
2782
|
+
"Unknown content type \u201C",
|
|
2783
|
+
typeId,
|
|
2784
|
+
"\u201D."
|
|
2785
|
+
] });
|
|
2786
|
+
const onSubmit = (s) => new Promise((resolve) => {
|
|
2787
|
+
startTransition(async () => {
|
|
2788
|
+
setError(null);
|
|
2789
|
+
const images = await pictures.toPayload(s.images, s.body);
|
|
2790
|
+
const result = await publishContent({ type: typeId, frontmatter: s.frontmatter, body: s.body, images, existing: existing ? { number: existing.number, branch: existing.branch } : null });
|
|
2791
|
+
if (result && result.ok === false) setError(result.error);
|
|
2792
|
+
resolve();
|
|
2793
|
+
});
|
|
2794
|
+
});
|
|
2795
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2796
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-alert", role: "alert", children: error }),
|
|
2797
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2798
|
+
ContentForm,
|
|
2799
|
+
{
|
|
2800
|
+
entry,
|
|
2801
|
+
initialValues,
|
|
2802
|
+
initialBody,
|
|
2803
|
+
onSubmit,
|
|
2804
|
+
submitLabel: existing ? "Save changes" : "Save and continue",
|
|
2805
|
+
stickyBar: true,
|
|
2806
|
+
status: existing ? `Change #${existing.number} is waiting to publish \u2014 saving updates it` : void 0,
|
|
2807
|
+
barExtra: existing ? /* @__PURE__ */ jsxRuntime.jsx(LinkButton, { forward: true, href: `/admin/pr/${existing.number}`, children: existing.state === "ready" ? "Review & publish" : "See progress" }) : void 0,
|
|
2808
|
+
bodyLabel: entry.id === "posts" ? "Post content" : "Description",
|
|
2809
|
+
renderPreview: previewBody,
|
|
2810
|
+
previewClassName: "blog-prose",
|
|
2811
|
+
termComponent: "StudioTerm",
|
|
2812
|
+
onPickImage: pictures.pickField,
|
|
2813
|
+
onUploadImage: pictures.uploadBody,
|
|
2814
|
+
previewSrc: pictures.previewSrc,
|
|
2815
|
+
renderLabel: (label, help) => /* @__PURE__ */ jsxRuntime.jsx(StudioTerm, { definition: help, children: label }),
|
|
2816
|
+
derive: isNew ? { source: "title", target: "slug" } : void 0,
|
|
2817
|
+
footer
|
|
2818
|
+
}
|
|
2819
|
+
)
|
|
2820
|
+
] });
|
|
2821
|
+
}
|
|
2822
|
+
function RemoveItem({ typeId, slug }) {
|
|
2823
|
+
const [confirm, setConfirm] = react.useState(false);
|
|
2824
|
+
const [error, setError] = react.useState(null);
|
|
2825
|
+
const [pending, start] = react.useTransition();
|
|
2826
|
+
const entry = useEntry(typeId);
|
|
2827
|
+
const { removeContent } = useStudio().actions;
|
|
2828
|
+
if (!entry) return null;
|
|
2829
|
+
const copy = copyFor(entry);
|
|
2830
|
+
const remove = () => start(async () => {
|
|
2831
|
+
const r = await removeContent({ type: typeId, slug });
|
|
2832
|
+
if (r && r.ok === false) setError(r.error);
|
|
2833
|
+
});
|
|
2834
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-discard", children: [
|
|
2835
|
+
confirm ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2836
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: copy.removeConfirm }),
|
|
2837
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "danger", size: "sm", disabled: pending, onClick: remove, children: pending ? "Preparing\u2026" : "Yes, remove it" }),
|
|
2838
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { size: "sm", onClick: () => setConfirm(false), children: "Keep it" })
|
|
2839
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "rz-discard__link", onClick: () => setConfirm(true), children: copy.removeLink }),
|
|
2840
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-alert admin__alert", children: error })
|
|
2841
|
+
] });
|
|
2842
|
+
}
|
|
2843
|
+
var POLL_MS = 8e3;
|
|
2844
|
+
var LOCAL_FALLBACK_MS = 15e4;
|
|
2845
|
+
var GIVE_UP_MS = 15 * 6e4;
|
|
2846
|
+
function useDeployed(active, mergedSha) {
|
|
2847
|
+
const [state, setState] = react.useState("waiting");
|
|
2848
|
+
react.useEffect(() => {
|
|
2849
|
+
if (!active) return;
|
|
2850
|
+
let stop = false;
|
|
2851
|
+
let baseline;
|
|
2852
|
+
const started = Date.now();
|
|
2853
|
+
const read2 = async () => {
|
|
2854
|
+
try {
|
|
2855
|
+
const r = await fetch("/api/version", { cache: "no-store" });
|
|
2856
|
+
const j = await r.json();
|
|
2857
|
+
return j.sha;
|
|
2858
|
+
} catch {
|
|
2859
|
+
return null;
|
|
2860
|
+
}
|
|
2861
|
+
};
|
|
2862
|
+
const tick = async () => {
|
|
2863
|
+
if (stop) return;
|
|
2864
|
+
const sha = await read2();
|
|
2865
|
+
if (stop) return;
|
|
2866
|
+
if (baseline === void 0) baseline = sha;
|
|
2867
|
+
const elapsed = Date.now() - started;
|
|
2868
|
+
const matched = sha !== null && mergedSha !== null && sha === mergedSha;
|
|
2869
|
+
const moved = sha !== null && baseline !== null && sha !== baseline;
|
|
2870
|
+
const localDone = sha === null && elapsed >= LOCAL_FALLBACK_MS;
|
|
2871
|
+
if (matched || moved || localDone) {
|
|
2872
|
+
setState("live");
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
if (elapsed >= GIVE_UP_MS) {
|
|
2876
|
+
setState("slow");
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
setTimeout(tick, POLL_MS);
|
|
2880
|
+
};
|
|
2881
|
+
void tick();
|
|
2882
|
+
return () => {
|
|
2883
|
+
stop = true;
|
|
2884
|
+
};
|
|
2885
|
+
}, [active, mergedSha]);
|
|
2886
|
+
return state;
|
|
2887
|
+
}
|
|
2888
|
+
var REFRESH_MS = 1e4;
|
|
2889
|
+
function PublishPanel({ number, htmlUrl, merged, closed = false, branch, removal = false, mergedSha = null, status, item }) {
|
|
2890
|
+
const { mergeContentPr, discardChange } = useStudio().actions;
|
|
2891
|
+
const router = navigation.useRouter();
|
|
2892
|
+
const [outcome, setOutcome] = react.useState(merged ? { state: "merged", sha: "" } : null);
|
|
2893
|
+
const [pending, start] = react.useTransition();
|
|
2894
|
+
const [confirmDiscard, setConfirmDiscard] = react.useState(false);
|
|
2895
|
+
const [discarded, setDiscarded] = react.useState(closed);
|
|
2896
|
+
const [discardError, setDiscardError] = react.useState(null);
|
|
2897
|
+
const isPending = !merged && outcome === null && status?.state === "pending";
|
|
2898
|
+
react.useEffect(() => {
|
|
2899
|
+
if (!isPending) return;
|
|
2900
|
+
const t = setInterval(() => router.refresh(), REFRESH_MS);
|
|
2901
|
+
return () => clearInterval(t);
|
|
2902
|
+
}, [isPending, router]);
|
|
2903
|
+
const publish = () => start(async () => setOutcome(await mergeContentPr(number)));
|
|
2904
|
+
const discard = () => start(async () => {
|
|
2905
|
+
const r = await discardChange(number, branch);
|
|
2906
|
+
if (r.state === "discarded") {
|
|
2907
|
+
setDiscarded(true);
|
|
2908
|
+
setConfirmDiscard(false);
|
|
2909
|
+
} else if (r.state === "already-published") setOutcome({ state: "merged", sha: "" });
|
|
2910
|
+
else setDiscardError(r.message);
|
|
2911
|
+
});
|
|
2912
|
+
const publicPath = item?.liveUrl ?? null;
|
|
2913
|
+
const liveUrl = publicPath;
|
|
2914
|
+
const editUrl = item ? `/admin/${item.typeId}/${item.slug}` : null;
|
|
2915
|
+
const noun = item?.noun ?? "item";
|
|
2916
|
+
const listUrl = item ? `/admin/${item.typeId}` : "/admin";
|
|
2917
|
+
const backLabel = item ? `Back to ${item.label}` : "Back to the dashboard";
|
|
2918
|
+
const live = outcome?.state === "merged";
|
|
2919
|
+
const deploy = useDeployed(live, outcome?.state === "merged" && outcome.sha ? outcome.sha : mergedSha);
|
|
2920
|
+
const onSite = live && deploy === "live";
|
|
2921
|
+
const failed = status?.state === "red" || outcome?.state === "red";
|
|
2922
|
+
const failing = status?.state === "red" ? status.failingCheck : outcome?.state === "red" ? outcome.failingCheck : null;
|
|
2923
|
+
const stale = outcome?.state === "stale";
|
|
2924
|
+
const ready = !live && !failed && !stale && status?.state === "green";
|
|
2925
|
+
const previewBase = status?.state === "green" ? status.previewUrl : void 0;
|
|
2926
|
+
const previewUrl = previewBase && publicPath && !removal ? `${previewBase.replace(/\/$/, "")}${publicPath}` : previewBase;
|
|
2927
|
+
const checkState = live || ready ? "done" : failed ? "failed" : "active";
|
|
2928
|
+
const steps = [
|
|
2929
|
+
{ label: "Saved", state: "done", note: `Change #${number}` },
|
|
2930
|
+
{ label: "Checks", state: checkState, note: checkState === "active" ? "Usually a minute or two" : checkState === "failed" ? "Something needs fixing" : "Passed" },
|
|
2931
|
+
{ label: "Published", state: live ? "done" : pending ? "active" : "todo", note: live ? "Sent to the site" : pending ? "Publishing\u2026" : "When you're ready" },
|
|
2932
|
+
{
|
|
2933
|
+
label: removal ? "Removed" : "Live",
|
|
2934
|
+
state: onSite ? "done" : live ? "active" : "todo",
|
|
2935
|
+
note: onSite ? removal ? "Off the site" : "On the site" : live ? deploy === "slow" ? "Taking longer than usual" : "Rebuilding, about two minutes" : "Automatic"
|
|
2936
|
+
}
|
|
2937
|
+
];
|
|
2938
|
+
if (discarded) {
|
|
2939
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2940
|
+
/* @__PURE__ */ jsxRuntime.jsx(Steps, { steps: [{ label: "Saved", state: "done", note: `Change #${number}` }, { label: "Discarded", state: "failed", note: "Nothing was published" }, { label: "Live", state: "todo", note: "Unchanged" }] }),
|
|
2941
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { style: { margin: 0 }, children: [
|
|
2942
|
+
"This change was discarded. The live site is exactly as it was",
|
|
2943
|
+
editUrl ? `; the ${noun} still shows its published version` : "",
|
|
2944
|
+
"."
|
|
2945
|
+
] }),
|
|
2946
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-actions", children: [
|
|
2947
|
+
editUrl && /* @__PURE__ */ jsxRuntime.jsxs(LinkButton, { variant: "primary", href: editUrl, children: [
|
|
2948
|
+
"Open the ",
|
|
2949
|
+
noun
|
|
2950
|
+
] }),
|
|
2951
|
+
/* @__PURE__ */ jsxRuntime.jsx(LinkButton, { href: listUrl, children: backLabel })
|
|
2952
|
+
] })
|
|
2953
|
+
] });
|
|
2954
|
+
}
|
|
2955
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2956
|
+
/* @__PURE__ */ jsxRuntime.jsx(Steps, { steps }),
|
|
2957
|
+
live ? onSite ? /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: 0 }, children: removal ? `Done \u2014 the ${noun} is off the website.` : "Done \u2014 your change is on the website now." }) : deploy === "slow" ? /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: 0 }, children: "The site is taking longer than usual to rebuild. It will get there; check back in a few minutes." }) : /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: 0 }, children: "Published. The website is rebuilding with your change \u2014 this usually takes about two minutes. Stay here and this page will tell you the moment it's live; if you look at the site before then you'll still see the old version." }) : stale ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-alert admin__alert", children: [
|
|
2958
|
+
"Someone else changed this file first. ",
|
|
2959
|
+
/* @__PURE__ */ jsxRuntime.jsx(Link3__default.default, { href: "/admin", children: "Open it again" }),
|
|
2960
|
+
", make your change on the latest version, and publish."
|
|
2961
|
+
] }) : failed ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-alert admin__alert", children: [
|
|
2962
|
+
"The site couldn't build with this change",
|
|
2963
|
+
failing ? ` (${failing})` : "",
|
|
2964
|
+
". Nothing was published. Open the ",
|
|
2965
|
+
noun,
|
|
2966
|
+
", fix it, and save again."
|
|
2967
|
+
] }) : outcome?.state === "error" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-alert admin__alert", children: outcome.message }) : ready ? /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: 0 }, children: removal ? `Everything checks out. Publishing takes this ${noun} off the live site.` : previewUrl ? "Everything checks out. Have a look at the preview \u2014 it's a private copy of the site with your change \u2014 then publish when you're happy." : "Everything checks out. Publishing puts this change on the live site." }) : /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: 0 }, className: "rz-muted", children: "We're building a test copy of the site with your change to make sure nothing breaks. This page updates itself." }),
|
|
2968
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-actions", children: live ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2969
|
+
liveUrl && !removal && (onSite ? /* @__PURE__ */ jsxRuntime.jsxs(LinkButton, { variant: "primary", external: true, href: liveUrl, children: [
|
|
2970
|
+
"View the ",
|
|
2971
|
+
noun
|
|
2972
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "primary", disabled: true, title: "Available once the site has rebuilt", children: [
|
|
2973
|
+
"View the ",
|
|
2974
|
+
noun
|
|
2975
|
+
] })),
|
|
2976
|
+
/* @__PURE__ */ jsxRuntime.jsx(LinkButton, { variant: removal || !liveUrl ? "primary" : "secondary", href: listUrl, children: backLabel }),
|
|
2977
|
+
live && !onSite && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-actions__hint", children: deploy === "slow" ? "Still rebuilding." : "Waiting for the site to rebuild\u2026" })
|
|
2978
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2979
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", disabled: pending || !ready, onClick: publish, children: pending ? "Publishing\u2026" : "Publish" }),
|
|
2980
|
+
previewUrl && ready && /* @__PURE__ */ jsxRuntime.jsx(LinkButton, { external: true, href: previewUrl, children: "Preview the change" }),
|
|
2981
|
+
editUrl && !removal && /* @__PURE__ */ jsxRuntime.jsx(LinkButton, { href: editUrl, children: "Keep editing" }),
|
|
2982
|
+
/* @__PURE__ */ jsxRuntime.jsx(LinkButton, { href: htmlUrl, external: true, children: "View on GitHub" }),
|
|
2983
|
+
!ready && !failed && !stale && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-actions__hint", children: "Publish unlocks when the checks pass." })
|
|
2984
|
+
] }) }),
|
|
2985
|
+
!live && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-discard", children: [
|
|
2986
|
+
confirmDiscard ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2987
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: removal ? `Cancel the removal and keep the ${noun} on the website?` : `Throw away the unpublished changes to this ${noun}? The live site isn't affected. This can't be undone.` }),
|
|
2988
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "danger", size: "sm", disabled: pending, onClick: discard, children: pending ? "Discarding\u2026" : "Yes, discard" }),
|
|
2989
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { size: "sm", onClick: () => setConfirmDiscard(false), children: "Keep it" })
|
|
2990
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "rz-discard__link", onClick: () => setConfirmDiscard(true), children: removal ? "Cancel this removal" : "Discard this change" }),
|
|
2991
|
+
discardError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-alert admin__alert", children: discardError })
|
|
2992
|
+
] })
|
|
2993
|
+
] });
|
|
2994
|
+
}
|
|
2995
|
+
var keyFor2 = (typeId) => `rz-${typeId}-slugs`;
|
|
2996
|
+
var EVENT = "rz-slugs-change";
|
|
2997
|
+
function read(typeId) {
|
|
2998
|
+
try {
|
|
2999
|
+
return window.localStorage.getItem(keyFor2(typeId)) !== "off";
|
|
3000
|
+
} catch {
|
|
3001
|
+
return true;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
function subscribe(cb) {
|
|
3005
|
+
window.addEventListener(EVENT, cb);
|
|
3006
|
+
window.addEventListener("storage", cb);
|
|
3007
|
+
return () => {
|
|
3008
|
+
window.removeEventListener(EVENT, cb);
|
|
3009
|
+
window.removeEventListener("storage", cb);
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
3012
|
+
function useSlugsOn(typeId) {
|
|
3013
|
+
return react.useSyncExternalStore(subscribe, () => read(typeId), () => true);
|
|
3014
|
+
}
|
|
3015
|
+
function setSlugsOn(typeId, next) {
|
|
3016
|
+
try {
|
|
3017
|
+
window.localStorage.setItem(keyFor2(typeId), next ? "on" : "off");
|
|
3018
|
+
} catch {
|
|
3019
|
+
}
|
|
3020
|
+
window.dispatchEvent(new Event(EVENT));
|
|
3021
|
+
}
|
|
3022
|
+
function SlugVisibility({ typeId, children }) {
|
|
3023
|
+
const on = useSlugsOn(typeId);
|
|
3024
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-slugs": on ? "on" : "off", children });
|
|
3025
|
+
}
|
|
3026
|
+
function SlugToggle({ typeId }) {
|
|
3027
|
+
const on = useSlugsOn(typeId);
|
|
3028
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "rz-switch", children: [
|
|
3029
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-switch__label", children: "Show web addresses" }),
|
|
3030
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", role: "switch", checked: on, onChange: (e) => setSlugsOn(typeId, e.target.checked) }),
|
|
3031
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-switch__track", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-switch__thumb" }) })
|
|
3032
|
+
] });
|
|
3033
|
+
}
|
|
3034
|
+
function Field2({ id, label, hint, children }) {
|
|
3035
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct-field", children: [
|
|
3036
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: id, className: "acct-label", children: label }),
|
|
3037
|
+
children,
|
|
3038
|
+
hint && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "acct-hint", children: hint })
|
|
3039
|
+
] });
|
|
3040
|
+
}
|
|
3041
|
+
function Result({ r }) {
|
|
3042
|
+
if (!r) return null;
|
|
3043
|
+
return r.ok ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "acct-ok", role: "status", children: r.message ?? "Saved." }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-alert admin__alert", role: "alert", children: r.error });
|
|
3044
|
+
}
|
|
3045
|
+
function FormCard({ title, blurb, children, action, saveLabel = "Save", disabled }) {
|
|
3046
|
+
const [busy, setBusy] = react.useState(false);
|
|
3047
|
+
const [result, setResult] = react.useState(null);
|
|
3048
|
+
const submit = async (e) => {
|
|
3049
|
+
e.preventDefault();
|
|
3050
|
+
if (busy) return;
|
|
3051
|
+
setBusy(true);
|
|
3052
|
+
setResult(null);
|
|
3053
|
+
try {
|
|
3054
|
+
setResult(await action());
|
|
3055
|
+
} catch (err) {
|
|
3056
|
+
setResult({ ok: false, error: err instanceof Error ? err.message : "Something went wrong. Try again." });
|
|
3057
|
+
} finally {
|
|
3058
|
+
setBusy(false);
|
|
3059
|
+
}
|
|
3060
|
+
};
|
|
3061
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("form", { className: "rz-card acct-card", onSubmit: submit, noValidate: true, children: [
|
|
3062
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "acct-title", children: title }),
|
|
3063
|
+
blurb && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "acct-blurb", children: blurb }),
|
|
3064
|
+
children,
|
|
3065
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct-actions", children: [
|
|
3066
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { type: "submit", variant: "primary", disabled: busy || disabled, children: busy ? `${saveLabel}\u2026` : saveLabel }),
|
|
3067
|
+
/* @__PURE__ */ jsxRuntime.jsx(Result, { r: result })
|
|
3068
|
+
] })
|
|
3069
|
+
] });
|
|
3070
|
+
}
|
|
3071
|
+
function toBase642(blob) {
|
|
3072
|
+
return new Promise((resolve, reject) => {
|
|
3073
|
+
const r = new FileReader();
|
|
3074
|
+
r.onload = () => resolve(String(r.result).split(",")[1] ?? "");
|
|
3075
|
+
r.onerror = () => reject(r.error);
|
|
3076
|
+
r.readAsDataURL(blob);
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
function PasswordInput({ id, value, onChange, autoComplete }) {
|
|
3080
|
+
const [show, setShow] = react.useState(false);
|
|
3081
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct-pw", children: [
|
|
3082
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { id, className: "acct-input", type: show ? "text" : "password", value, onChange: (e) => onChange(e.target.value), autoComplete, spellCheck: false }),
|
|
3083
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "acct-pw__eye", onClick: () => setShow((s) => !s), "aria-label": show ? "Hide password" : "Show password", "aria-pressed": show, children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: show ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3084
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }),
|
|
3085
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "3" })
|
|
3086
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3087
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" }),
|
|
3088
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "1", y1: "1", x2: "23", y2: "23" })
|
|
3089
|
+
] }) }) })
|
|
3090
|
+
] });
|
|
3091
|
+
}
|
|
3092
|
+
function formatPhone(raw) {
|
|
3093
|
+
let d = raw.replace(/\D/g, "");
|
|
3094
|
+
if (d.length === 11 && d.startsWith("1")) d = d.slice(1);
|
|
3095
|
+
d = d.slice(0, 10);
|
|
3096
|
+
if (d.length === 0) return "";
|
|
3097
|
+
if (d.length < 4) return `(${d}`;
|
|
3098
|
+
if (d.length < 7) return `(${d.slice(0, 3)}) ${d.slice(3)}`;
|
|
3099
|
+
return `(${d.slice(0, 3)}) ${d.slice(3, 6)}-${d.slice(6)}`;
|
|
3100
|
+
}
|
|
3101
|
+
function Dropdown({ value, options, onChange, disabled, label }) {
|
|
3102
|
+
const [open, setOpen] = react.useState(false);
|
|
3103
|
+
const wrap = react.useRef(null);
|
|
3104
|
+
react.useEffect(() => {
|
|
3105
|
+
if (!open) return;
|
|
3106
|
+
const onDoc = (e) => {
|
|
3107
|
+
if (!wrap.current?.contains(e.target)) setOpen(false);
|
|
3108
|
+
};
|
|
3109
|
+
const onKey = (e) => {
|
|
3110
|
+
if (e.key === "Escape") setOpen(false);
|
|
3111
|
+
};
|
|
3112
|
+
document.addEventListener("mousedown", onDoc);
|
|
3113
|
+
document.addEventListener("keydown", onKey);
|
|
3114
|
+
return () => {
|
|
3115
|
+
document.removeEventListener("mousedown", onDoc);
|
|
3116
|
+
document.removeEventListener("keydown", onKey);
|
|
3117
|
+
};
|
|
3118
|
+
}, [open]);
|
|
3119
|
+
const current = options.find((o) => o.value === value);
|
|
3120
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: wrap, className: "acct-dd", children: [
|
|
3121
|
+
/* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "acct-dd__btn", "aria-haspopup": "listbox", "aria-expanded": open, "aria-label": label, disabled, onClick: () => setOpen((v) => !v), children: [
|
|
3122
|
+
current?.label ?? value,
|
|
3123
|
+
/* @__PURE__ */ jsxRuntime.jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 9l6 6 6-6" }) })
|
|
3124
|
+
] }),
|
|
3125
|
+
open && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-menu acct-dd__menu", role: "listbox", "aria-label": label, children: options.map((o) => /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "option", "aria-selected": o.value === value, className: "rz-menu__item acct-dd__item", onClick: () => {
|
|
3126
|
+
setOpen(false);
|
|
3127
|
+
if (o.value !== value) onChange(o.value);
|
|
3128
|
+
}, children: [
|
|
3129
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "acct-dd__check", "aria-hidden": "true", children: o.value === value ? "\u2713" : "" }),
|
|
3130
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "acct-dd__text", children: [
|
|
3131
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: o.label }),
|
|
3132
|
+
o.hint && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "acct-dd__hint", children: o.hint })
|
|
3133
|
+
] })
|
|
3134
|
+
] }, o.value)) })
|
|
3135
|
+
] });
|
|
3136
|
+
}
|
|
3137
|
+
function PasswordSignIn({ url, anonKey, next = "/admin" }) {
|
|
3138
|
+
const supabase = react.useMemo(() => ssr.createBrowserClient(url, anonKey), [url, anonKey]);
|
|
3139
|
+
const [open, setOpen] = react.useState(false);
|
|
3140
|
+
const [email, setEmail] = react.useState("");
|
|
3141
|
+
const [password, setPassword] = react.useState("");
|
|
3142
|
+
const [busy, setBusy] = react.useState(false);
|
|
3143
|
+
const [error, setError] = react.useState(null);
|
|
3144
|
+
const submit = async (e) => {
|
|
3145
|
+
e.preventDefault();
|
|
3146
|
+
if (busy) return;
|
|
3147
|
+
setBusy(true);
|
|
3148
|
+
setError(null);
|
|
3149
|
+
const { error: error2 } = await supabase.auth.signInWithPassword({ email: email.trim().toLowerCase(), password });
|
|
3150
|
+
if (error2) {
|
|
3151
|
+
setError(/invalid/i.test(error2.message) ? "That email and password don\u2019t match. Try again, or use an email link instead." : error2.message);
|
|
3152
|
+
setBusy(false);
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
window.location.assign(next);
|
|
3156
|
+
};
|
|
3157
|
+
if (!open) {
|
|
3158
|
+
return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "pwsi-toggle", children: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "acct-link", onClick: () => setOpen(true), children: "Have a password? Sign in with it instead" }) });
|
|
3159
|
+
}
|
|
3160
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("form", { className: "pwsi", onSubmit: submit, noValidate: true, children: [
|
|
3161
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "acct-title", children: "Sign in with a password" }),
|
|
3162
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "acct-blurb", children: "Forgotten it? Sign in with an email link and set a new one under Settings \u2192 Change Password." }),
|
|
3163
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct-field", children: [
|
|
3164
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "pwsi-email", className: "acct-label", children: "Email address" }),
|
|
3165
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { id: "pwsi-email", className: "acct-input", type: "email", value: email, onChange: (e) => setEmail(e.target.value), autoComplete: "username", required: true })
|
|
3166
|
+
] }),
|
|
3167
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct-field", children: [
|
|
3168
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "pwsi-password", className: "acct-label", children: "Password" }),
|
|
3169
|
+
/* @__PURE__ */ jsxRuntime.jsx(PasswordInput, { id: "pwsi-password", value: password, onChange: setPassword, autoComplete: "current-password" })
|
|
3170
|
+
] }),
|
|
3171
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-alert admin__alert", role: "alert", children: error }),
|
|
3172
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct-actions", children: [
|
|
3173
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { type: "submit", variant: "primary", disabled: busy, children: busy ? "Signing in\u2026" : "Sign in" }),
|
|
3174
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "acct-link", onClick: () => setOpen(false), children: "Forgot it? Use an email link instead" })
|
|
3175
|
+
] })
|
|
3176
|
+
] });
|
|
3177
|
+
}
|
|
3178
|
+
function AcceptInvite({ url, anonKey }) {
|
|
3179
|
+
const [error, setError] = react.useState(null);
|
|
3180
|
+
react.useEffect(() => {
|
|
3181
|
+
const run = async () => {
|
|
3182
|
+
const search = new URLSearchParams(window.location.search);
|
|
3183
|
+
const code = search.get("code");
|
|
3184
|
+
if (code) {
|
|
3185
|
+
window.location.replace(`/admin/auth/callback?code=${encodeURIComponent(code)}&next=/admin/account`);
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
const hash = new URLSearchParams(window.location.hash.replace(/^#/, ""));
|
|
3189
|
+
const access_token = hash.get("access_token");
|
|
3190
|
+
const refresh_token = hash.get("refresh_token");
|
|
3191
|
+
const described = hash.get("error_description");
|
|
3192
|
+
if (described) {
|
|
3193
|
+
setError(described.replace(/\+/g, " "));
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
if (!access_token || !refresh_token) {
|
|
3197
|
+
setError("This invitation link is incomplete or has already been used. Ask for a new one.");
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
const supabase = ssr.createBrowserClient(url, anonKey);
|
|
3201
|
+
const { error: error2 } = await supabase.auth.setSession({ access_token, refresh_token });
|
|
3202
|
+
if (error2) {
|
|
3203
|
+
setError(error2.message);
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
window.history.replaceState(null, "", window.location.pathname);
|
|
3207
|
+
window.location.replace("/admin/account");
|
|
3208
|
+
};
|
|
3209
|
+
void run();
|
|
3210
|
+
}, [url, anonKey]);
|
|
3211
|
+
if (error) {
|
|
3212
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-alert admin__alert", role: "alert", children: [
|
|
3213
|
+
error,
|
|
3214
|
+
" ",
|
|
3215
|
+
/* @__PURE__ */ jsxRuntime.jsx(Link3__default.default, { href: "/admin/sign-in", children: "Sign in with an email link instead." })
|
|
3216
|
+
] });
|
|
3217
|
+
}
|
|
3218
|
+
return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "rz-muted", role: "status", children: "Setting up your access\u2026" });
|
|
3219
|
+
}
|
|
3220
|
+
var pendingText = (p) => p.removal ? p.state === "ready" ? "Removal ready" : p.state === "failed" ? "Removal needs fixing" : "Removing\u2026" : p.state === "ready" ? "Ready to publish" : p.state === "failed" ? "Needs fixing" : "Checking\u2026";
|
|
3221
|
+
var pendingLabel = { checking: "Checking\u2026", ready: "Ready to publish", failed: "Needs fixing" };
|
|
3222
|
+
function StudioDashboard({ greeting, cards }) {
|
|
3223
|
+
const { contentTypes, siteName } = useStudio();
|
|
3224
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3225
|
+
/* @__PURE__ */ jsxRuntime.jsx(PageHeader, { eyebrow: siteName, title: `Welcome back, ${greeting}`, subtitle: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3226
|
+
"Nothing goes live by accident: every edit is saved and ",
|
|
3227
|
+
/* @__PURE__ */ jsxRuntime.jsx(Jargon, { term: "pullRequest", children: "checked first" }),
|
|
3228
|
+
", then you press Publish."
|
|
3229
|
+
] }) }),
|
|
3230
|
+
cards.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(EmptyState, { title: "No content types yet", children: [
|
|
3231
|
+
"Content types are defined in ",
|
|
3232
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { children: "src/lib/admin/content-types.ts" }),
|
|
3233
|
+
"."
|
|
3234
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-grid", children: cards.map((c) => {
|
|
3235
|
+
const e = contentTypes[c.typeId];
|
|
3236
|
+
if (!e) return null;
|
|
3237
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3238
|
+
Card,
|
|
3239
|
+
{
|
|
3240
|
+
title: e.label,
|
|
3241
|
+
meta: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3242
|
+
c.count,
|
|
3243
|
+
" ",
|
|
3244
|
+
c.count === 1 ? "item" : "items",
|
|
3245
|
+
" \xB7 ",
|
|
3246
|
+
e.folder
|
|
3247
|
+
] }),
|
|
3248
|
+
note: c.waiting > 0 && /* @__PURE__ */ jsxRuntime.jsx(Link3__default.default, { href: `/admin/${e.id}`, className: "rz-status-link", children: /* @__PURE__ */ jsxRuntime.jsxs(StatusPill, { tone: "warn", children: [
|
|
3249
|
+
c.waiting,
|
|
3250
|
+
" ",
|
|
3251
|
+
c.waiting === 1 ? "change" : "changes",
|
|
3252
|
+
" waiting"
|
|
3253
|
+
] }) }),
|
|
3254
|
+
actions: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3255
|
+
/* @__PURE__ */ jsxRuntime.jsx(LinkButton, { variant: "primary", href: `/admin/${e.id}/new`, children: "New" }),
|
|
3256
|
+
/* @__PURE__ */ jsxRuntime.jsx(LinkButton, { variant: "secondary", href: `/admin/${e.id}`, children: "Open" })
|
|
3257
|
+
] })
|
|
3258
|
+
},
|
|
3259
|
+
e.id
|
|
3260
|
+
);
|
|
3261
|
+
}) })
|
|
3262
|
+
] });
|
|
3263
|
+
}
|
|
3264
|
+
function StudioList({ typeId, items }) {
|
|
3265
|
+
const entry = useEntry(typeId);
|
|
3266
|
+
if (!entry) return null;
|
|
3267
|
+
const copy = copyFor(entry);
|
|
3268
|
+
const dateHeading = entry.id === "posts" ? "Published" : "Date";
|
|
3269
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3270
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3271
|
+
PageHeader,
|
|
3272
|
+
{
|
|
3273
|
+
eyebrow: entry.folder,
|
|
3274
|
+
title: entry.label,
|
|
3275
|
+
subtitle: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3276
|
+
"Everything in this list comes straight from ",
|
|
3277
|
+
/* @__PURE__ */ jsxRuntime.jsx(Jargon, { term: "repository", children: "where the site is stored" }),
|
|
3278
|
+
" \u2014 including anything added outside Studio."
|
|
3279
|
+
] }),
|
|
3280
|
+
actions: /* @__PURE__ */ jsxRuntime.jsx(LinkButton, { variant: "primary", href: `/admin/${entry.id}/new`, children: copy.newItem })
|
|
3281
|
+
}
|
|
3282
|
+
),
|
|
3283
|
+
items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { title: copy.empty, action: /* @__PURE__ */ jsxRuntime.jsx(LinkButton, { variant: "primary", href: `/admin/${entry.id}/new`, children: copy.first }) }) : /* @__PURE__ */ jsxRuntime.jsxs(SlugVisibility, { typeId: entry.id, children: [
|
|
3284
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-toolbar", children: /* @__PURE__ */ jsxRuntime.jsx(SlugToggle, { typeId: entry.id }) }),
|
|
3285
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-table-wrap", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "rz-table rz-table--posts", children: [
|
|
3286
|
+
/* @__PURE__ */ jsxRuntime.jsxs("colgroup", { children: [
|
|
3287
|
+
/* @__PURE__ */ jsxRuntime.jsx("col", { className: "rz-col-title" }),
|
|
3288
|
+
/* @__PURE__ */ jsxRuntime.jsx("col", { className: "rz-col-date" }),
|
|
3289
|
+
/* @__PURE__ */ jsxRuntime.jsx("col", { className: "rz-col-status" }),
|
|
3290
|
+
/* @__PURE__ */ jsxRuntime.jsx("col", { className: "rz-col-actions" })
|
|
3291
|
+
] }),
|
|
3292
|
+
/* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
|
|
3293
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { children: entry.singular ?? entry.label }),
|
|
3294
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "rz-fit", children: dateHeading }),
|
|
3295
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "rz-fit", children: "Status" }),
|
|
3296
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "rz-fit" })
|
|
3297
|
+
] }) }),
|
|
3298
|
+
/* @__PURE__ */ jsxRuntime.jsx("tbody", { children: items.map((p) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
|
|
3299
|
+
/* @__PURE__ */ jsxRuntime.jsxs("td", { className: "rz-title-cell", children: [
|
|
3300
|
+
p.isNew || !p.liveUrl ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-title-text", title: p.title, children: p.title }) : /* @__PURE__ */ jsxRuntime.jsxs("a", { href: p.liveUrl, target: "_blank", rel: "noreferrer", className: "rz-title-link", title: `${p.title} \u2014 opens the live page in a new tab`, children: [
|
|
3301
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-title-text", children: p.title }),
|
|
3302
|
+
/* @__PURE__ */ jsxRuntime.jsx(OutboundArrow, {})
|
|
3303
|
+
] }),
|
|
3304
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-muted rz-title-text rz-slug", title: p.liveUrl ?? p.slug, children: p.liveUrl ?? p.slug }),
|
|
3305
|
+
p.needsAttention && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginTop: 4 }, children: /* @__PURE__ */ jsxRuntime.jsx(StatusPill, { tone: "warn", children: "Needs attention" }) })
|
|
3306
|
+
] }),
|
|
3307
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "rz-muted rz-fit", children: p.date ? p.date.slice(0, 10) : "\u2014" }),
|
|
3308
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "rz-fit", children: p.pending ? /* @__PURE__ */ jsxRuntime.jsx(Link3__default.default, { href: `/admin/pr/${p.pending.number}`, className: "rz-status-link", title: p.isNew ? "Not on the live site yet" : "An unpublished change is waiting", children: /* @__PURE__ */ jsxRuntime.jsx(StatusPill, { tone: p.pending.state === "ready" ? "up" : p.pending.state === "failed" ? "down" : "warn", children: p.isNew ? `New \xB7 ${pendingText(p.pending)}` : pendingText(p.pending) }) }) : /* @__PURE__ */ jsxRuntime.jsx(StatusPill, { tone: "up", children: "Live" }) }),
|
|
3309
|
+
/* @__PURE__ */ jsxRuntime.jsxs("td", { className: "rz-fit", children: [
|
|
3310
|
+
/* @__PURE__ */ jsxRuntime.jsx(Link3__default.default, { href: `/admin/${entry.id}/${p.slug}`, children: "Edit" }),
|
|
3311
|
+
p.pending && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3312
|
+
" \xB7 ",
|
|
3313
|
+
/* @__PURE__ */ jsxRuntime.jsx(Link3__default.default, { href: `/admin/pr/${p.pending.number}`, children: p.pending.state === "ready" ? "Publish" : "Review" })
|
|
3314
|
+
] })
|
|
3315
|
+
] })
|
|
3316
|
+
] }, p.slug)) })
|
|
3317
|
+
] }) })
|
|
3318
|
+
] })
|
|
3319
|
+
] });
|
|
3320
|
+
}
|
|
3321
|
+
function StudioNew({ typeId, initialValues }) {
|
|
3322
|
+
const entry = useEntry(typeId);
|
|
3323
|
+
if (!entry) return null;
|
|
3324
|
+
const copy = copyFor(entry);
|
|
3325
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3326
|
+
/* @__PURE__ */ jsxRuntime.jsx(PageHeader, { eyebrow: entry.label, title: copy.newItem, subtitle: copy.newSubtitle }),
|
|
3327
|
+
/* @__PURE__ */ jsxRuntime.jsx(ContentEditor, { typeId: entry.id, isNew: true, initialValues })
|
|
3328
|
+
] });
|
|
3329
|
+
}
|
|
3330
|
+
function StudioEdit({ typeId, slug, frontmatter, body, pending }) {
|
|
3331
|
+
const entry = useEntry(typeId);
|
|
3332
|
+
if (!entry) return null;
|
|
3333
|
+
const copy = copyFor(entry);
|
|
3334
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3335
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3336
|
+
PageHeader,
|
|
3337
|
+
{
|
|
3338
|
+
eyebrow: entry.label,
|
|
3339
|
+
title: String(frontmatter.title ?? slug),
|
|
3340
|
+
subtitle: pending ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3341
|
+
"You're picking up an edit you haven't published yet. Save again and it stays as one ",
|
|
3342
|
+
/* @__PURE__ */ jsxRuntime.jsx(Jargon, { term: "change", children: "change" }),
|
|
3343
|
+
"."
|
|
3344
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3345
|
+
"Saving doesn't touch the live site. Your edit is ",
|
|
3346
|
+
/* @__PURE__ */ jsxRuntime.jsx(Jargon, { term: "checks", children: "checked first" }),
|
|
3347
|
+
", then you decide when to ",
|
|
3348
|
+
/* @__PURE__ */ jsxRuntime.jsx(Jargon, { term: "publish", children: "publish" }),
|
|
3349
|
+
"."
|
|
3350
|
+
] })
|
|
3351
|
+
}
|
|
3352
|
+
),
|
|
3353
|
+
pending && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-notice", role: "status", children: [
|
|
3354
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "rz-notice__text", children: [
|
|
3355
|
+
/* @__PURE__ */ jsxRuntime.jsx(StatusPill, { tone: pending.state === "ready" ? "up" : pending.state === "failed" ? "down" : "warn", children: pendingLabel[pending.state] }),
|
|
3356
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
3357
|
+
/* @__PURE__ */ jsxRuntime.jsxs("strong", { children: [
|
|
3358
|
+
pending.removal ? "Removal" : "Unpublished change",
|
|
3359
|
+
" #",
|
|
3360
|
+
pending.number
|
|
3361
|
+
] }),
|
|
3362
|
+
pending.removal ? pending.state === "ready" ? " \u2014 ready to publish; it stays on the site until you do." : pending.state === "failed" ? ` \u2014 ${pending.failingCheck ?? "something needs fixing"}` : " \u2014 we're checking it now." : pending.state === "failed" && pending.failingCheck ? ` \u2014 ${pending.failingCheck}` : pending.state === "ready" ? " \u2014 checks passed, nothing is live until you publish." : " \u2014 we're checking it now."
|
|
3363
|
+
] })
|
|
3364
|
+
] }),
|
|
3365
|
+
/* @__PURE__ */ jsxRuntime.jsx(LinkButton, { variant: "primary", size: "sm", forward: true, href: `/admin/pr/${pending.number}`, children: pending.removal ? "Review removal" : pending.state === "ready" ? "Review & publish" : "See progress" })
|
|
3366
|
+
] }),
|
|
3367
|
+
pending?.removal ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "rz-muted", children: copy.removing }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
3368
|
+
ContentEditor,
|
|
3369
|
+
{
|
|
3370
|
+
typeId: entry.id,
|
|
3371
|
+
initialValues: frontmatter,
|
|
3372
|
+
initialBody: body,
|
|
3373
|
+
existing: pending ? { number: pending.number, branch: pending.branch, state: pending.state } : null,
|
|
3374
|
+
footer: !pending ? /* @__PURE__ */ jsxRuntime.jsx(RemoveItem, { typeId: entry.id, slug }) : void 0
|
|
3375
|
+
}
|
|
3376
|
+
)
|
|
3377
|
+
] });
|
|
3378
|
+
}
|
|
3379
|
+
function StudioPullRequest({ pr, status, item }) {
|
|
3380
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3381
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3382
|
+
PageHeader,
|
|
3383
|
+
{
|
|
3384
|
+
eyebrow: pr.removal ? pr.merged ? "Removed" : "Removing" : pr.merged ? "Published" : "Publishing",
|
|
3385
|
+
title: pr.title.replace(/^(Publish|Remove):\s*/, ""),
|
|
3386
|
+
subtitle: item ? `Changes to ${item.liveUrl ?? `${item.label} \xB7 ${item.slug}`}` : void 0
|
|
3387
|
+
}
|
|
3388
|
+
),
|
|
3389
|
+
/* @__PURE__ */ jsxRuntime.jsx(Card, { className: "rz-card--narrow", children: /* @__PURE__ */ jsxRuntime.jsx(PublishPanel, { number: pr.number, htmlUrl: pr.html_url, merged: pr.merged, closed: pr.state === "closed" && !pr.merged, branch: pr.branch, removal: pr.removal, mergedSha: pr.mergeCommitSha, status, item }) })
|
|
3390
|
+
] });
|
|
3391
|
+
}
|
|
3392
|
+
function StudioHelp() {
|
|
3393
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3394
|
+
/* @__PURE__ */ jsxRuntime.jsx(PageHeader, { eyebrow: "Help", title: "Help centre", subtitle: "Everything about running your website from Studio, in plain English." }),
|
|
3395
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Card, { className: "rz-card--narrow", children: [
|
|
3396
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { style: { marginTop: 0 }, children: [
|
|
3397
|
+
"The full guide is on its way. In the meantime the ",
|
|
3398
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { children: "Ask" }),
|
|
3399
|
+
" bubble in the bottom-right corner knows how Studio works \u2014 ask it anything, from \u201Cwhat does Publish do?\u201D to \u201Chow do I add a picture?\u201D."
|
|
3400
|
+
] }),
|
|
3401
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { style: { marginBottom: 4 }, children: "Coming here soon:" }),
|
|
3402
|
+
/* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "acct-help-list", children: [
|
|
3403
|
+
/* @__PURE__ */ jsxRuntime.jsx("li", { children: "Writing and editing a blog post \u2014 every field explained" }),
|
|
3404
|
+
/* @__PURE__ */ jsxRuntime.jsx("li", { children: "Pictures: choosing, sizing and where they go" }),
|
|
3405
|
+
/* @__PURE__ */ jsxRuntime.jsx("li", { children: "Save, checks, preview and Publish \u2014 the whole journey" }),
|
|
3406
|
+
/* @__PURE__ */ jsxRuntime.jsx("li", { children: "Fixing a change that \u201Cneeds fixing\u201D, discarding, removing a post" }),
|
|
3407
|
+
/* @__PURE__ */ jsxRuntime.jsx("li", { children: "Your account, your team and putting your own name on Studio" })
|
|
3408
|
+
] })
|
|
3409
|
+
] })
|
|
3410
|
+
] });
|
|
3411
|
+
}
|
|
3412
|
+
function StudioSignIn({ url, anonKey, redirectTo, siteName, error }) {
|
|
3413
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-card", style: { maxWidth: 440, margin: "40px auto" }, children: [
|
|
3414
|
+
error === "link" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-alert admin__alert", role: "alert", children: "That sign-in link didn't work \u2014 it may have expired or been used already. Request a new one below." }),
|
|
3415
|
+
/* @__PURE__ */ jsxRuntime.jsx(MagicLinkForm, { url, anonKey, redirectTo, siteName: `${siteName} Studio` }),
|
|
3416
|
+
/* @__PURE__ */ jsxRuntime.jsx(PasswordSignIn, { url, anonKey })
|
|
3417
|
+
] });
|
|
3418
|
+
}
|
|
3419
|
+
function StudioAcceptInvite({ url, anonKey }) {
|
|
3420
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rz-card", style: { maxWidth: 440, margin: "40px auto" }, children: [
|
|
3421
|
+
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: "acct-h1", children: "Welcome to Studio" }),
|
|
3422
|
+
/* @__PURE__ */ jsxRuntime.jsx(AcceptInvite, { url, anonKey })
|
|
3423
|
+
] });
|
|
3424
|
+
}
|
|
3425
|
+
function PictureUploader({ kind, shape, currentUrl, fallback, maxWidth, hint, onUpload, onRemove, onChanged }) {
|
|
3426
|
+
const input = react.useRef(null);
|
|
3427
|
+
const [url, setUrl] = react.useState(currentUrl);
|
|
3428
|
+
const [busy, setBusy] = react.useState(false);
|
|
3429
|
+
const [note, setNote] = react.useState(null);
|
|
3430
|
+
const [over, setOver] = react.useState(false);
|
|
3431
|
+
const choose = async (file) => {
|
|
3432
|
+
if (!file || busy) return;
|
|
3433
|
+
setBusy(true);
|
|
3434
|
+
setNote(null);
|
|
3435
|
+
try {
|
|
3436
|
+
const r = await resizeImage(file, { maxWidth, maxBytes: 300 * 1024 });
|
|
3437
|
+
const preview = URL.createObjectURL(r.blob);
|
|
3438
|
+
const res = await onUpload(await toBase642(r.blob));
|
|
3439
|
+
if (res.ok) {
|
|
3440
|
+
setUrl(preview);
|
|
3441
|
+
setNote({ ok: true, text: res.message ?? "Saved." });
|
|
3442
|
+
onChanged?.();
|
|
3443
|
+
} else setNote({ ok: false, text: res.error });
|
|
3444
|
+
} catch (err) {
|
|
3445
|
+
setNote({ ok: false, text: err instanceof Error ? err.message : "That picture couldn\u2019t be prepared. Try a different file." });
|
|
3446
|
+
} finally {
|
|
3447
|
+
setBusy(false);
|
|
3448
|
+
if (input.current) input.current.value = "";
|
|
3449
|
+
}
|
|
3450
|
+
};
|
|
3451
|
+
const remove = async () => {
|
|
3452
|
+
if (busy) return;
|
|
3453
|
+
setBusy(true);
|
|
3454
|
+
setNote(null);
|
|
3455
|
+
const res = await onRemove();
|
|
3456
|
+
if (res.ok) {
|
|
3457
|
+
setUrl(null);
|
|
3458
|
+
setNote({ ok: true, text: res.message ?? "Removed." });
|
|
3459
|
+
onChanged?.();
|
|
3460
|
+
} else setNote({ ok: false, text: res.error });
|
|
3461
|
+
setBusy(false);
|
|
3462
|
+
};
|
|
3463
|
+
const onDrop = (e) => {
|
|
3464
|
+
e.preventDefault();
|
|
3465
|
+
setOver(false);
|
|
3466
|
+
void choose(e.dataTransfer.files?.[0]);
|
|
3467
|
+
};
|
|
3468
|
+
const onPaste = (e) => {
|
|
3469
|
+
const f = Array.from(e.clipboardData.files ?? [])[0];
|
|
3470
|
+
if (f) {
|
|
3471
|
+
e.preventDefault();
|
|
3472
|
+
void choose(f);
|
|
3473
|
+
}
|
|
3474
|
+
};
|
|
3475
|
+
const zoneCls = ["acct-pic", over && "acct-pic--over", busy && "acct-pic--busy"].filter(Boolean).join(" ");
|
|
3476
|
+
const thumbCls = shape === "circle" ? "acct-pic__thumb acct-pic__thumb--circle" : "acct-pic__thumb";
|
|
3477
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3478
|
+
"div",
|
|
3479
|
+
{
|
|
3480
|
+
className: zoneCls,
|
|
3481
|
+
"data-testid": `${kind}-zone`,
|
|
3482
|
+
onDragOver: (e) => {
|
|
3483
|
+
e.preventDefault();
|
|
3484
|
+
setOver(true);
|
|
3485
|
+
},
|
|
3486
|
+
onDragLeave: () => setOver(false),
|
|
3487
|
+
onDrop,
|
|
3488
|
+
onPaste,
|
|
3489
|
+
tabIndex: 0,
|
|
3490
|
+
children: [
|
|
3491
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { ref: input, type: "file", accept: "image/jpeg,image/png,image/webp", className: "acct-pic__file", onChange: (e) => void choose(e.target.files?.[0]), tabIndex: -1, "aria-hidden": "true" }),
|
|
3492
|
+
url ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct-pic__has", children: [
|
|
3493
|
+
/* @__PURE__ */ jsxRuntime.jsx("img", { src: url, alt: "", className: thumbCls, "data-testid": `${kind}-preview` }),
|
|
3494
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct-pic__meta", children: [
|
|
3495
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct-pic__actions", children: [
|
|
3496
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { onClick: () => input.current?.click(), disabled: busy, children: busy ? "Uploading\u2026" : "Replace" }),
|
|
3497
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "acct-pic__remove", onClick: remove, disabled: busy, children: "Remove" })
|
|
3498
|
+
] }),
|
|
3499
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "acct-pic__hint", children: hint })
|
|
3500
|
+
] })
|
|
3501
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3502
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct-pic__empty", children: [
|
|
3503
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: shape === "circle" ? "acct-pic__fallback acct-pic__fallback--circle" : "acct-pic__fallback", "aria-hidden": "true", children: fallback }),
|
|
3504
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { onClick: () => input.current?.click(), disabled: busy, children: busy ? "Uploading\u2026" : "Choose a picture" }),
|
|
3505
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "acct-pic__or", children: "or drag one here" })
|
|
3506
|
+
] }),
|
|
3507
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "acct-pic__hint", children: hint })
|
|
3508
|
+
] }),
|
|
3509
|
+
note && /* @__PURE__ */ jsxRuntime.jsx("p", { className: note.ok ? "acct-ok acct-pic__note" : "acct-pic__error", role: note.ok ? "status" : "alert", children: note.text })
|
|
3510
|
+
]
|
|
3511
|
+
}
|
|
3512
|
+
);
|
|
3513
|
+
}
|
|
3514
|
+
function initialsOf2(name, email) {
|
|
3515
|
+
const parts = (name ?? "").trim().split(/\s+/).filter(Boolean);
|
|
3516
|
+
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
3517
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
3518
|
+
return (email[0] ?? "?").toUpperCase();
|
|
3519
|
+
}
|
|
3520
|
+
function ProfileTab({ email, name, displayName, avatarUrl }) {
|
|
3521
|
+
const router = navigation.useRouter();
|
|
3522
|
+
const { saveAvatar, saveProfile } = useStudio().actions;
|
|
3523
|
+
const { supportName } = useStudio();
|
|
3524
|
+
const [fullName, setFullName] = react.useState(name ?? "");
|
|
3525
|
+
const [byline, setByline] = react.useState(displayName && displayName !== name ? displayName : "");
|
|
3526
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct-stack", children: [
|
|
3527
|
+
/* @__PURE__ */ jsxRuntime.jsxs("section", { className: "rz-card acct-card", children: [
|
|
3528
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "acct-title", children: "Avatar" }),
|
|
3529
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3530
|
+
PictureUploader,
|
|
3531
|
+
{
|
|
3532
|
+
kind: "photo",
|
|
3533
|
+
shape: "circle",
|
|
3534
|
+
currentUrl: avatarUrl,
|
|
3535
|
+
fallback: initialsOf2(name, email),
|
|
3536
|
+
maxWidth: 256,
|
|
3537
|
+
hint: "Square works best. JPG, PNG or WebP.",
|
|
3538
|
+
onUpload: (base64) => saveAvatar({ base64 }),
|
|
3539
|
+
onRemove: () => saveAvatar(null),
|
|
3540
|
+
onChanged: () => router.refresh()
|
|
3541
|
+
}
|
|
3542
|
+
)
|
|
3543
|
+
] }),
|
|
3544
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3545
|
+
FormCard,
|
|
3546
|
+
{
|
|
3547
|
+
title: "Your name",
|
|
3548
|
+
blurb: "How you're greeted in Studio and how teammates see you.",
|
|
3549
|
+
action: async () => {
|
|
3550
|
+
const r = await saveProfile({ name: fullName, displayName: byline });
|
|
3551
|
+
if (r.ok) router.refresh();
|
|
3552
|
+
return r;
|
|
3553
|
+
},
|
|
3554
|
+
children: [
|
|
3555
|
+
/* @__PURE__ */ jsxRuntime.jsx(Field2, { id: "acct-name", label: "Full name", children: /* @__PURE__ */ jsxRuntime.jsx("input", { id: "acct-name", className: "acct-input", value: fullName, onChange: (e) => setFullName(e.target.value), maxLength: 80, autoComplete: "name" }) }),
|
|
3556
|
+
/* @__PURE__ */ jsxRuntime.jsx(Field2, { id: "acct-byline", label: "Name on blog posts", hint: "How your name appears as the author of a post. Leave blank to use your full name.", children: /* @__PURE__ */ jsxRuntime.jsx("input", { id: "acct-byline", className: "acct-input", value: byline, onChange: (e) => setByline(e.target.value), maxLength: 80, placeholder: fullName || "e.g. E. Bordi" }) }),
|
|
3557
|
+
/* @__PURE__ */ jsxRuntime.jsx(Field2, { id: "acct-email", label: "Email", hint: `This is your login. Ask ${supportName} if it needs to change.`, children: /* @__PURE__ */ jsxRuntime.jsx("input", { id: "acct-email", className: "acct-input", value: email, readOnly: true }) })
|
|
3558
|
+
]
|
|
3559
|
+
}
|
|
3560
|
+
)
|
|
3561
|
+
] });
|
|
3562
|
+
}
|
|
3563
|
+
var RULES = [
|
|
3564
|
+
{ id: "length", label: "At least 10 characters", test: (pw) => pw.length >= 10 },
|
|
3565
|
+
{ id: "case", label: "Uppercase and lowercase letters", test: (pw) => /[A-Z]/.test(pw) && /[a-z]/.test(pw) },
|
|
3566
|
+
{ id: "number", label: "At least one number", test: (pw) => /[0-9]/.test(pw) },
|
|
3567
|
+
{ id: "special", label: "At least one special character (e.g. !@#$)", test: (pw) => /[^A-Za-z0-9]/.test(pw) },
|
|
3568
|
+
{ id: "match", label: "Passwords match", test: (pw, confirm) => pw === confirm }
|
|
3569
|
+
];
|
|
3570
|
+
function PasswordTab() {
|
|
3571
|
+
const { changePassword } = useStudio().actions;
|
|
3572
|
+
const [password, setPassword] = react.useState("");
|
|
3573
|
+
const [confirm, setConfirm] = react.useState("");
|
|
3574
|
+
const met = (r) => password.length > 0 && r.test(password, confirm);
|
|
3575
|
+
const allMet = RULES.every(met);
|
|
3576
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "acct-stack", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3577
|
+
FormCard,
|
|
3578
|
+
{
|
|
3579
|
+
title: "Set a password",
|
|
3580
|
+
blurb: "Email links always work. A password is a second way in, handy when you're somewhere without your inbox.",
|
|
3581
|
+
saveLabel: "Update password",
|
|
3582
|
+
disabled: !allMet,
|
|
3583
|
+
action: async () => {
|
|
3584
|
+
const r = await changePassword({ password, confirm });
|
|
3585
|
+
if (r.ok) {
|
|
3586
|
+
setPassword("");
|
|
3587
|
+
setConfirm("");
|
|
3588
|
+
}
|
|
3589
|
+
return r;
|
|
3590
|
+
},
|
|
3591
|
+
children: [
|
|
3592
|
+
/* @__PURE__ */ jsxRuntime.jsx(Field2, { id: "acct-pw", label: "New password", children: /* @__PURE__ */ jsxRuntime.jsx(PasswordInput, { id: "acct-pw", value: password, onChange: setPassword, autoComplete: "new-password" }) }),
|
|
3593
|
+
/* @__PURE__ */ jsxRuntime.jsx(Field2, { id: "acct-pw2", label: "Confirm new password", children: /* @__PURE__ */ jsxRuntime.jsx(PasswordInput, { id: "acct-pw2", value: confirm, onChange: setConfirm, autoComplete: "new-password" }) }),
|
|
3594
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { className: "acct-rules", "aria-label": "Password requirements", children: RULES.map((r) => {
|
|
3595
|
+
const ok = met(r);
|
|
3596
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: ok ? "acct-rule acct-rule--met" : "acct-rule", "data-met": ok, children: [
|
|
3597
|
+
/* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", "aria-hidden": "true", children: ok ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3598
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10", fill: "var(--signal)" }),
|
|
3599
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7 12.5l3.2 3.2L17 9", fill: "none", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round" })
|
|
3600
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10", fill: "none", stroke: "var(--text-muted)", strokeWidth: "2" }) }),
|
|
3601
|
+
r.label
|
|
3602
|
+
] }, r.id);
|
|
3603
|
+
}) })
|
|
3604
|
+
]
|
|
3605
|
+
}
|
|
3606
|
+
) });
|
|
3607
|
+
}
|
|
3608
|
+
function initials(m) {
|
|
3609
|
+
const parts = (m.name ?? "").trim().split(/\s+/).filter(Boolean);
|
|
3610
|
+
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
3611
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
3612
|
+
return (m.email[0] ?? "?").toUpperCase();
|
|
3613
|
+
}
|
|
3614
|
+
function TeamTab({ members, canInvite, myUserId, myRole }) {
|
|
3615
|
+
const router = navigation.useRouter();
|
|
3616
|
+
const { changeRole, invite, remove, resend } = useStudio().actions;
|
|
3617
|
+
const { supportName } = useStudio();
|
|
3618
|
+
const [pending, start] = react.useTransition();
|
|
3619
|
+
const [result, setResult] = react.useState(null);
|
|
3620
|
+
const [inviting, setInviting] = react.useState(false);
|
|
3621
|
+
const [email, setEmail] = react.useState("");
|
|
3622
|
+
const [role, setRole] = react.useState("editor");
|
|
3623
|
+
const [confirmRemove, setConfirmRemove] = react.useState(null);
|
|
3624
|
+
const run = (fn) => start(async () => {
|
|
3625
|
+
setResult(null);
|
|
3626
|
+
const r = await fn();
|
|
3627
|
+
setResult(r);
|
|
3628
|
+
if (r.ok) router.refresh();
|
|
3629
|
+
});
|
|
3630
|
+
const sendInvite = (e) => {
|
|
3631
|
+
e.preventDefault();
|
|
3632
|
+
run(async () => {
|
|
3633
|
+
const r = await invite({ email, role });
|
|
3634
|
+
if (r.ok) {
|
|
3635
|
+
setEmail("");
|
|
3636
|
+
setInviting(false);
|
|
3637
|
+
}
|
|
3638
|
+
return r;
|
|
3639
|
+
});
|
|
3640
|
+
};
|
|
3641
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct-stack", children: [
|
|
3642
|
+
!canInvite && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-alert admin__alert", children: "Inviting people isn\u2019t set up on this deployment yet." }),
|
|
3643
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct-team-head", children: [
|
|
3644
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "acct-blurb", style: { margin: 0 }, children: [
|
|
3645
|
+
"Everyone here can sign in and publish. ",
|
|
3646
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { children: "Owners" }),
|
|
3647
|
+
" can also manage this list and the business details; ",
|
|
3648
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { children: "editors" }),
|
|
3649
|
+
" just write and publish."
|
|
3650
|
+
] }),
|
|
3651
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", onClick: () => setInviting((v) => !v), disabled: !canInvite || pending, children: "+ Invite someone" })
|
|
3652
|
+
] }),
|
|
3653
|
+
inviting && /* @__PURE__ */ jsxRuntime.jsxs("form", { className: "rz-card acct-card acct-invite", onSubmit: sendInvite, children: [
|
|
3654
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "acct-title", children: "Invite someone" }),
|
|
3655
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "acct-blurb", children: "They get an email with a sign-in link. No password to set up." }),
|
|
3656
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct-invite__row", children: [
|
|
3657
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { className: "acct-input", type: "email", placeholder: "name@company.com", value: email, onChange: (e) => setEmail(e.target.value), required: true, autoFocus: true }),
|
|
3658
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3659
|
+
Dropdown,
|
|
3660
|
+
{
|
|
3661
|
+
label: "Role",
|
|
3662
|
+
value: role,
|
|
3663
|
+
onChange: setRole,
|
|
3664
|
+
options: [
|
|
3665
|
+
{ value: "editor", label: "Editor", hint: "Writes and publishes" },
|
|
3666
|
+
{ value: "owner", label: "Owner", hint: "Also manages the team" }
|
|
3667
|
+
]
|
|
3668
|
+
}
|
|
3669
|
+
),
|
|
3670
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { type: "submit", variant: "primary", disabled: pending, children: pending ? "Sending\u2026" : "Send invitation" }),
|
|
3671
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { type: "button", onClick: () => setInviting(false), children: "Cancel" })
|
|
3672
|
+
] })
|
|
3673
|
+
] }),
|
|
3674
|
+
/* @__PURE__ */ jsxRuntime.jsx(Result, { r: result }),
|
|
3675
|
+
myRole === "staff" && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "acct-blurb", style: { margin: 0 }, children: [
|
|
3676
|
+
"You\u2019re signed in as ",
|
|
3677
|
+
/* @__PURE__ */ jsxRuntime.jsxs("b", { children: [
|
|
3678
|
+
supportName,
|
|
3679
|
+
" staff"
|
|
3680
|
+
] }),
|
|
3681
|
+
", so you\u2019re not listed here \u2014 staff can reach every site without being a member of it. The site\u2019s own owners and editors appear below."
|
|
3682
|
+
] }),
|
|
3683
|
+
members.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(EmptyState, { title: "Nobody on this site\u2019s team yet", action: canInvite ? /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", onClick: () => setInviting(true), children: "Invite the site owner" }) : void 0, children: [
|
|
3684
|
+
"Invite the person who owns this website as an ",
|
|
3685
|
+
/* @__PURE__ */ jsxRuntime.jsx("b", { children: "Owner" }),
|
|
3686
|
+
"; they can bring in editors themselves."
|
|
3687
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-table-wrap", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "rz-table acct-team", children: [
|
|
3688
|
+
/* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
|
|
3689
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { children: "Person" }),
|
|
3690
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "rz-fit", children: "Role" }),
|
|
3691
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "rz-fit", children: "Status" }),
|
|
3692
|
+
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "rz-fit" })
|
|
3693
|
+
] }) }),
|
|
3694
|
+
/* @__PURE__ */ jsxRuntime.jsx("tbody", { children: members.map((m) => {
|
|
3695
|
+
const me = m.userId === myUserId;
|
|
3696
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("tr", { style: { opacity: m.status === "invited" ? 0.8 : 1 }, children: [
|
|
3697
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct-person", children: [
|
|
3698
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "acct-person__avatar", children: m.avatarUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: m.avatarUrl, alt: "" }) : m.status === "invited" ? "\u2014" : initials(m) }),
|
|
3699
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "acct-person__text", children: [
|
|
3700
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "acct-person__name", children: [
|
|
3701
|
+
m.name ?? m.email,
|
|
3702
|
+
me && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rz-muted", children: " (you)" })
|
|
3703
|
+
] }),
|
|
3704
|
+
m.name && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "acct-person__email rz-muted", children: m.email })
|
|
3705
|
+
] })
|
|
3706
|
+
] }) }),
|
|
3707
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "rz-fit", children: m.status === "active" && !me && canInvite ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3708
|
+
Dropdown,
|
|
3709
|
+
{
|
|
3710
|
+
label: `Role for ${m.email}`,
|
|
3711
|
+
value: m.role,
|
|
3712
|
+
disabled: pending,
|
|
3713
|
+
options: [
|
|
3714
|
+
{ value: "editor", label: "Editor", hint: "Writes and publishes" },
|
|
3715
|
+
{ value: "owner", label: "Owner", hint: "Also manages the team" }
|
|
3716
|
+
],
|
|
3717
|
+
onChange: (role2) => run(() => changeRole({ userId: m.userId, role: role2 }))
|
|
3718
|
+
}
|
|
3719
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(StatusPill, { tone: "plain", children: m.role }) }),
|
|
3720
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "rz-fit", children: /* @__PURE__ */ jsxRuntime.jsx(StatusPill, { tone: m.status === "active" ? "up" : "warn", children: m.status === "active" ? "Active" : "Pending invite" }) }),
|
|
3721
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "rz-fit", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "acct-row-actions", children: me || !canInvite ? null : m.status === "invited" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3722
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "acct-link", disabled: pending, onClick: () => run(() => resend({ email: m.email })), children: "Resend" }),
|
|
3723
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "acct-link acct-link--danger", disabled: pending, onClick: () => run(() => remove({ userId: m.userId })), children: "Revoke" })
|
|
3724
|
+
] }) : confirmRemove === m.userId ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3725
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "rz-muted", children: [
|
|
3726
|
+
"Remove ",
|
|
3727
|
+
m.name ?? m.email,
|
|
3728
|
+
" from this site?"
|
|
3729
|
+
] }),
|
|
3730
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "acct-link acct-link--danger", disabled: pending, onClick: () => {
|
|
3731
|
+
setConfirmRemove(null);
|
|
3732
|
+
run(() => remove({ userId: m.userId }));
|
|
3733
|
+
}, children: "Yes, remove" }),
|
|
3734
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "acct-link", onClick: () => setConfirmRemove(null), children: "Keep" })
|
|
3735
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "acct-link acct-link--danger", disabled: pending, onClick: () => setConfirmRemove(m.userId), children: "Remove from this site" }) }) })
|
|
3736
|
+
] }, m.userId);
|
|
3737
|
+
}) })
|
|
3738
|
+
] }) })
|
|
3739
|
+
] });
|
|
3740
|
+
}
|
|
3741
|
+
var ImageIcon = () => /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
3742
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
|
|
3743
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "10", r: "1.6" }),
|
|
3744
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 16l-5-5-8 8" })
|
|
3745
|
+
] });
|
|
3746
|
+
function BusinessTab({ initial }) {
|
|
3747
|
+
const router = navigation.useRouter();
|
|
3748
|
+
const { saveBranding, saveBrandingPicture } = useStudio().actions;
|
|
3749
|
+
const [name, setName] = react.useState(initial?.name ?? "");
|
|
3750
|
+
const [email, setEmail] = react.useState(initial?.businessEmail ?? "");
|
|
3751
|
+
const [phone, setPhone] = react.useState(formatPhone(initial?.businessPhone ?? ""));
|
|
3752
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct-stack", children: [
|
|
3753
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3754
|
+
FormCard,
|
|
3755
|
+
{
|
|
3756
|
+
title: "Your business",
|
|
3757
|
+
blurb: "Put your own name and logo on Studio. They show at the top of every page for everyone who signs in to your site.",
|
|
3758
|
+
action: async () => {
|
|
3759
|
+
const r = await saveBranding({ name, businessEmail: email, businessPhone: phone });
|
|
3760
|
+
if (r.ok) router.refresh();
|
|
3761
|
+
return r;
|
|
3762
|
+
},
|
|
3763
|
+
children: [
|
|
3764
|
+
/* @__PURE__ */ jsxRuntime.jsx(Field2, { id: "biz-name", label: "Business name", hint: "Appears next to \u201CStudio\u201D at the top of the page.", children: /* @__PURE__ */ jsxRuntime.jsx("input", { id: "biz-name", className: "acct-input", value: name, onChange: (e) => setName(e.target.value), maxLength: 80, required: true }) }),
|
|
3765
|
+
/* @__PURE__ */ jsxRuntime.jsx(Field2, { id: "biz-email", label: "Business email \u2014 optional", children: /* @__PURE__ */ jsxRuntime.jsx("input", { id: "biz-email", className: "acct-input", type: "email", value: email, onChange: (e) => setEmail(e.target.value), placeholder: "hello@yourbusiness.com" }) }),
|
|
3766
|
+
/* @__PURE__ */ jsxRuntime.jsx(Field2, { id: "biz-phone", label: "Business phone \u2014 optional", children: /* @__PURE__ */ jsxRuntime.jsx("input", { id: "biz-phone", className: "acct-input", type: "tel", inputMode: "tel", autoComplete: "tel-national", value: phone, onChange: (e) => setPhone(formatPhone(e.target.value)), placeholder: "(555) 123-4567", maxLength: 14 }) })
|
|
3767
|
+
]
|
|
3768
|
+
}
|
|
3769
|
+
),
|
|
3770
|
+
/* @__PURE__ */ jsxRuntime.jsxs("section", { className: "rz-card acct-card", children: [
|
|
3771
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "acct-title", children: "Logo" }),
|
|
3772
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3773
|
+
PictureUploader,
|
|
3774
|
+
{
|
|
3775
|
+
kind: "logo",
|
|
3776
|
+
shape: "square",
|
|
3777
|
+
currentUrl: initial?.logoUrl ?? null,
|
|
3778
|
+
fallback: /* @__PURE__ */ jsxRuntime.jsx(ImageIcon, {}),
|
|
3779
|
+
maxWidth: 512,
|
|
3780
|
+
hint: "Shown in the top bar. Square or wide.",
|
|
3781
|
+
onUpload: (base64) => saveBrandingPicture({ kind: "logo", base64 }),
|
|
3782
|
+
onRemove: () => saveBrandingPicture({ kind: "logo", base64: null }),
|
|
3783
|
+
onChanged: () => router.refresh()
|
|
3784
|
+
}
|
|
3785
|
+
)
|
|
3786
|
+
] }),
|
|
3787
|
+
/* @__PURE__ */ jsxRuntime.jsxs("section", { className: "rz-card acct-card", children: [
|
|
3788
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "acct-title", children: "Favicon" }),
|
|
3789
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3790
|
+
PictureUploader,
|
|
3791
|
+
{
|
|
3792
|
+
kind: "favicon",
|
|
3793
|
+
shape: "square",
|
|
3794
|
+
currentUrl: initial?.faviconUrl ?? null,
|
|
3795
|
+
fallback: /* @__PURE__ */ jsxRuntime.jsx(ImageIcon, {}),
|
|
3796
|
+
maxWidth: 64,
|
|
3797
|
+
hint: "The browser-tab icon. Square, simple.",
|
|
3798
|
+
onUpload: (base64) => saveBrandingPicture({ kind: "favicon", base64 }),
|
|
3799
|
+
onRemove: () => saveBrandingPicture({ kind: "favicon", base64: null }),
|
|
3800
|
+
onChanged: () => router.refresh()
|
|
3801
|
+
}
|
|
3802
|
+
)
|
|
3803
|
+
] })
|
|
3804
|
+
] });
|
|
3805
|
+
}
|
|
3806
|
+
var LABELS = { profile: "My Profile", password: "Change Password", team: "Team", business: "Business" };
|
|
3807
|
+
function AccountTabs({ me, manages, team, branding, myUserId, myRole }) {
|
|
3808
|
+
const router = navigation.useRouter();
|
|
3809
|
+
const pathname = navigation.usePathname();
|
|
3810
|
+
const params = navigation.useSearchParams();
|
|
3811
|
+
const ids = manages ? ["profile", "password", "team", "business"] : ["profile", "password"];
|
|
3812
|
+
const requested = params.get("tab");
|
|
3813
|
+
const active = requested && ids.includes(requested) ? requested : "profile";
|
|
3814
|
+
const setActive = (v) => router.replace(`${pathname}?tab=${v}`, { scroll: false });
|
|
3815
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3816
|
+
/* @__PURE__ */ jsxRuntime.jsx(Tabs, { options: ids.map((id) => ({ value: id, label: LABELS[id] })), value: active, onChange: setActive, label: "Account sections" }),
|
|
3817
|
+
active === "profile" && /* @__PURE__ */ jsxRuntime.jsx(ProfileTab, { email: me.email, name: me.name, displayName: me.displayName, avatarUrl: me.avatarUrl }),
|
|
3818
|
+
active === "password" && /* @__PURE__ */ jsxRuntime.jsx(PasswordTab, {}),
|
|
3819
|
+
active === "team" && manages && (team?.ok ? /* @__PURE__ */ jsxRuntime.jsx(TeamTab, { members: team.members, canInvite: team.canInvite, myUserId, myRole }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rz-alert admin__alert", children: team?.error ?? "Couldn\u2019t load the team." })),
|
|
3820
|
+
active === "business" && manages && /* @__PURE__ */ jsxRuntime.jsx(BusinessTab, { initial: branding })
|
|
3821
|
+
] });
|
|
3822
|
+
}
|
|
3823
|
+
function StudioAccount({ me, manages, team, branding, myUserId, myRole }) {
|
|
3824
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "acct", children: [
|
|
3825
|
+
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: "acct-h1", children: "Account" }),
|
|
3826
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "acct-email", children: me.email }),
|
|
3827
|
+
/* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(AccountTabs, { me, manages, team, branding, myUserId, myRole }) })
|
|
3828
|
+
] });
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
// src/studio-ui/facts.ts
|
|
3832
|
+
var STUDIO_FACTS = `
|
|
3833
|
+
HOW REALIIZ STUDIO WORKS (the publishing dashboard at /admin)
|
|
3834
|
+
|
|
3835
|
+
The big picture: your website is stored as files in a private "repository" on GitHub (think: a version-controlled folder in the cloud that keeps every change ever made). Studio is a friendly front end over that folder. You never need to open GitHub \u2014 every action you take in Studio is one of these steps:
|
|
3836
|
+
|
|
3837
|
+
1. SAVE. You edit a post and click "Save and continue" (or "Save changes"). Studio writes your post as a file and opens a "change" \u2014 GitHub calls this a pull request, or PR. Nothing on the live site changes yet. Think of it as a draft submitted for checking.
|
|
3838
|
+
2. CHECKS. The moment a change exists, the site builds a private test copy with your change in it, to make sure nothing breaks (a broken link tag, a missing required field, an image path that doesn't exist). This usually takes a minute or two. The change page shows a tracker: Saved \u2192 Checks \u2192 Published \u2192 Live, and refreshes itself.
|
|
3839
|
+
3. PUBLISH. When the checks pass the status becomes "Ready to publish" and the Publish button unlocks. A "Preview the change" button also appears: it opens a private copy of the website with your edit in it, so you can see exactly how the post will look before anyone else does. Clicking Publish sends your change to the live site.
|
|
3840
|
+
4. LIVE. The website then rebuilds itself with your change \u2014 usually about two minutes. The Live step keeps spinning until the rebuild is really done, and "View the post" unlocks at that moment. If you look at the website before the Live step turns green you will still see the old version; that's normal, not a failure.
|
|
3841
|
+
|
|
3842
|
+
CONTENT TYPES: the sidebar lists every kind of content this site publishes \u2014 Blog posts and Speaking & Media (talks, panels, podcasts; shown at realiiz.com/speaking, upcoming first). Speaking items have a date, optional start/end time and time zone, a kind, a host, a place, links for details, the recording and slides, and \u2014 for Ed's own events \u2014 an RSVP link, capacity, \u201Csign-ups closed\u201D and \u201Cprivate\u201D (invited guests only; the event still shows but has no RSVP button). Each works the same way \u2014 a list, a New button, a form, Save, checks, Publish.
|
|
3843
|
+
|
|
3844
|
+
STATUSES you'll see in each list and on each item:
|
|
3845
|
+
- Live: what's on the website right now, no change waiting.
|
|
3846
|
+
- Checking\u2026: you saved; the test build is running. Wait a minute or two.
|
|
3847
|
+
- Ready to publish: checks passed; open the change and click Publish.
|
|
3848
|
+
- Needs fixing: the test build failed. Nothing was published. Open the post, fix the problem the message names, save again \u2014 the same change re-runs its checks.
|
|
3849
|
+
- New \xB7 \u2026: a brand-new post that exists only in a change; it isn't on the live site until you publish.
|
|
3850
|
+
|
|
3851
|
+
RULES OF THUMB:
|
|
3852
|
+
- One open change per post. Saving again while a change is waiting updates that same change (it doesn't create a second one). The sticky bar at the bottom of the editor tells you which change you're updating.
|
|
3853
|
+
- You can leave and come back: the posts table's Status column links to the change, and the post page shows a notice with a Publish button when a change is waiting.
|
|
3854
|
+
- Changed your mind? On the change page, "Discard this change" (with a confirm step) throws the unpublished edits away. The live site is untouched and the post goes back to Live.
|
|
3855
|
+
- "View on GitHub" opens the raw change for the curious; you never need it.
|
|
3856
|
+
- Only signed-in editors and owners can save or publish. Sign-in is by email link (no password).
|
|
3857
|
+
- If two people edit the same post, the second to publish is told the file changed first and asked to redo their edit on the latest version \u2014 nothing is silently overwritten.
|
|
3858
|
+
|
|
3859
|
+
FIELDS on a blog post:
|
|
3860
|
+
- Title (H1): the headline at the top of the post. Can be long.
|
|
3861
|
+
- Web address (slug): the last part of the post's URL, e.g. my-first-post \u2192 realiiz.com/blog/my-first-post. Lowercase words joined by hyphens.
|
|
3862
|
+
- Excerpt: the short teaser shown on the blog listing page.
|
|
3863
|
+
- Publish date / Last updated: shown on the post and used for ordering; Last updated is optional, for revisions.
|
|
3864
|
+
- Author, Tags (exactly 3, lowercase-hyphenated), Header image (choose or drop a picture \u2014 see PICTURES below), Header image description (alt text for accessibility), Featured (pins the post).
|
|
3865
|
+
- Search & social group: Search title (what Google and the browser tab show \u2014 aim for 50\u201360 characters including the site name; blank means same as Title), Meta description (the snippet under the title in Google \u2014 aim for 120\u2013155 characters), Focus keyword (the phrase the post targets; used for SEO reporting, never shown), Social share image (the picture for social cards; blank reuses the hero image), Canonical (advanced; leave blank unless the article was published elsewhere first).
|
|
3866
|
+
- Post content: Markdown. The toolbar buttons toggle formatting on and off; Preview shows exactly how the blog will render it; the tooltip button wraps a selected term in a definition that appears on hover; Expand fills the window (Esc to come back).
|
|
3867
|
+
- Length gauges under the search fields: the green band is the target range; the fill turns green when you're inside it and red when you're over.
|
|
3868
|
+
|
|
3869
|
+
PICTURES:
|
|
3870
|
+
- Choose a picture (or drag one in, or paste) for the header image, the social share image, or anywhere in the post content via the toolbar's picture button. JPG, PNG or WebP up to 10 MB; Studio shrinks it in your browser to a web-friendly WebP (about 1600 pixels wide, usually 100\u2013400 KB) before sending.
|
|
3871
|
+
- Nothing is uploaded when you pick \u2014 the pictures are saved together with the post when you press Save, into the same change, and go live with it when you Publish. Discarding a change discards its pictures too.
|
|
3872
|
+
- Pictures are named after the post's web address (slug), so fill that in first: the header image becomes /blog/<slug>.webp and pictures in the text go under /blog/<slug>/.
|
|
3873
|
+
- Preview shows new pictures before they're saved. The "Preview the change" link after the checks shows them on the real site design.
|
|
3874
|
+
|
|
3875
|
+
ACCOUNT (avatar menu \u2192 Account, or Settings in the sidebar):
|
|
3876
|
+
- My Profile: your photo, your name (shown in the greeting and to teammates) and \u201CName on blog posts\u201D \u2014 how you're credited as the author; blank means your full name. Your email is your login and can't be changed here \u2014 ask your web team.
|
|
3877
|
+
- Change Password: optional. Email links always work; a password is a second way in. At least 10 characters. On the sign-in page, "Have a password? Sign in with it instead" reveals the password form. Forgot your password? There's nothing to reset \u2014 sign in with an email link and set a new one under Settings \u2192 Change Password.
|
|
3878
|
+
- Team (owners only): who can sign in to this site. "+ Invite someone" sends an email link \u2014 no password needed. Editors write and publish; owners also manage the team and the business details. Pending invites can be resent or revoked; active people can be removed from this site (their login survives for any other site). A site always keeps at least one owner. Support staff aren't listed in a site's team \u2014 they can reach every site without being a member.
|
|
3879
|
+
- Business (owners only): your business name, contact email/phone, logo and favicon. Saving puts your name and logo at the top of Studio for everyone who signs in to your site \u2014 it never changes the public website.
|
|
3880
|
+
- Help (avatar menu): the help centre. For now it points here, to the Ask bubble; a full browsable guide is coming.
|
|
3881
|
+
`;
|
|
3882
|
+
|
|
3883
|
+
// src/studio-ui/types.ts
|
|
3884
|
+
var ACTION_NAMES = [
|
|
3885
|
+
"publishContent",
|
|
3886
|
+
"removeContent",
|
|
3887
|
+
"mergeContentPr",
|
|
3888
|
+
"discardChange",
|
|
3889
|
+
"previewBody",
|
|
3890
|
+
"saveProfile",
|
|
3891
|
+
"saveAvatar",
|
|
3892
|
+
"changePassword",
|
|
3893
|
+
"loadBranding",
|
|
3894
|
+
"saveBranding",
|
|
3895
|
+
"saveBrandingPicture",
|
|
3896
|
+
"loadMe",
|
|
3897
|
+
"loadTeam",
|
|
3898
|
+
"invite",
|
|
3899
|
+
"changeRole",
|
|
3900
|
+
"resend",
|
|
3901
|
+
"remove"
|
|
3902
|
+
];
|
|
3903
|
+
|
|
3904
|
+
exports.ACTION_NAMES = ACTION_NAMES;
|
|
3905
|
+
exports.ContentEditor = ContentEditor;
|
|
3906
|
+
exports.GLOSSARY = GLOSSARY;
|
|
3907
|
+
exports.Jargon = Jargon;
|
|
3908
|
+
exports.PublishPanel = PublishPanel;
|
|
3909
|
+
exports.RemoveItem = RemoveItem;
|
|
3910
|
+
exports.STUDIO_CSS = STUDIO_CSS;
|
|
3911
|
+
exports.STUDIO_FACTS = STUDIO_FACTS;
|
|
3912
|
+
exports.StudioAcceptInvite = StudioAcceptInvite;
|
|
3913
|
+
exports.StudioAccount = StudioAccount;
|
|
3914
|
+
exports.StudioDashboard = StudioDashboard;
|
|
3915
|
+
exports.StudioEdit = StudioEdit;
|
|
3916
|
+
exports.StudioHelp = StudioHelp;
|
|
3917
|
+
exports.StudioList = StudioList;
|
|
3918
|
+
exports.StudioNew = StudioNew;
|
|
3919
|
+
exports.StudioNoAccess = StudioNoAccess;
|
|
3920
|
+
exports.StudioNotConfigured = StudioNotConfigured;
|
|
3921
|
+
exports.StudioProvider = StudioProvider;
|
|
3922
|
+
exports.StudioPullRequest = StudioPullRequest;
|
|
3923
|
+
exports.StudioShellFrame = StudioShellFrame;
|
|
3924
|
+
exports.StudioSignIn = StudioSignIn;
|
|
3925
|
+
exports.StudioTerm = StudioTerm;
|
|
3926
|
+
exports.copyFor = copyFor;
|
|
3927
|
+
exports.publicDirOf = publicDirOf;
|
|
3928
|
+
exports.useEntry = useEntry;
|
|
3929
|
+
exports.useStudio = useStudio;
|
|
3930
|
+
//# sourceMappingURL=index.cjs.map
|
|
3931
|
+
//# sourceMappingURL=index.cjs.map
|