@groveback/ui 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -3
- package/dist/MarkdownField.d.ts +19 -0
- package/dist/SignIn.d.ts +38 -0
- package/dist/components.d.ts +27 -5
- package/dist/index.d.ts +9 -4
- package/dist/index.js +732 -99
- package/dist/routes.d.ts +85 -0
- package/dist/runtime.d.ts +20 -2
- package/dist/styles.d.ts +20 -0
- package/dist/types.d.ts +39 -7
- package/package.json +8 -6
- package/tailwind.css +12 -0
package/dist/index.js
CHANGED
|
@@ -1,9 +1,133 @@
|
|
|
1
1
|
// src/components.tsx
|
|
2
|
-
import { useCallback as useCallback2, useEffect as useEffect2, useState as
|
|
2
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
|
|
3
3
|
|
|
4
|
-
// src/
|
|
5
|
-
import {
|
|
4
|
+
// src/MarkdownField.tsx
|
|
5
|
+
import { useState } from "react";
|
|
6
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
function escapeHtml(s) {
|
|
8
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
9
|
+
}
|
|
10
|
+
function renderInline(s) {
|
|
11
|
+
return escapeHtml(s).replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1</a>');
|
|
12
|
+
}
|
|
13
|
+
function renderMarkdown(src) {
|
|
14
|
+
const lines = src.split(`
|
|
15
|
+
`);
|
|
16
|
+
const html = [];
|
|
17
|
+
let inList = false;
|
|
18
|
+
const closeList = () => {
|
|
19
|
+
if (inList) {
|
|
20
|
+
html.push("</ul>");
|
|
21
|
+
inList = false;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
const heading = /^(#{1,3})\s+(.*)$/.exec(line);
|
|
26
|
+
const item = /^[-*]\s+(.*)$/.exec(line);
|
|
27
|
+
if (heading) {
|
|
28
|
+
closeList();
|
|
29
|
+
const level = heading[1].length;
|
|
30
|
+
html.push(`<h${level}>${renderInline(heading[2])}</h${level}>`);
|
|
31
|
+
} else if (item) {
|
|
32
|
+
if (!inList) {
|
|
33
|
+
html.push("<ul>");
|
|
34
|
+
inList = true;
|
|
35
|
+
}
|
|
36
|
+
html.push(`<li>${renderInline(item[1])}</li>`);
|
|
37
|
+
} else if (line.trim() === "") {
|
|
38
|
+
closeList();
|
|
39
|
+
} else {
|
|
40
|
+
closeList();
|
|
41
|
+
html.push(`<p>${renderInline(line)}</p>`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
closeList();
|
|
45
|
+
return html.join(`
|
|
46
|
+
`);
|
|
47
|
+
}
|
|
48
|
+
var TOOLS = [
|
|
49
|
+
{ key: "b", label: "Bold", glyph: "B", wrap: { before: "**", after: "**", placeholder: "bold" } },
|
|
50
|
+
{ key: "i", label: "Italic", glyph: "I", wrap: { before: "*", after: "*", placeholder: "italic" } },
|
|
51
|
+
{ key: "h", label: "Heading", glyph: "H", wrap: { before: "## ", after: "", placeholder: "Heading" } },
|
|
52
|
+
{ key: "ul", label: "List item", glyph: "•", wrap: { before: "- ", after: "", placeholder: "item" } },
|
|
53
|
+
{ key: "code", label: "Code", glyph: "</>", wrap: { before: "`", after: "`", placeholder: "code" } },
|
|
54
|
+
{ key: "a", label: "Link", glyph: "\uD83D\uDD17", wrap: { before: "[", after: "](https://)", placeholder: "text" } }
|
|
55
|
+
];
|
|
56
|
+
var TOOL_BUTTON = "inline-flex h-7 min-w-7 items-center justify-center rounded px-1.5 text-xs text-neutral-600 " + "hover:bg-neutral-100 hover:text-neutral-900 disabled:opacity-40 " + "dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-100";
|
|
57
|
+
var PREVIEW_TYPOGRAPHY = "min-h-40 px-3 py-2 text-sm [&_a]:underline [&_code]:rounded [&_code]:bg-neutral-100 [&_code]:px-1 " + "[&_h1]:text-lg [&_h1]:font-semibold [&_h2]:text-base [&_h2]:font-semibold [&_h3]:font-semibold " + "[&_p]:my-2 [&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5 dark:[&_code]:bg-neutral-800";
|
|
58
|
+
function MarkdownField({
|
|
59
|
+
value,
|
|
60
|
+
onChange
|
|
61
|
+
}) {
|
|
62
|
+
const [mode, setMode] = useState("write");
|
|
63
|
+
const [el, setEl] = useState(null);
|
|
64
|
+
function apply(wrap) {
|
|
65
|
+
if (!el)
|
|
66
|
+
return;
|
|
67
|
+
const start = el.selectionStart;
|
|
68
|
+
const end = el.selectionEnd;
|
|
69
|
+
const selected = value.slice(start, end) || wrap.placeholder;
|
|
70
|
+
onChange(value.slice(0, start) + wrap.before + selected + wrap.after + value.slice(end));
|
|
71
|
+
requestAnimationFrame(() => {
|
|
72
|
+
el.focus();
|
|
73
|
+
const caret = start + wrap.before.length;
|
|
74
|
+
el.setSelectionRange(caret, caret + selected.length);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const tab = (active) => `inline-flex h-7 items-center rounded px-2 text-xs ${active ? "bg-neutral-100 font-medium text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100" : "text-neutral-600 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"}`;
|
|
78
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
79
|
+
className: "rounded-md border border-neutral-300 bg-white text-neutral-900 dark:border-neutral-700 dark:bg-neutral-950 dark:text-neutral-100",
|
|
80
|
+
children: [
|
|
81
|
+
/* @__PURE__ */ jsxs("div", {
|
|
82
|
+
className: "flex items-center gap-0.5 border-b border-neutral-200 px-1 py-1 dark:border-neutral-800",
|
|
83
|
+
children: [
|
|
84
|
+
TOOLS.map((t) => /* @__PURE__ */ jsx("button", {
|
|
85
|
+
type: "button",
|
|
86
|
+
title: t.label,
|
|
87
|
+
"aria-label": t.label,
|
|
88
|
+
disabled: mode === "preview",
|
|
89
|
+
onClick: () => apply(t.wrap),
|
|
90
|
+
className: TOOL_BUTTON,
|
|
91
|
+
children: t.glyph
|
|
92
|
+
}, t.key)),
|
|
93
|
+
/* @__PURE__ */ jsxs("div", {
|
|
94
|
+
className: "ml-auto flex gap-0.5",
|
|
95
|
+
children: [
|
|
96
|
+
/* @__PURE__ */ jsx("button", {
|
|
97
|
+
type: "button",
|
|
98
|
+
onClick: () => setMode("write"),
|
|
99
|
+
className: tab(mode === "write"),
|
|
100
|
+
children: "Write"
|
|
101
|
+
}),
|
|
102
|
+
/* @__PURE__ */ jsx("button", {
|
|
103
|
+
type: "button",
|
|
104
|
+
onClick: () => setMode("preview"),
|
|
105
|
+
className: tab(mode === "preview"),
|
|
106
|
+
children: "Preview"
|
|
107
|
+
})
|
|
108
|
+
]
|
|
109
|
+
})
|
|
110
|
+
]
|
|
111
|
+
}),
|
|
112
|
+
mode === "write" ? /* @__PURE__ */ jsx("textarea", {
|
|
113
|
+
ref: setEl,
|
|
114
|
+
rows: 8,
|
|
115
|
+
value,
|
|
116
|
+
spellCheck: false,
|
|
117
|
+
placeholder: "Write Markdown…",
|
|
118
|
+
onChange: (e) => onChange(e.target.value),
|
|
119
|
+
className: "w-full resize-y rounded-b-md border-0 bg-transparent px-3 py-2 font-mono text-sm text-neutral-900 outline-none placeholder:text-neutral-400 dark:text-neutral-100 dark:placeholder:text-neutral-500"
|
|
120
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
121
|
+
className: PREVIEW_TYPOGRAPHY,
|
|
122
|
+
dangerouslySetInnerHTML: { __html: renderMarkdown(value || "_Nothing to preview_") }
|
|
123
|
+
})
|
|
124
|
+
]
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/RelationSelect.tsx
|
|
129
|
+
import { useEffect, useMemo, useRef, useState as useState2 } from "react";
|
|
130
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
7
131
|
var PAGE = 200;
|
|
8
132
|
function labelOf(doc, field) {
|
|
9
133
|
const raw = doc[field];
|
|
@@ -15,11 +139,11 @@ function labelOf(doc, field) {
|
|
|
15
139
|
}
|
|
16
140
|
function RelationSelect(props) {
|
|
17
141
|
const { collection, displayField, value, onChange, placeholder, required } = props;
|
|
18
|
-
const [docs, setDocs] =
|
|
19
|
-
const [error, setError] =
|
|
20
|
-
const [query, setQuery] =
|
|
21
|
-
const [open, setOpen] =
|
|
22
|
-
const [truncated, setTruncated] =
|
|
142
|
+
const [docs, setDocs] = useState2(null);
|
|
143
|
+
const [error, setError] = useState2(null);
|
|
144
|
+
const [query, setQuery] = useState2("");
|
|
145
|
+
const [open, setOpen] = useState2(false);
|
|
146
|
+
const [truncated, setTruncated] = useState2(false);
|
|
23
147
|
const boxRef = useRef(null);
|
|
24
148
|
useEffect(() => {
|
|
25
149
|
let live = true;
|
|
@@ -55,47 +179,47 @@ function RelationSelect(props) {
|
|
|
55
179
|
return all.filter((d) => labelOf(d, displayField).toLowerCase().includes(q));
|
|
56
180
|
}, [docs, displayField, query]);
|
|
57
181
|
if (error) {
|
|
58
|
-
return /* @__PURE__ */
|
|
182
|
+
return /* @__PURE__ */ jsx2("p", {
|
|
59
183
|
className: "text-xs text-red-600",
|
|
60
184
|
children: error
|
|
61
185
|
});
|
|
62
186
|
}
|
|
63
187
|
const buttonLabel = selected ? labelOf(selected, displayField) : value !== "" ? value : placeholder ?? "Select…";
|
|
64
|
-
return /* @__PURE__ */
|
|
188
|
+
return /* @__PURE__ */ jsxs2("div", {
|
|
65
189
|
ref: boxRef,
|
|
66
190
|
className: "relative",
|
|
67
191
|
children: [
|
|
68
|
-
/* @__PURE__ */
|
|
192
|
+
/* @__PURE__ */ jsxs2("button", {
|
|
69
193
|
type: "button",
|
|
70
194
|
onClick: () => setOpen((v) => !v),
|
|
71
195
|
className: "flex w-full items-center justify-between rounded-md border border-neutral-300 px-3 py-1.5 text-left text-sm dark:border-neutral-700 dark:bg-neutral-950",
|
|
72
196
|
children: [
|
|
73
|
-
/* @__PURE__ */
|
|
197
|
+
/* @__PURE__ */ jsx2("span", {
|
|
74
198
|
className: selected || value ? "" : "text-neutral-400",
|
|
75
199
|
children: docs === null ? "Loading…" : buttonLabel
|
|
76
200
|
}),
|
|
77
|
-
/* @__PURE__ */
|
|
201
|
+
/* @__PURE__ */ jsx2("span", {
|
|
78
202
|
"aria-hidden": true,
|
|
79
203
|
className: "ml-2 text-neutral-400",
|
|
80
204
|
children: "▾"
|
|
81
205
|
})
|
|
82
206
|
]
|
|
83
207
|
}),
|
|
84
|
-
open ? /* @__PURE__ */
|
|
208
|
+
open ? /* @__PURE__ */ jsxs2("div", {
|
|
85
209
|
className: "absolute z-20 mt-1 w-full rounded-md border border-neutral-200 bg-white shadow-lg dark:border-neutral-800 dark:bg-neutral-950",
|
|
86
210
|
children: [
|
|
87
|
-
/* @__PURE__ */
|
|
211
|
+
/* @__PURE__ */ jsx2("input", {
|
|
88
212
|
autoFocus: true,
|
|
89
213
|
value: query,
|
|
90
214
|
onChange: (e) => setQuery(e.target.value),
|
|
91
215
|
placeholder: "Search…",
|
|
92
216
|
className: "w-full rounded-t-md border-b border-neutral-200 px-3 py-1.5 text-sm outline-none dark:border-neutral-800 dark:bg-neutral-950"
|
|
93
217
|
}),
|
|
94
|
-
/* @__PURE__ */
|
|
218
|
+
/* @__PURE__ */ jsxs2("ul", {
|
|
95
219
|
className: "max-h-56 overflow-auto py-1",
|
|
96
220
|
children: [
|
|
97
|
-
!required ? /* @__PURE__ */
|
|
98
|
-
children: /* @__PURE__ */
|
|
221
|
+
!required ? /* @__PURE__ */ jsx2("li", {
|
|
222
|
+
children: /* @__PURE__ */ jsx2("button", {
|
|
99
223
|
type: "button",
|
|
100
224
|
onClick: () => {
|
|
101
225
|
onChange("");
|
|
@@ -106,13 +230,13 @@ function RelationSelect(props) {
|
|
|
106
230
|
children: "None"
|
|
107
231
|
})
|
|
108
232
|
}) : null,
|
|
109
|
-
matches.length === 0 ? /* @__PURE__ */
|
|
233
|
+
matches.length === 0 ? /* @__PURE__ */ jsx2("li", {
|
|
110
234
|
className: "px-3 py-2 text-xs text-neutral-500",
|
|
111
235
|
children: "No matches."
|
|
112
236
|
}) : matches.map((doc) => {
|
|
113
237
|
const id = typeof doc["id"] === "string" ? doc["id"] : "";
|
|
114
|
-
return /* @__PURE__ */
|
|
115
|
-
children: /* @__PURE__ */
|
|
238
|
+
return /* @__PURE__ */ jsx2("li", {
|
|
239
|
+
children: /* @__PURE__ */ jsx2("button", {
|
|
116
240
|
type: "button",
|
|
117
241
|
onClick: () => {
|
|
118
242
|
onChange(id);
|
|
@@ -126,7 +250,7 @@ function RelationSelect(props) {
|
|
|
126
250
|
})
|
|
127
251
|
]
|
|
128
252
|
}),
|
|
129
|
-
truncated ? /* @__PURE__ */
|
|
253
|
+
truncated ? /* @__PURE__ */ jsxs2("p", {
|
|
130
254
|
className: "border-t border-neutral-200 px-3 py-1.5 text-[11px] text-neutral-500 dark:border-neutral-800",
|
|
131
255
|
children: [
|
|
132
256
|
"Showing the first ",
|
|
@@ -142,10 +266,10 @@ function RelationSelect(props) {
|
|
|
142
266
|
|
|
143
267
|
// src/runtime.tsx
|
|
144
268
|
import { createContext, useContext, useCallback } from "react";
|
|
145
|
-
import { jsx as
|
|
269
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
146
270
|
var Ctx = createContext(null);
|
|
147
271
|
function GroveUiProvider({ value, children }) {
|
|
148
|
-
return /* @__PURE__ */
|
|
272
|
+
return /* @__PURE__ */ jsx3(Ctx.Provider, {
|
|
149
273
|
value,
|
|
150
274
|
children
|
|
151
275
|
});
|
|
@@ -187,15 +311,19 @@ function useAction() {
|
|
|
187
311
|
return;
|
|
188
312
|
}
|
|
189
313
|
case "endpoint": {
|
|
190
|
-
const base = ctx.endpointBase ?? "";
|
|
191
314
|
const href = resolveHref(action.path, opts.doc);
|
|
192
|
-
|
|
315
|
+
const method = action.method ?? "POST";
|
|
316
|
+
if (ctx.callEndpoint)
|
|
317
|
+
await ctx.callEndpoint(href, method);
|
|
318
|
+
else
|
|
319
|
+
await fetch(`${ctx.endpointBase ?? ""}${href}`, { method });
|
|
193
320
|
opts.onDone?.();
|
|
194
321
|
return;
|
|
195
322
|
}
|
|
196
323
|
}
|
|
197
324
|
}, [ctx]);
|
|
198
325
|
}
|
|
326
|
+
var MARKDOWN_CELL_MAX = 80;
|
|
199
327
|
function formatValue(value, format, opts = {}) {
|
|
200
328
|
if (value === null || value === undefined)
|
|
201
329
|
return "";
|
|
@@ -216,74 +344,245 @@ function formatValue(value, format, opts = {}) {
|
|
|
216
344
|
return Number.isFinite(n) ? new Intl.NumberFormat(opts.locale).format(n) : String(value);
|
|
217
345
|
}
|
|
218
346
|
case "date": {
|
|
219
|
-
const
|
|
220
|
-
|
|
347
|
+
const text = String(value);
|
|
348
|
+
const day = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
|
|
349
|
+
const d = day ? new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3])) : new Date(text);
|
|
350
|
+
return Number.isNaN(d.getTime()) ? text : d.toLocaleDateString(opts.locale);
|
|
351
|
+
}
|
|
352
|
+
case "markdown": {
|
|
353
|
+
const text = String(value).replace(/\s+/g, " ").trim();
|
|
354
|
+
if (text.length <= MARKDOWN_CELL_MAX)
|
|
355
|
+
return text;
|
|
356
|
+
const cut = text.slice(0, MARKDOWN_CELL_MAX);
|
|
357
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
358
|
+
return `${(lastSpace > MARKDOWN_CELL_MAX / 2 ? cut.slice(0, lastSpace) : cut).trimEnd()}…`;
|
|
221
359
|
}
|
|
222
360
|
default:
|
|
223
361
|
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
224
362
|
}
|
|
225
363
|
}
|
|
364
|
+
function useMessages(messages, fallback) {
|
|
365
|
+
const { locale } = useGroveUi();
|
|
366
|
+
return useCallback((key) => {
|
|
367
|
+
const entry = messages[key];
|
|
368
|
+
if (!entry)
|
|
369
|
+
return key;
|
|
370
|
+
return (locale !== undefined ? entry[locale] : undefined) ?? entry[fallback] ?? key;
|
|
371
|
+
}, [messages, locale, fallback]);
|
|
372
|
+
}
|
|
373
|
+
function detectLocale(locales, preferred = typeof navigator !== "undefined" ? navigator.languages : []) {
|
|
374
|
+
const first = locales[0] ?? "en";
|
|
375
|
+
for (const want of preferred) {
|
|
376
|
+
const exact = locales.find((l) => l.toLowerCase() === want.toLowerCase());
|
|
377
|
+
if (exact)
|
|
378
|
+
return exact;
|
|
379
|
+
const lang = want.split("-")[0]?.toLowerCase();
|
|
380
|
+
const loose = locales.find((l) => l.split("-")[0]?.toLowerCase() === lang);
|
|
381
|
+
if (loose)
|
|
382
|
+
return loose;
|
|
383
|
+
}
|
|
384
|
+
return first;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/styles.ts
|
|
388
|
+
function cx(...parts) {
|
|
389
|
+
return parts.filter(Boolean).join(" ");
|
|
390
|
+
}
|
|
391
|
+
var INPUT_CLASS = "rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-sm text-neutral-900 " + "dark:border-neutral-700 dark:bg-neutral-950 dark:text-neutral-100";
|
|
392
|
+
var BUTTON_BASE = "inline-flex w-fit self-start items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium " + "transition-colors disabled:opacity-50 disabled:pointer-events-none";
|
|
393
|
+
var VARIANT = {
|
|
394
|
+
default: "bg-neutral-900 text-white hover:bg-neutral-800 dark:bg-white dark:text-neutral-900",
|
|
395
|
+
secondary: "bg-neutral-100 text-neutral-900 hover:bg-neutral-200 dark:bg-neutral-800 dark:text-neutral-100",
|
|
396
|
+
destructive: "bg-red-600 text-white hover:bg-red-700",
|
|
397
|
+
outline: "border border-neutral-300 text-neutral-900 hover:bg-neutral-50 " + "dark:border-neutral-700 dark:text-neutral-100 dark:hover:bg-neutral-900",
|
|
398
|
+
ghost: "text-neutral-900 hover:bg-neutral-100 dark:text-neutral-100 dark:hover:bg-neutral-800"
|
|
399
|
+
};
|
|
400
|
+
var TONE = {
|
|
401
|
+
error: "text-sm text-red-600",
|
|
402
|
+
success: "text-sm text-green-700 dark:text-green-500",
|
|
403
|
+
muted: "text-sm text-neutral-500"
|
|
404
|
+
};
|
|
226
405
|
|
|
227
406
|
// src/components.tsx
|
|
228
|
-
import { jsx as
|
|
407
|
+
import { jsx as jsx4, jsxs as jsxs3, Fragment } from "react/jsx-runtime";
|
|
408
|
+
var WIDTH_CLASS = { narrow: "w-[12%]", normal: "", wide: "w-[45%]" };
|
|
229
409
|
var GAP = ["gap-0", "gap-1", "gap-2", "gap-3", "gap-4", "gap-5", "gap-6", "gap-7", "gap-8", "gap-9", "gap-10", "gap-11", "gap-12"];
|
|
230
410
|
var COLUMNS = ["", "md:grid-cols-1", "md:grid-cols-2", "md:grid-cols-3", "md:grid-cols-4", "md:grid-cols-5", "md:grid-cols-6"];
|
|
231
411
|
var WIDTH = { full: "w-full", container: "w-full max-w-5xl mx-auto", narrow: "w-full max-w-xl mx-auto" };
|
|
232
412
|
var ALIGN = { start: "items-start", center: "items-center", end: "items-end", stretch: "items-stretch" };
|
|
233
413
|
var JUSTIFY = { start: "justify-start", center: "justify-center", end: "justify-end", between: "justify-between" };
|
|
234
|
-
function cx(...parts) {
|
|
235
|
-
return parts.filter(Boolean).join(" ");
|
|
236
|
-
}
|
|
237
414
|
function Stack(props) {
|
|
238
415
|
const { direction = "vertical", gap = 4, align, justify, width, wrap, children } = props;
|
|
239
|
-
return /* @__PURE__ */
|
|
416
|
+
return /* @__PURE__ */ jsx4("div", {
|
|
240
417
|
className: cx("flex", direction === "vertical" ? "flex-col" : "flex-row", GAP[Math.min(Math.max(gap, 0), 12)], align && ALIGN[align], justify && JUSTIFY[justify], width && WIDTH[width], wrap && "flex-wrap"),
|
|
241
418
|
children
|
|
242
419
|
});
|
|
243
420
|
}
|
|
244
421
|
function Grid(props) {
|
|
245
422
|
const { columns, gap = 4, width, children } = props;
|
|
246
|
-
return /* @__PURE__ */
|
|
423
|
+
return /* @__PURE__ */ jsx4("div", {
|
|
247
424
|
className: cx("grid grid-cols-1", COLUMNS[Math.min(Math.max(columns, 1), 6)], GAP[Math.min(Math.max(gap, 0), 12)], width && WIDTH[width]),
|
|
248
425
|
children
|
|
249
426
|
});
|
|
250
427
|
}
|
|
251
428
|
function Divider() {
|
|
252
|
-
return /* @__PURE__ */
|
|
429
|
+
return /* @__PURE__ */ jsx4("hr", {
|
|
253
430
|
className: "border-t border-neutral-200 dark:border-neutral-800"
|
|
254
431
|
});
|
|
255
432
|
}
|
|
256
433
|
var HEADING = { 1: "text-3xl font-semibold tracking-tight", 2: "text-2xl font-semibold", 3: "text-lg font-medium" };
|
|
257
434
|
function Heading({ level = 1, children }) {
|
|
258
435
|
const Tag = ["h1", "h2", "h3"][level - 1] ?? "h1";
|
|
259
|
-
return /* @__PURE__ */
|
|
436
|
+
return /* @__PURE__ */ jsx4(Tag, {
|
|
260
437
|
className: HEADING[level],
|
|
261
438
|
children
|
|
262
439
|
});
|
|
263
440
|
}
|
|
264
441
|
function Text({ muted, children }) {
|
|
265
|
-
return /* @__PURE__ */
|
|
442
|
+
return /* @__PURE__ */ jsx4("p", {
|
|
266
443
|
className: muted ? "text-sm text-neutral-500" : "text-sm",
|
|
267
444
|
children
|
|
268
445
|
});
|
|
269
446
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
447
|
+
function Header(props) {
|
|
448
|
+
const { brand, brandHref = "/", links, actions, sticky } = props;
|
|
449
|
+
const dispatch = useAction();
|
|
450
|
+
const [open, setOpen] = useState3(false);
|
|
451
|
+
const go = useCallback2((href) => (e) => {
|
|
452
|
+
if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0)
|
|
453
|
+
return;
|
|
454
|
+
e.preventDefault();
|
|
455
|
+
setOpen(false);
|
|
456
|
+
dispatch({ kind: "navigate", href }, {});
|
|
457
|
+
}, [dispatch]);
|
|
458
|
+
const hasNav = links && links.length > 0 || actions && actions.length > 0;
|
|
459
|
+
return /* @__PURE__ */ jsxs3("header", {
|
|
460
|
+
className: cx("border-b border-neutral-200 bg-white px-4 py-3 text-neutral-900", "dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-100", sticky ? "sticky top-0 z-40" : ""),
|
|
461
|
+
children: [
|
|
462
|
+
/* @__PURE__ */ jsxs3("div", {
|
|
463
|
+
className: "flex items-center gap-6",
|
|
464
|
+
children: [
|
|
465
|
+
brand !== undefined && brand !== null && brand !== "" ? /* @__PURE__ */ jsx4("a", {
|
|
466
|
+
href: brandHref,
|
|
467
|
+
onClick: go(brandHref),
|
|
468
|
+
className: "text-sm font-semibold tracking-tight",
|
|
469
|
+
children: brand
|
|
470
|
+
}) : null,
|
|
471
|
+
links && links.length > 0 ? /* @__PURE__ */ jsx4("nav", {
|
|
472
|
+
"data-grove-nav": true,
|
|
473
|
+
className: "hidden items-center gap-4 md:flex",
|
|
474
|
+
children: links.map((link, i) => /* @__PURE__ */ jsx4("a", {
|
|
475
|
+
href: link.href,
|
|
476
|
+
onClick: go(link.href),
|
|
477
|
+
className: "text-sm text-neutral-600 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100",
|
|
478
|
+
children: link.label
|
|
479
|
+
}, i))
|
|
480
|
+
}) : null,
|
|
481
|
+
actions && actions.length > 0 ? /* @__PURE__ */ jsx4("div", {
|
|
482
|
+
"data-grove-nav-actions": true,
|
|
483
|
+
className: "ml-auto hidden items-center gap-2 md:flex",
|
|
484
|
+
children: actions.map((a, i) => /* @__PURE__ */ jsx4(Button, {
|
|
485
|
+
action: a.action,
|
|
486
|
+
variant: a.variant,
|
|
487
|
+
children: a.label
|
|
488
|
+
}, i))
|
|
489
|
+
}) : null,
|
|
490
|
+
hasNav ? /* @__PURE__ */ jsx4("button", {
|
|
491
|
+
type: "button",
|
|
492
|
+
"data-grove-nav-toggle": true,
|
|
493
|
+
"aria-label": open ? "Close menu" : "Open menu",
|
|
494
|
+
"aria-expanded": open,
|
|
495
|
+
onClick: () => setOpen((v) => !v),
|
|
496
|
+
className: "ml-auto inline-flex items-center justify-center rounded-md p-2 text-neutral-600 hover:bg-neutral-100 hover:text-neutral-900 md:hidden dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-100",
|
|
497
|
+
children: /* @__PURE__ */ jsx4("svg", {
|
|
498
|
+
width: "20",
|
|
499
|
+
height: "20",
|
|
500
|
+
viewBox: "0 0 24 24",
|
|
501
|
+
fill: "none",
|
|
502
|
+
stroke: "currentColor",
|
|
503
|
+
strokeWidth: "2",
|
|
504
|
+
strokeLinecap: "round",
|
|
505
|
+
"aria-hidden": "true",
|
|
506
|
+
children: open ? /* @__PURE__ */ jsxs3(Fragment, {
|
|
507
|
+
children: [
|
|
508
|
+
/* @__PURE__ */ jsx4("line", {
|
|
509
|
+
x1: "18",
|
|
510
|
+
y1: "6",
|
|
511
|
+
x2: "6",
|
|
512
|
+
y2: "18"
|
|
513
|
+
}),
|
|
514
|
+
/* @__PURE__ */ jsx4("line", {
|
|
515
|
+
x1: "6",
|
|
516
|
+
y1: "6",
|
|
517
|
+
x2: "18",
|
|
518
|
+
y2: "18"
|
|
519
|
+
})
|
|
520
|
+
]
|
|
521
|
+
}) : /* @__PURE__ */ jsxs3(Fragment, {
|
|
522
|
+
children: [
|
|
523
|
+
/* @__PURE__ */ jsx4("line", {
|
|
524
|
+
x1: "3",
|
|
525
|
+
y1: "6",
|
|
526
|
+
x2: "21",
|
|
527
|
+
y2: "6"
|
|
528
|
+
}),
|
|
529
|
+
/* @__PURE__ */ jsx4("line", {
|
|
530
|
+
x1: "3",
|
|
531
|
+
y1: "12",
|
|
532
|
+
x2: "21",
|
|
533
|
+
y2: "12"
|
|
534
|
+
}),
|
|
535
|
+
/* @__PURE__ */ jsx4("line", {
|
|
536
|
+
x1: "3",
|
|
537
|
+
y1: "18",
|
|
538
|
+
x2: "21",
|
|
539
|
+
y2: "18"
|
|
540
|
+
})
|
|
541
|
+
]
|
|
542
|
+
})
|
|
543
|
+
})
|
|
544
|
+
}) : null
|
|
545
|
+
]
|
|
546
|
+
}),
|
|
547
|
+
hasNav && open ? /* @__PURE__ */ jsxs3("div", {
|
|
548
|
+
"data-grove-nav-drawer": true,
|
|
549
|
+
className: "mt-3 flex flex-col gap-1 border-t border-neutral-200 pt-3 md:hidden dark:border-neutral-800",
|
|
550
|
+
children: [
|
|
551
|
+
links?.map((link, i) => /* @__PURE__ */ jsx4("a", {
|
|
552
|
+
href: link.href,
|
|
553
|
+
onClick: go(link.href),
|
|
554
|
+
className: "rounded-md px-2 py-2 text-sm text-neutral-600 hover:bg-neutral-100 hover:text-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-100",
|
|
555
|
+
children: link.label
|
|
556
|
+
}, i)),
|
|
557
|
+
actions && actions.length > 0 ? /* @__PURE__ */ jsx4("div", {
|
|
558
|
+
className: "mt-2 flex flex-col gap-2",
|
|
559
|
+
children: actions.map((a, i) => /* @__PURE__ */ jsx4(Button, {
|
|
560
|
+
action: a.action,
|
|
561
|
+
variant: a.variant,
|
|
562
|
+
children: a.label
|
|
563
|
+
}, i))
|
|
564
|
+
}) : null
|
|
565
|
+
]
|
|
566
|
+
}) : null
|
|
567
|
+
]
|
|
568
|
+
});
|
|
569
|
+
}
|
|
278
570
|
function Button(props) {
|
|
279
571
|
const { action, variant = "default", doc, collection, onDone, children } = props;
|
|
280
572
|
const dispatch = useAction();
|
|
281
|
-
const [busy, setBusy] =
|
|
573
|
+
const [busy, setBusy] = useState3(false);
|
|
574
|
+
const running = useRef2(false);
|
|
282
575
|
const onClick = useCallback2(() => {
|
|
576
|
+
if (running.current)
|
|
577
|
+
return;
|
|
578
|
+
running.current = true;
|
|
283
579
|
setBusy(true);
|
|
284
|
-
dispatch(action, { doc, collection, onDone }).finally(() =>
|
|
580
|
+
dispatch(action, { doc, collection, onDone }).finally(() => {
|
|
581
|
+
running.current = false;
|
|
582
|
+
setBusy(false);
|
|
583
|
+
});
|
|
285
584
|
}, [action, dispatch, doc, collection, onDone]);
|
|
286
|
-
return /* @__PURE__ */
|
|
585
|
+
return /* @__PURE__ */ jsx4("button", {
|
|
287
586
|
type: "button",
|
|
288
587
|
disabled: busy,
|
|
289
588
|
onClick,
|
|
@@ -292,10 +591,10 @@ function Button(props) {
|
|
|
292
591
|
});
|
|
293
592
|
}
|
|
294
593
|
function useAsync(load, deps) {
|
|
295
|
-
const [data, setData] =
|
|
296
|
-
const [error, setError] =
|
|
297
|
-
const [loading, setLoading] =
|
|
298
|
-
const [nonce, setNonce] =
|
|
594
|
+
const [data, setData] = useState3(null);
|
|
595
|
+
const [error, setError] = useState3(null);
|
|
596
|
+
const [loading, setLoading] = useState3(true);
|
|
597
|
+
const [nonce, setNonce] = useState3(0);
|
|
299
598
|
useEffect2(() => {
|
|
300
599
|
let live = true;
|
|
301
600
|
setLoading(true);
|
|
@@ -317,14 +616,14 @@ function useAsync(load, deps) {
|
|
|
317
616
|
return { data, error, loading, reload: () => setNonce((n) => n + 1) };
|
|
318
617
|
}
|
|
319
618
|
function Message({ tone, children }) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
619
|
+
return /* @__PURE__ */ jsx4("p", {
|
|
620
|
+
className: TONE[tone],
|
|
621
|
+
...tone === "success" ? { role: "status" } : {},
|
|
323
622
|
children
|
|
324
623
|
});
|
|
325
624
|
}
|
|
326
625
|
function useRelations(fields, docs, resolveCollection) {
|
|
327
|
-
const [map, setMap] =
|
|
626
|
+
const [map, setMap] = useState3({});
|
|
328
627
|
const relations = fields.filter((f) => f.display);
|
|
329
628
|
const key = JSON.stringify(relations.map((f) => f.field)) + (docs?.length ?? 0);
|
|
330
629
|
useEffect2(() => {
|
|
@@ -367,54 +666,54 @@ function DataTable(props) {
|
|
|
367
666
|
const { data, error, loading, reload } = useAsync(() => collection.find(options), [JSON.stringify(options)]);
|
|
368
667
|
const relations = useRelations(columns, data, resolveRelation);
|
|
369
668
|
if (loading)
|
|
370
|
-
return /* @__PURE__ */
|
|
669
|
+
return /* @__PURE__ */ jsx4(Message, {
|
|
371
670
|
tone: "muted",
|
|
372
671
|
children: "Loading…"
|
|
373
672
|
});
|
|
374
673
|
if (error)
|
|
375
|
-
return /* @__PURE__ */
|
|
674
|
+
return /* @__PURE__ */ jsx4(Message, {
|
|
376
675
|
tone: "error",
|
|
377
676
|
children: error
|
|
378
677
|
});
|
|
379
678
|
if (!data || data.length === 0)
|
|
380
|
-
return /* @__PURE__ */
|
|
679
|
+
return /* @__PURE__ */ jsx4(Message, {
|
|
381
680
|
tone: "muted",
|
|
382
681
|
children: emptyText ?? "Nothing here yet."
|
|
383
682
|
});
|
|
384
|
-
return /* @__PURE__ */
|
|
683
|
+
return /* @__PURE__ */ jsx4("div", {
|
|
385
684
|
className: "w-full overflow-x-auto rounded-md border border-neutral-200 dark:border-neutral-800",
|
|
386
|
-
children: /* @__PURE__ */
|
|
685
|
+
children: /* @__PURE__ */ jsxs3("table", {
|
|
387
686
|
className: "w-full text-sm",
|
|
388
687
|
children: [
|
|
389
|
-
/* @__PURE__ */
|
|
390
|
-
className: "bg-neutral-50 dark:bg-neutral-900",
|
|
391
|
-
children: /* @__PURE__ */
|
|
688
|
+
/* @__PURE__ */ jsx4("thead", {
|
|
689
|
+
className: "bg-neutral-50 text-neutral-900 dark:bg-neutral-900 dark:text-neutral-100",
|
|
690
|
+
children: /* @__PURE__ */ jsxs3("tr", {
|
|
392
691
|
children: [
|
|
393
|
-
columns.map((c) => /* @__PURE__ */
|
|
394
|
-
className:
|
|
692
|
+
columns.map((c) => /* @__PURE__ */ jsx4("th", {
|
|
693
|
+
className: `px-3 py-2 text-left font-medium text-neutral-600 dark:text-neutral-400 ${WIDTH_CLASS[c.width ?? "normal"]}`,
|
|
395
694
|
children: c.label
|
|
396
695
|
}, c.field)),
|
|
397
|
-
rowActions && rowActions.length > 0 ? /* @__PURE__ */
|
|
696
|
+
rowActions && rowActions.length > 0 ? /* @__PURE__ */ jsx4("th", {
|
|
398
697
|
className: "px-3 py-2"
|
|
399
698
|
}) : null
|
|
400
699
|
]
|
|
401
700
|
})
|
|
402
701
|
}),
|
|
403
|
-
/* @__PURE__ */
|
|
404
|
-
children: data.map((doc, i) => /* @__PURE__ */
|
|
702
|
+
/* @__PURE__ */ jsx4("tbody", {
|
|
703
|
+
children: data.map((doc, i) => /* @__PURE__ */ jsxs3("tr", {
|
|
405
704
|
className: cx("border-t border-neutral-200 dark:border-neutral-800", rowHref && "cursor-pointer hover:bg-neutral-50 dark:hover:bg-neutral-900"),
|
|
406
705
|
onClick: rowHref ? () => ctx.navigate(resolveHref(rowHref, doc)) : undefined,
|
|
407
706
|
children: [
|
|
408
|
-
columns.map((c) => /* @__PURE__ */
|
|
707
|
+
columns.map((c) => /* @__PURE__ */ jsx4("td", {
|
|
409
708
|
className: "px-3 py-2",
|
|
410
709
|
children: cellValue(doc, c, relations, ctx.locale, ctx.currency)
|
|
411
710
|
}, c.field)),
|
|
412
|
-
rowActions && rowActions.length > 0 ? /* @__PURE__ */
|
|
711
|
+
rowActions && rowActions.length > 0 ? /* @__PURE__ */ jsx4("td", {
|
|
413
712
|
className: "px-3 py-2",
|
|
414
|
-
children: /* @__PURE__ */
|
|
713
|
+
children: /* @__PURE__ */ jsx4("div", {
|
|
415
714
|
className: "flex justify-end gap-2",
|
|
416
715
|
onClick: (e) => e.stopPropagation(),
|
|
417
|
-
children: rowActions.map((a) => /* @__PURE__ */
|
|
716
|
+
children: rowActions.map((a) => /* @__PURE__ */ jsx4(Button, {
|
|
418
717
|
action: a.action,
|
|
419
718
|
variant: a.variant,
|
|
420
719
|
doc,
|
|
@@ -437,30 +736,33 @@ function Detail(props) {
|
|
|
437
736
|
const { data, error, loading } = useAsync(() => collection.get(id), [id]);
|
|
438
737
|
const relations = useRelations(fields, data ? [data] : null, resolveRelation);
|
|
439
738
|
if (loading)
|
|
440
|
-
return /* @__PURE__ */
|
|
739
|
+
return /* @__PURE__ */ jsx4(Message, {
|
|
441
740
|
tone: "muted",
|
|
442
741
|
children: "Loading…"
|
|
443
742
|
});
|
|
444
743
|
if (error)
|
|
445
|
-
return /* @__PURE__ */
|
|
744
|
+
return /* @__PURE__ */ jsx4(Message, {
|
|
446
745
|
tone: "error",
|
|
447
746
|
children: error
|
|
448
747
|
});
|
|
449
748
|
if (!data)
|
|
450
|
-
return /* @__PURE__ */
|
|
749
|
+
return /* @__PURE__ */ jsx4(Message, {
|
|
451
750
|
tone: "muted",
|
|
452
751
|
children: "Not found."
|
|
453
752
|
});
|
|
454
|
-
return /* @__PURE__ */
|
|
753
|
+
return /* @__PURE__ */ jsx4("dl", {
|
|
455
754
|
className: "divide-y divide-neutral-200 rounded-md border border-neutral-200 dark:divide-neutral-800 dark:border-neutral-800",
|
|
456
|
-
children: fields.map((f) => /* @__PURE__ */
|
|
755
|
+
children: fields.map((f) => /* @__PURE__ */ jsxs3("div", {
|
|
457
756
|
className: "grid grid-cols-3 gap-4 px-3 py-2",
|
|
458
757
|
children: [
|
|
459
|
-
/* @__PURE__ */
|
|
758
|
+
/* @__PURE__ */ jsx4("dt", {
|
|
460
759
|
className: "text-sm text-neutral-500",
|
|
461
760
|
children: f.label
|
|
462
761
|
}),
|
|
463
|
-
/* @__PURE__ */
|
|
762
|
+
f.format === "markdown" && !f.display ? /* @__PURE__ */ jsx4("dd", {
|
|
763
|
+
className: "col-span-2 whitespace-pre-wrap text-sm",
|
|
764
|
+
children: String(data[f.field] ?? "")
|
|
765
|
+
}) : /* @__PURE__ */ jsx4("dd", {
|
|
464
766
|
className: "col-span-2 text-sm",
|
|
465
767
|
children: cellValue(data, f, relations, ctx.locale, ctx.currency)
|
|
466
768
|
})
|
|
@@ -493,10 +795,12 @@ function coerce(raw, format) {
|
|
|
493
795
|
function Form(props) {
|
|
494
796
|
const { collection, mode, id, fields, submitLabel, onSuccess, resolveRelation } = props;
|
|
495
797
|
const dispatch = useAction();
|
|
496
|
-
const [values, setValues] =
|
|
497
|
-
const [error, setError] =
|
|
498
|
-
const [busy, setBusy] =
|
|
499
|
-
const [
|
|
798
|
+
const [values, setValues] = useState3({});
|
|
799
|
+
const [error, setError] = useState3(null);
|
|
800
|
+
const [busy, setBusy] = useState3(false);
|
|
801
|
+
const [done, setDone] = useState3(false);
|
|
802
|
+
const submitting = useRef2(false);
|
|
803
|
+
const [loading, setLoading] = useState3(mode === "update");
|
|
500
804
|
useEffect2(() => {
|
|
501
805
|
if (mode !== "update" || !id)
|
|
502
806
|
return;
|
|
@@ -523,10 +827,18 @@ function Form(props) {
|
|
|
523
827
|
live = false;
|
|
524
828
|
};
|
|
525
829
|
}, [id, mode]);
|
|
830
|
+
const setField = useCallback2((field, value) => {
|
|
831
|
+
setDone(false);
|
|
832
|
+
setValues((v) => ({ ...v, [field]: value }));
|
|
833
|
+
}, []);
|
|
526
834
|
const submit = useCallback2(async (e) => {
|
|
527
835
|
e.preventDefault();
|
|
836
|
+
if (submitting.current)
|
|
837
|
+
return;
|
|
838
|
+
submitting.current = true;
|
|
528
839
|
setBusy(true);
|
|
529
840
|
setError(null);
|
|
841
|
+
setDone(false);
|
|
530
842
|
try {
|
|
531
843
|
const payload = {};
|
|
532
844
|
for (const f of fields) {
|
|
@@ -538,53 +850,79 @@ function Form(props) {
|
|
|
538
850
|
if (onSuccess) {
|
|
539
851
|
const doc = mode === "create" ? saved : { id, ...payload };
|
|
540
852
|
await dispatch(onSuccess, { doc });
|
|
853
|
+
} else {
|
|
854
|
+
setDone(true);
|
|
855
|
+
if (mode === "create")
|
|
856
|
+
setValues({});
|
|
541
857
|
}
|
|
542
858
|
} catch (err) {
|
|
543
859
|
setError(err instanceof Error ? err.message : String(err));
|
|
544
860
|
} finally {
|
|
861
|
+
submitting.current = false;
|
|
545
862
|
setBusy(false);
|
|
546
863
|
}
|
|
547
864
|
}, [collection, dispatch, fields, id, mode, onSuccess, values]);
|
|
548
865
|
if (loading)
|
|
549
|
-
return /* @__PURE__ */
|
|
866
|
+
return /* @__PURE__ */ jsx4(Message, {
|
|
550
867
|
tone: "muted",
|
|
551
868
|
children: "Loading…"
|
|
552
869
|
});
|
|
553
|
-
return /* @__PURE__ */
|
|
870
|
+
return /* @__PURE__ */ jsxs3("form", {
|
|
554
871
|
onSubmit: submit,
|
|
555
872
|
className: "flex w-full max-w-xl flex-col gap-3",
|
|
556
873
|
children: [
|
|
557
|
-
fields.map((f) => /* @__PURE__ */
|
|
874
|
+
fields.map((f) => /* @__PURE__ */ jsxs3("label", {
|
|
558
875
|
className: "flex flex-col gap-1",
|
|
559
876
|
children: [
|
|
560
|
-
/* @__PURE__ */
|
|
877
|
+
/* @__PURE__ */ jsx4("span", {
|
|
561
878
|
className: "text-sm text-neutral-600 dark:text-neutral-400",
|
|
562
879
|
children: f.label
|
|
563
880
|
}),
|
|
564
|
-
f.display && resolveRelation ? /* @__PURE__ */
|
|
881
|
+
f.display && resolveRelation ? /* @__PURE__ */ jsx4(RelationSelect, {
|
|
565
882
|
collection: resolveRelation(f.field),
|
|
566
883
|
displayField: f.display.field,
|
|
567
884
|
value: String(values[f.field] ?? ""),
|
|
568
|
-
onChange: (next) =>
|
|
569
|
-
}) : f.format === "boolean" ? /* @__PURE__ */
|
|
885
|
+
onChange: (next) => setField(f.field, next)
|
|
886
|
+
}) : f.format === "boolean" ? /* @__PURE__ */ jsx4("input", {
|
|
570
887
|
type: "checkbox",
|
|
571
888
|
className: "h-4 w-4",
|
|
572
889
|
checked: Boolean(values[f.field]),
|
|
573
|
-
onChange: (e) =>
|
|
574
|
-
}) : /* @__PURE__ */
|
|
890
|
+
onChange: (e) => setField(f.field, e.target.checked)
|
|
891
|
+
}) : f.options && f.options.length > 0 ? /* @__PURE__ */ jsxs3("select", {
|
|
892
|
+
className: INPUT_CLASS,
|
|
893
|
+
value: String(values[f.field] ?? ""),
|
|
894
|
+
onChange: (e) => setField(f.field, e.target.value),
|
|
895
|
+
children: [
|
|
896
|
+
/* @__PURE__ */ jsx4("option", {
|
|
897
|
+
value: "",
|
|
898
|
+
children: "—"
|
|
899
|
+
}),
|
|
900
|
+
f.options.map((opt) => /* @__PURE__ */ jsx4("option", {
|
|
901
|
+
value: opt,
|
|
902
|
+
children: opt
|
|
903
|
+
}, opt))
|
|
904
|
+
]
|
|
905
|
+
}) : f.format === "markdown" ? /* @__PURE__ */ jsx4(MarkdownField, {
|
|
906
|
+
value: String(values[f.field] ?? ""),
|
|
907
|
+
onChange: (next) => setField(f.field, next)
|
|
908
|
+
}) : /* @__PURE__ */ jsx4("input", {
|
|
575
909
|
type: inputType(f.format),
|
|
576
|
-
className:
|
|
910
|
+
className: INPUT_CLASS,
|
|
577
911
|
value: String(values[f.field] ?? ""),
|
|
578
|
-
onChange: (e) =>
|
|
912
|
+
onChange: (e) => setField(f.field, e.target.value)
|
|
579
913
|
})
|
|
580
914
|
]
|
|
581
915
|
}, f.field)),
|
|
582
|
-
error ? /* @__PURE__ */
|
|
916
|
+
error ? /* @__PURE__ */ jsx4(Message, {
|
|
583
917
|
tone: "error",
|
|
584
918
|
children: error
|
|
585
919
|
}) : null,
|
|
586
|
-
/* @__PURE__ */
|
|
587
|
-
|
|
920
|
+
done && !error ? /* @__PURE__ */ jsx4(Message, {
|
|
921
|
+
tone: "success",
|
|
922
|
+
children: mode === "create" ? "Saved." : "Changes saved."
|
|
923
|
+
}) : null,
|
|
924
|
+
/* @__PURE__ */ jsx4("div", {
|
|
925
|
+
children: /* @__PURE__ */ jsx4("button", {
|
|
588
926
|
type: "submit",
|
|
589
927
|
disabled: busy,
|
|
590
928
|
className: cx(BUTTON_BASE, VARIANT.default),
|
|
@@ -594,6 +932,291 @@ function Form(props) {
|
|
|
594
932
|
]
|
|
595
933
|
});
|
|
596
934
|
}
|
|
935
|
+
|
|
936
|
+
// src/routes.tsx
|
|
937
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo2, useState as useState4 } from "react";
|
|
938
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
939
|
+
function matchRoute(path, pattern) {
|
|
940
|
+
const clean = (path.split(/[?#]/)[0] ?? "").replace(/\/+$/, "") || "/";
|
|
941
|
+
const given = clean.split("/");
|
|
942
|
+
const parts = pattern.split("/");
|
|
943
|
+
if (given.length !== parts.length)
|
|
944
|
+
return null;
|
|
945
|
+
const params = {};
|
|
946
|
+
for (let i = 0;i < parts.length; i++) {
|
|
947
|
+
const seg = parts[i];
|
|
948
|
+
const value = given[i];
|
|
949
|
+
if (seg.startsWith(":")) {
|
|
950
|
+
if (value.length === 0)
|
|
951
|
+
return null;
|
|
952
|
+
params[seg.slice(1)] = value;
|
|
953
|
+
} else if (seg !== value) {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return params;
|
|
958
|
+
}
|
|
959
|
+
function paramCount(pattern) {
|
|
960
|
+
return pattern.split("/").filter((s) => s.startsWith(":")).length;
|
|
961
|
+
}
|
|
962
|
+
function resolveRoute(routes, path) {
|
|
963
|
+
const ordered = [...routes].sort((a, b) => paramCount(a.path) - paramCount(b.path));
|
|
964
|
+
for (const route of ordered) {
|
|
965
|
+
const params = matchRoute(path, route.path);
|
|
966
|
+
if (params !== null)
|
|
967
|
+
return { route, params };
|
|
968
|
+
}
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
function GroveRoutes(props) {
|
|
972
|
+
const { routes, context, fallback, children, gate } = props;
|
|
973
|
+
const [path, setPath] = useState4(() => window.location.pathname);
|
|
974
|
+
useEffect3(() => {
|
|
975
|
+
const onPop = () => setPath(window.location.pathname);
|
|
976
|
+
window.addEventListener("popstate", onPop);
|
|
977
|
+
return () => window.removeEventListener("popstate", onPop);
|
|
978
|
+
}, []);
|
|
979
|
+
const navigate = useCallback3((href) => {
|
|
980
|
+
window.history.pushState({}, "", href);
|
|
981
|
+
setPath(href.split(/[?#]/)[0] ?? href);
|
|
982
|
+
}, []);
|
|
983
|
+
const value = useMemo2(() => ({ ...context, navigate }), [context, navigate]);
|
|
984
|
+
const hit = resolveRoute(routes, path);
|
|
985
|
+
const isRoot = (path.replace(/\/+$/, "") || "/") === "/";
|
|
986
|
+
const matched = hit ? hit.route.render(hit.params) : fallback ?? (isRoot ? /* @__PURE__ */ jsx5(RouteIndex, {
|
|
987
|
+
routes
|
|
988
|
+
}) : /* @__PURE__ */ jsx5(NotFound, {}));
|
|
989
|
+
const screen = gate && hit && hit.route.requiresAuth !== false ? gate(matched) : matched;
|
|
990
|
+
return /* @__PURE__ */ jsxs4(GroveUiProvider, {
|
|
991
|
+
value,
|
|
992
|
+
children: [
|
|
993
|
+
children,
|
|
994
|
+
screen
|
|
995
|
+
]
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
function NotFound() {
|
|
999
|
+
const { navigate } = useGroveUi();
|
|
1000
|
+
return /* @__PURE__ */ jsxs4("div", {
|
|
1001
|
+
className: "flex flex-col gap-2 p-6",
|
|
1002
|
+
children: [
|
|
1003
|
+
/* @__PURE__ */ jsx5("p", {
|
|
1004
|
+
className: "text-sm text-neutral-500",
|
|
1005
|
+
children: "Not found."
|
|
1006
|
+
}),
|
|
1007
|
+
/* @__PURE__ */ jsx5("a", {
|
|
1008
|
+
href: "/",
|
|
1009
|
+
className: "w-fit text-sm underline",
|
|
1010
|
+
onClick: (e) => {
|
|
1011
|
+
e.preventDefault();
|
|
1012
|
+
navigate("/");
|
|
1013
|
+
},
|
|
1014
|
+
children: "Home"
|
|
1015
|
+
})
|
|
1016
|
+
]
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
function RouteIndex({ routes }) {
|
|
1020
|
+
const { navigate } = useGroveUi();
|
|
1021
|
+
const pages = routes.filter((r) => !r.path.split("/").some((seg) => seg.startsWith(":")));
|
|
1022
|
+
return /* @__PURE__ */ jsxs4("nav", {
|
|
1023
|
+
className: "flex flex-col gap-3 p-6",
|
|
1024
|
+
"aria-label": "Pages",
|
|
1025
|
+
children: [
|
|
1026
|
+
/* @__PURE__ */ jsx5("h1", {
|
|
1027
|
+
className: "text-2xl font-semibold",
|
|
1028
|
+
children: "Pages"
|
|
1029
|
+
}),
|
|
1030
|
+
/* @__PURE__ */ jsx5("ul", {
|
|
1031
|
+
className: "flex flex-col gap-1",
|
|
1032
|
+
children: pages.map((r) => /* @__PURE__ */ jsxs4("li", {
|
|
1033
|
+
children: [
|
|
1034
|
+
/* @__PURE__ */ jsx5("a", {
|
|
1035
|
+
href: r.path,
|
|
1036
|
+
className: "text-sm underline",
|
|
1037
|
+
onClick: (e) => {
|
|
1038
|
+
e.preventDefault();
|
|
1039
|
+
navigate(r.path);
|
|
1040
|
+
},
|
|
1041
|
+
children: r.title ?? r.path
|
|
1042
|
+
}),
|
|
1043
|
+
/* @__PURE__ */ jsx5("span", {
|
|
1044
|
+
className: "ml-2 text-xs text-neutral-500",
|
|
1045
|
+
children: r.path
|
|
1046
|
+
})
|
|
1047
|
+
]
|
|
1048
|
+
}, r.path))
|
|
1049
|
+
})
|
|
1050
|
+
]
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// src/SignIn.tsx
|
|
1055
|
+
import { useCallback as useCallback4, useRef as useRef3, useState as useState5 } from "react";
|
|
1056
|
+
import { jsx as jsx6, jsxs as jsxs5, Fragment as Fragment2 } from "react/jsx-runtime";
|
|
1057
|
+
function SignIn(props) {
|
|
1058
|
+
const { auth, title, allowRegister = typeof auth.register === "function", onSignedIn, children } = props;
|
|
1059
|
+
const [signedIn, setSignedIn] = useState5(() => auth.getTokens() !== null);
|
|
1060
|
+
const [mode, setMode] = useState5("login");
|
|
1061
|
+
const [email, setEmail] = useState5("");
|
|
1062
|
+
const [password, setPassword] = useState5("");
|
|
1063
|
+
const [name, setName] = useState5("");
|
|
1064
|
+
const [code, setCode] = useState5("");
|
|
1065
|
+
const [error, setError] = useState5(null);
|
|
1066
|
+
const [busy, setBusy] = useState5(false);
|
|
1067
|
+
const submitting = useRef3(false);
|
|
1068
|
+
const finish = useCallback4((user) => {
|
|
1069
|
+
setSignedIn(true);
|
|
1070
|
+
onSignedIn?.(user);
|
|
1071
|
+
}, [onSignedIn]);
|
|
1072
|
+
const submit = useCallback4(async (e) => {
|
|
1073
|
+
e.preventDefault();
|
|
1074
|
+
if (submitting.current)
|
|
1075
|
+
return;
|
|
1076
|
+
submitting.current = true;
|
|
1077
|
+
setBusy(true);
|
|
1078
|
+
setError(null);
|
|
1079
|
+
try {
|
|
1080
|
+
if (mode === "mfa") {
|
|
1081
|
+
if (!auth.verifyMfa)
|
|
1082
|
+
throw new Error("This account requires a second factor, which this app does not support.");
|
|
1083
|
+
finish(await auth.verifyMfa(code));
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (mode === "register") {
|
|
1087
|
+
if (!auth.register)
|
|
1088
|
+
throw new Error("Registration is not available.");
|
|
1089
|
+
await auth.register(email, password, name.trim().length > 0 ? name.trim() : undefined);
|
|
1090
|
+
}
|
|
1091
|
+
const user = await auth.login(email, password);
|
|
1092
|
+
if (user["mfaRequired"] === true) {
|
|
1093
|
+
setMode("mfa");
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
finish(user);
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1099
|
+
} finally {
|
|
1100
|
+
submitting.current = false;
|
|
1101
|
+
setBusy(false);
|
|
1102
|
+
}
|
|
1103
|
+
}, [auth, code, email, finish, mode, name, password]);
|
|
1104
|
+
if (signedIn)
|
|
1105
|
+
return /* @__PURE__ */ jsx6(Fragment2, {
|
|
1106
|
+
children
|
|
1107
|
+
});
|
|
1108
|
+
const heading = mode === "register" ? "Create account" : mode === "mfa" ? "Two-factor code" : title ?? "Sign in";
|
|
1109
|
+
const label = "text-sm text-neutral-600 dark:text-neutral-400";
|
|
1110
|
+
return /* @__PURE__ */ jsx6("div", {
|
|
1111
|
+
className: "flex min-h-screen items-center justify-center p-6",
|
|
1112
|
+
children: /* @__PURE__ */ jsxs5("form", {
|
|
1113
|
+
onSubmit: submit,
|
|
1114
|
+
className: "flex w-full max-w-sm flex-col gap-3",
|
|
1115
|
+
"aria-busy": busy,
|
|
1116
|
+
children: [
|
|
1117
|
+
/* @__PURE__ */ jsx6("h1", {
|
|
1118
|
+
className: "text-2xl font-semibold",
|
|
1119
|
+
children: heading
|
|
1120
|
+
}),
|
|
1121
|
+
mode === "mfa" ? /* @__PURE__ */ jsxs5("label", {
|
|
1122
|
+
className: "flex flex-col gap-1",
|
|
1123
|
+
children: [
|
|
1124
|
+
/* @__PURE__ */ jsx6("span", {
|
|
1125
|
+
className: label,
|
|
1126
|
+
children: "Code from your authenticator app, or a backup code"
|
|
1127
|
+
}),
|
|
1128
|
+
/* @__PURE__ */ jsx6("input", {
|
|
1129
|
+
className: INPUT_CLASS,
|
|
1130
|
+
autoComplete: "one-time-code",
|
|
1131
|
+
inputMode: "numeric",
|
|
1132
|
+
required: true,
|
|
1133
|
+
autoFocus: true,
|
|
1134
|
+
value: code,
|
|
1135
|
+
onChange: (e) => setCode(e.target.value)
|
|
1136
|
+
})
|
|
1137
|
+
]
|
|
1138
|
+
}) : /* @__PURE__ */ jsxs5(Fragment2, {
|
|
1139
|
+
children: [
|
|
1140
|
+
mode === "register" ? /* @__PURE__ */ jsxs5("label", {
|
|
1141
|
+
className: "flex flex-col gap-1",
|
|
1142
|
+
children: [
|
|
1143
|
+
/* @__PURE__ */ jsx6("span", {
|
|
1144
|
+
className: label,
|
|
1145
|
+
children: "Name"
|
|
1146
|
+
}),
|
|
1147
|
+
/* @__PURE__ */ jsx6("input", {
|
|
1148
|
+
className: INPUT_CLASS,
|
|
1149
|
+
autoComplete: "name",
|
|
1150
|
+
value: name,
|
|
1151
|
+
onChange: (e) => setName(e.target.value)
|
|
1152
|
+
})
|
|
1153
|
+
]
|
|
1154
|
+
}) : null,
|
|
1155
|
+
/* @__PURE__ */ jsxs5("label", {
|
|
1156
|
+
className: "flex flex-col gap-1",
|
|
1157
|
+
children: [
|
|
1158
|
+
/* @__PURE__ */ jsx6("span", {
|
|
1159
|
+
className: label,
|
|
1160
|
+
children: "Email"
|
|
1161
|
+
}),
|
|
1162
|
+
/* @__PURE__ */ jsx6("input", {
|
|
1163
|
+
className: INPUT_CLASS,
|
|
1164
|
+
type: "email",
|
|
1165
|
+
autoComplete: "email",
|
|
1166
|
+
required: true,
|
|
1167
|
+
value: email,
|
|
1168
|
+
onChange: (e) => setEmail(e.target.value)
|
|
1169
|
+
})
|
|
1170
|
+
]
|
|
1171
|
+
}),
|
|
1172
|
+
/* @__PURE__ */ jsxs5("label", {
|
|
1173
|
+
className: "flex flex-col gap-1",
|
|
1174
|
+
children: [
|
|
1175
|
+
/* @__PURE__ */ jsx6("span", {
|
|
1176
|
+
className: label,
|
|
1177
|
+
children: "Password"
|
|
1178
|
+
}),
|
|
1179
|
+
/* @__PURE__ */ jsx6("input", {
|
|
1180
|
+
className: INPUT_CLASS,
|
|
1181
|
+
type: "password",
|
|
1182
|
+
autoComplete: mode === "register" ? "new-password" : "current-password",
|
|
1183
|
+
required: true,
|
|
1184
|
+
value: password,
|
|
1185
|
+
onChange: (e) => setPassword(e.target.value)
|
|
1186
|
+
})
|
|
1187
|
+
]
|
|
1188
|
+
})
|
|
1189
|
+
]
|
|
1190
|
+
}),
|
|
1191
|
+
error ? /* @__PURE__ */ jsx6("p", {
|
|
1192
|
+
className: TONE.error,
|
|
1193
|
+
role: "alert",
|
|
1194
|
+
children: error
|
|
1195
|
+
}) : null,
|
|
1196
|
+
/* @__PURE__ */ jsxs5("div", {
|
|
1197
|
+
className: "flex items-center justify-between gap-3 pt-1",
|
|
1198
|
+
children: [
|
|
1199
|
+
/* @__PURE__ */ jsx6("button", {
|
|
1200
|
+
type: "submit",
|
|
1201
|
+
disabled: busy,
|
|
1202
|
+
className: cx(BUTTON_BASE, VARIANT.default),
|
|
1203
|
+
children: busy ? "Please wait…" : mode === "register" ? "Create account" : mode === "mfa" ? "Verify" : "Sign in"
|
|
1204
|
+
}),
|
|
1205
|
+
allowRegister && mode !== "mfa" ? /* @__PURE__ */ jsx6("button", {
|
|
1206
|
+
type: "button",
|
|
1207
|
+
className: cx(BUTTON_BASE, VARIANT.ghost),
|
|
1208
|
+
onClick: () => {
|
|
1209
|
+
setError(null);
|
|
1210
|
+
setMode(mode === "register" ? "login" : "register");
|
|
1211
|
+
},
|
|
1212
|
+
children: mode === "register" ? "Have an account? Sign in" : "Create an account"
|
|
1213
|
+
}) : null
|
|
1214
|
+
]
|
|
1215
|
+
})
|
|
1216
|
+
]
|
|
1217
|
+
})
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
597
1220
|
export {
|
|
598
1221
|
Button,
|
|
599
1222
|
DataTable,
|
|
@@ -601,13 +1224,23 @@ export {
|
|
|
601
1224
|
Divider,
|
|
602
1225
|
Form,
|
|
603
1226
|
Grid,
|
|
1227
|
+
GroveRoutes,
|
|
604
1228
|
GroveUiProvider,
|
|
1229
|
+
Header,
|
|
605
1230
|
Heading,
|
|
1231
|
+
MarkdownField,
|
|
606
1232
|
RelationSelect,
|
|
1233
|
+
RouteIndex,
|
|
1234
|
+
SignIn,
|
|
607
1235
|
Stack,
|
|
608
1236
|
Text,
|
|
1237
|
+
detectLocale,
|
|
609
1238
|
formatValue,
|
|
1239
|
+
matchRoute,
|
|
1240
|
+
renderMarkdown,
|
|
610
1241
|
resolveHref,
|
|
1242
|
+
resolveRoute,
|
|
611
1243
|
useAction,
|
|
612
|
-
useGroveUi
|
|
1244
|
+
useGroveUi,
|
|
1245
|
+
useMessages
|
|
613
1246
|
};
|