@groveback/ui 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -3
- package/dist/MarkdownField.d.ts +19 -0
- package/dist/RelationSelect.d.ts +1 -1
- package/dist/SignIn.d.ts +38 -0
- package/dist/components.d.ts +28 -6
- package/dist/index.d.ts +24 -6
- package/dist/index.js +796 -164
- package/dist/routes.d.ts +72 -0
- package/dist/runtime.d.ts +21 -3
- package/dist/styles.d.ts +20 -0
- package/dist/types.d.ts +39 -7
- package/package.json +9 -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
|
+
|
|
4
|
+
// src/MarkdownField.tsx
|
|
5
|
+
import { useState } from "react";
|
|
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
|
+
}
|
|
3
127
|
|
|
4
128
|
// src/RelationSelect.tsx
|
|
5
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
-
import {
|
|
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
|
-
}
|
|
77
|
-
/* @__PURE__ */
|
|
200
|
+
}),
|
|
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
|
-
}
|
|
84
|
-
open ? /* @__PURE__ */
|
|
207
|
+
}),
|
|
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
|
-
}
|
|
94
|
-
/* @__PURE__ */
|
|
217
|
+
}),
|
|
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("");
|
|
@@ -104,15 +228,15 @@ function RelationSelect(props) {
|
|
|
104
228
|
},
|
|
105
229
|
className: "w-full px-3 py-1.5 text-left text-sm text-neutral-500 hover:bg-neutral-100 dark:hover:bg-neutral-900",
|
|
106
230
|
children: "None"
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
matches.length === 0 ? /* @__PURE__ */
|
|
231
|
+
})
|
|
232
|
+
}) : null,
|
|
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);
|
|
@@ -121,34 +245,34 @@ function RelationSelect(props) {
|
|
|
121
245
|
},
|
|
122
246
|
className: `w-full px-3 py-1.5 text-left text-sm hover:bg-neutral-100 dark:hover:bg-neutral-900 ${id === value ? "font-medium" : ""}`,
|
|
123
247
|
children: labelOf(doc, displayField)
|
|
124
|
-
}
|
|
125
|
-
}, id
|
|
248
|
+
})
|
|
249
|
+
}, id);
|
|
126
250
|
})
|
|
127
251
|
]
|
|
128
|
-
}
|
|
129
|
-
truncated ? /* @__PURE__ */
|
|
252
|
+
}),
|
|
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 ",
|
|
133
257
|
PAGE,
|
|
134
258
|
". Type to filter within them."
|
|
135
259
|
]
|
|
136
|
-
}
|
|
260
|
+
}) : null
|
|
137
261
|
]
|
|
138
|
-
}
|
|
262
|
+
}) : null
|
|
139
263
|
]
|
|
140
|
-
}
|
|
264
|
+
});
|
|
141
265
|
}
|
|
142
266
|
|
|
143
267
|
// src/runtime.tsx
|
|
144
268
|
import { createContext, useContext, useCallback } from "react";
|
|
145
|
-
import {
|
|
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
|
+
});
|
|
152
276
|
}
|
|
153
277
|
var DEFAULT_CONTEXT = {
|
|
154
278
|
navigate: (href) => {
|
|
@@ -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,86 +344,257 @@ 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 {
|
|
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
|
+
});
|
|
446
|
+
}
|
|
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
|
+
});
|
|
269
569
|
}
|
|
270
|
-
var VARIANT = {
|
|
271
|
-
default: "bg-neutral-900 text-white hover:bg-neutral-800 dark:bg-white dark:text-neutral-900",
|
|
272
|
-
secondary: "bg-neutral-100 text-neutral-900 hover:bg-neutral-200 dark:bg-neutral-800 dark:text-neutral-100",
|
|
273
|
-
destructive: "bg-red-600 text-white hover:bg-red-700",
|
|
274
|
-
outline: "border border-neutral-300 hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-900",
|
|
275
|
-
ghost: "hover:bg-neutral-100 dark:hover:bg-neutral-800"
|
|
276
|
-
};
|
|
277
|
-
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";
|
|
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,
|
|
290
589
|
className: cx(BUTTON_BASE, VARIANT[variant]),
|
|
291
590
|
children
|
|
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,69 +666,69 @@ 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
|
-
}
|
|
384
|
-
return /* @__PURE__ */
|
|
682
|
+
});
|
|
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
|
-
}, c.field
|
|
397
|
-
rowActions && rowActions.length > 0 ? /* @__PURE__ */
|
|
695
|
+
}, c.field)),
|
|
696
|
+
rowActions && rowActions.length > 0 ? /* @__PURE__ */ jsx4("th", {
|
|
398
697
|
className: "px-3 py-2"
|
|
399
|
-
}
|
|
698
|
+
}) : null
|
|
400
699
|
]
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
/* @__PURE__ */
|
|
404
|
-
children: data.map((doc, i) => /* @__PURE__ */
|
|
700
|
+
})
|
|
701
|
+
}),
|
|
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
|
-
}, c.field
|
|
412
|
-
rowActions && rowActions.length > 0 ? /* @__PURE__ */
|
|
710
|
+
}, c.field)),
|
|
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,
|
|
421
720
|
collection,
|
|
422
721
|
onDone: reload,
|
|
423
722
|
children: a.label
|
|
424
|
-
}, a.label
|
|
425
|
-
}
|
|
426
|
-
}
|
|
723
|
+
}, a.label))
|
|
724
|
+
})
|
|
725
|
+
}) : null
|
|
427
726
|
]
|
|
428
|
-
}, typeof doc["id"] === "string" ? doc["id"] : i
|
|
429
|
-
}
|
|
727
|
+
}, typeof doc["id"] === "string" ? doc["id"] : i))
|
|
728
|
+
})
|
|
430
729
|
]
|
|
431
|
-
}
|
|
432
|
-
}
|
|
730
|
+
})
|
|
731
|
+
});
|
|
433
732
|
}
|
|
434
733
|
function Detail(props) {
|
|
435
734
|
const { collection, id, fields, resolveRelation } = props;
|
|
@@ -437,36 +736,39 @@ 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
|
-
}
|
|
454
|
-
return /* @__PURE__ */
|
|
752
|
+
});
|
|
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
|
-
}
|
|
463
|
-
/* @__PURE__ */
|
|
761
|
+
}),
|
|
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
|
+
})
|
|
467
769
|
]
|
|
468
|
-
}, f.field
|
|
469
|
-
}
|
|
770
|
+
}, f.field))
|
|
771
|
+
});
|
|
470
772
|
}
|
|
471
773
|
function inputType(format) {
|
|
472
774
|
switch (format) {
|
|
@@ -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,76 +850,396 @@ 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
|
-
}
|
|
553
|
-
return /* @__PURE__ */
|
|
869
|
+
});
|
|
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
|
-
}
|
|
564
|
-
f.display && resolveRelation ? /* @__PURE__ */
|
|
880
|
+
}),
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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) =>
|
|
579
|
-
}
|
|
912
|
+
onChange: (e) => setField(f.field, e.target.value)
|
|
913
|
+
})
|
|
580
914
|
]
|
|
581
|
-
}, f.field
|
|
582
|
-
error ? /* @__PURE__ */
|
|
915
|
+
}, f.field)),
|
|
916
|
+
error ? /* @__PURE__ */ jsx4(Message, {
|
|
583
917
|
tone: "error",
|
|
584
918
|
children: error
|
|
585
|
-
}
|
|
586
|
-
/* @__PURE__ */
|
|
587
|
-
|
|
919
|
+
}) : null,
|
|
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),
|
|
591
929
|
children: busy ? "Saving…" : submitLabel ?? "Save"
|
|
592
|
-
}
|
|
593
|
-
}
|
|
930
|
+
})
|
|
931
|
+
})
|
|
594
932
|
]
|
|
595
|
-
}
|
|
933
|
+
});
|
|
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 } = 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 screen = hit ? hit.route.render(hit.params) : fallback ?? (isRoot ? /* @__PURE__ */ jsx5(RouteIndex, {
|
|
987
|
+
routes
|
|
988
|
+
}) : /* @__PURE__ */ jsx5(NotFound, {}));
|
|
989
|
+
return /* @__PURE__ */ jsxs4(GroveUiProvider, {
|
|
990
|
+
value,
|
|
991
|
+
children: [
|
|
992
|
+
children,
|
|
993
|
+
screen
|
|
994
|
+
]
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
function NotFound() {
|
|
998
|
+
const { navigate } = useGroveUi();
|
|
999
|
+
return /* @__PURE__ */ jsxs4("div", {
|
|
1000
|
+
className: "flex flex-col gap-2 p-6",
|
|
1001
|
+
children: [
|
|
1002
|
+
/* @__PURE__ */ jsx5("p", {
|
|
1003
|
+
className: "text-sm text-neutral-500",
|
|
1004
|
+
children: "Not found."
|
|
1005
|
+
}),
|
|
1006
|
+
/* @__PURE__ */ jsx5("a", {
|
|
1007
|
+
href: "/",
|
|
1008
|
+
className: "w-fit text-sm underline",
|
|
1009
|
+
onClick: (e) => {
|
|
1010
|
+
e.preventDefault();
|
|
1011
|
+
navigate("/");
|
|
1012
|
+
},
|
|
1013
|
+
children: "Home"
|
|
1014
|
+
})
|
|
1015
|
+
]
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
function RouteIndex({ routes }) {
|
|
1019
|
+
const { navigate } = useGroveUi();
|
|
1020
|
+
const pages = routes.filter((r) => !r.path.split("/").some((seg) => seg.startsWith(":")));
|
|
1021
|
+
return /* @__PURE__ */ jsxs4("nav", {
|
|
1022
|
+
className: "flex flex-col gap-3 p-6",
|
|
1023
|
+
"aria-label": "Pages",
|
|
1024
|
+
children: [
|
|
1025
|
+
/* @__PURE__ */ jsx5("h1", {
|
|
1026
|
+
className: "text-2xl font-semibold",
|
|
1027
|
+
children: "Pages"
|
|
1028
|
+
}),
|
|
1029
|
+
/* @__PURE__ */ jsx5("ul", {
|
|
1030
|
+
className: "flex flex-col gap-1",
|
|
1031
|
+
children: pages.map((r) => /* @__PURE__ */ jsxs4("li", {
|
|
1032
|
+
children: [
|
|
1033
|
+
/* @__PURE__ */ jsx5("a", {
|
|
1034
|
+
href: r.path,
|
|
1035
|
+
className: "text-sm underline",
|
|
1036
|
+
onClick: (e) => {
|
|
1037
|
+
e.preventDefault();
|
|
1038
|
+
navigate(r.path);
|
|
1039
|
+
},
|
|
1040
|
+
children: r.title ?? r.path
|
|
1041
|
+
}),
|
|
1042
|
+
/* @__PURE__ */ jsx5("span", {
|
|
1043
|
+
className: "ml-2 text-xs text-neutral-500",
|
|
1044
|
+
children: r.path
|
|
1045
|
+
})
|
|
1046
|
+
]
|
|
1047
|
+
}, r.path))
|
|
1048
|
+
})
|
|
1049
|
+
]
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// src/SignIn.tsx
|
|
1054
|
+
import { useCallback as useCallback4, useRef as useRef3, useState as useState5 } from "react";
|
|
1055
|
+
import { jsx as jsx6, jsxs as jsxs5, Fragment as Fragment2 } from "react/jsx-runtime";
|
|
1056
|
+
function SignIn(props) {
|
|
1057
|
+
const { auth, title, allowRegister = typeof auth.register === "function", onSignedIn, children } = props;
|
|
1058
|
+
const [signedIn, setSignedIn] = useState5(() => auth.getTokens() !== null);
|
|
1059
|
+
const [mode, setMode] = useState5("login");
|
|
1060
|
+
const [email, setEmail] = useState5("");
|
|
1061
|
+
const [password, setPassword] = useState5("");
|
|
1062
|
+
const [name, setName] = useState5("");
|
|
1063
|
+
const [code, setCode] = useState5("");
|
|
1064
|
+
const [error, setError] = useState5(null);
|
|
1065
|
+
const [busy, setBusy] = useState5(false);
|
|
1066
|
+
const submitting = useRef3(false);
|
|
1067
|
+
const finish = useCallback4((user) => {
|
|
1068
|
+
setSignedIn(true);
|
|
1069
|
+
onSignedIn?.(user);
|
|
1070
|
+
}, [onSignedIn]);
|
|
1071
|
+
const submit = useCallback4(async (e) => {
|
|
1072
|
+
e.preventDefault();
|
|
1073
|
+
if (submitting.current)
|
|
1074
|
+
return;
|
|
1075
|
+
submitting.current = true;
|
|
1076
|
+
setBusy(true);
|
|
1077
|
+
setError(null);
|
|
1078
|
+
try {
|
|
1079
|
+
if (mode === "mfa") {
|
|
1080
|
+
if (!auth.verifyMfa)
|
|
1081
|
+
throw new Error("This account requires a second factor, which this app does not support.");
|
|
1082
|
+
finish(await auth.verifyMfa(code));
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (mode === "register") {
|
|
1086
|
+
if (!auth.register)
|
|
1087
|
+
throw new Error("Registration is not available.");
|
|
1088
|
+
await auth.register(email, password, name.trim().length > 0 ? name.trim() : undefined);
|
|
1089
|
+
}
|
|
1090
|
+
const user = await auth.login(email, password);
|
|
1091
|
+
if (user["mfaRequired"] === true) {
|
|
1092
|
+
setMode("mfa");
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
finish(user);
|
|
1096
|
+
} catch (err) {
|
|
1097
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1098
|
+
} finally {
|
|
1099
|
+
submitting.current = false;
|
|
1100
|
+
setBusy(false);
|
|
1101
|
+
}
|
|
1102
|
+
}, [auth, code, email, finish, mode, name, password]);
|
|
1103
|
+
if (signedIn)
|
|
1104
|
+
return /* @__PURE__ */ jsx6(Fragment2, {
|
|
1105
|
+
children
|
|
1106
|
+
});
|
|
1107
|
+
const heading = mode === "register" ? "Create account" : mode === "mfa" ? "Two-factor code" : title ?? "Sign in";
|
|
1108
|
+
const label = "text-sm text-neutral-600 dark:text-neutral-400";
|
|
1109
|
+
return /* @__PURE__ */ jsx6("div", {
|
|
1110
|
+
className: "flex min-h-screen items-center justify-center p-6",
|
|
1111
|
+
children: /* @__PURE__ */ jsxs5("form", {
|
|
1112
|
+
onSubmit: submit,
|
|
1113
|
+
className: "flex w-full max-w-sm flex-col gap-3",
|
|
1114
|
+
"aria-busy": busy,
|
|
1115
|
+
children: [
|
|
1116
|
+
/* @__PURE__ */ jsx6("h1", {
|
|
1117
|
+
className: "text-2xl font-semibold",
|
|
1118
|
+
children: heading
|
|
1119
|
+
}),
|
|
1120
|
+
mode === "mfa" ? /* @__PURE__ */ jsxs5("label", {
|
|
1121
|
+
className: "flex flex-col gap-1",
|
|
1122
|
+
children: [
|
|
1123
|
+
/* @__PURE__ */ jsx6("span", {
|
|
1124
|
+
className: label,
|
|
1125
|
+
children: "Code from your authenticator app, or a backup code"
|
|
1126
|
+
}),
|
|
1127
|
+
/* @__PURE__ */ jsx6("input", {
|
|
1128
|
+
className: INPUT_CLASS,
|
|
1129
|
+
autoComplete: "one-time-code",
|
|
1130
|
+
inputMode: "numeric",
|
|
1131
|
+
required: true,
|
|
1132
|
+
autoFocus: true,
|
|
1133
|
+
value: code,
|
|
1134
|
+
onChange: (e) => setCode(e.target.value)
|
|
1135
|
+
})
|
|
1136
|
+
]
|
|
1137
|
+
}) : /* @__PURE__ */ jsxs5(Fragment2, {
|
|
1138
|
+
children: [
|
|
1139
|
+
mode === "register" ? /* @__PURE__ */ jsxs5("label", {
|
|
1140
|
+
className: "flex flex-col gap-1",
|
|
1141
|
+
children: [
|
|
1142
|
+
/* @__PURE__ */ jsx6("span", {
|
|
1143
|
+
className: label,
|
|
1144
|
+
children: "Name"
|
|
1145
|
+
}),
|
|
1146
|
+
/* @__PURE__ */ jsx6("input", {
|
|
1147
|
+
className: INPUT_CLASS,
|
|
1148
|
+
autoComplete: "name",
|
|
1149
|
+
value: name,
|
|
1150
|
+
onChange: (e) => setName(e.target.value)
|
|
1151
|
+
})
|
|
1152
|
+
]
|
|
1153
|
+
}) : null,
|
|
1154
|
+
/* @__PURE__ */ jsxs5("label", {
|
|
1155
|
+
className: "flex flex-col gap-1",
|
|
1156
|
+
children: [
|
|
1157
|
+
/* @__PURE__ */ jsx6("span", {
|
|
1158
|
+
className: label,
|
|
1159
|
+
children: "Email"
|
|
1160
|
+
}),
|
|
1161
|
+
/* @__PURE__ */ jsx6("input", {
|
|
1162
|
+
className: INPUT_CLASS,
|
|
1163
|
+
type: "email",
|
|
1164
|
+
autoComplete: "email",
|
|
1165
|
+
required: true,
|
|
1166
|
+
value: email,
|
|
1167
|
+
onChange: (e) => setEmail(e.target.value)
|
|
1168
|
+
})
|
|
1169
|
+
]
|
|
1170
|
+
}),
|
|
1171
|
+
/* @__PURE__ */ jsxs5("label", {
|
|
1172
|
+
className: "flex flex-col gap-1",
|
|
1173
|
+
children: [
|
|
1174
|
+
/* @__PURE__ */ jsx6("span", {
|
|
1175
|
+
className: label,
|
|
1176
|
+
children: "Password"
|
|
1177
|
+
}),
|
|
1178
|
+
/* @__PURE__ */ jsx6("input", {
|
|
1179
|
+
className: INPUT_CLASS,
|
|
1180
|
+
type: "password",
|
|
1181
|
+
autoComplete: mode === "register" ? "new-password" : "current-password",
|
|
1182
|
+
required: true,
|
|
1183
|
+
value: password,
|
|
1184
|
+
onChange: (e) => setPassword(e.target.value)
|
|
1185
|
+
})
|
|
1186
|
+
]
|
|
1187
|
+
})
|
|
1188
|
+
]
|
|
1189
|
+
}),
|
|
1190
|
+
error ? /* @__PURE__ */ jsx6("p", {
|
|
1191
|
+
className: TONE.error,
|
|
1192
|
+
role: "alert",
|
|
1193
|
+
children: error
|
|
1194
|
+
}) : null,
|
|
1195
|
+
/* @__PURE__ */ jsxs5("div", {
|
|
1196
|
+
className: "flex items-center justify-between gap-3 pt-1",
|
|
1197
|
+
children: [
|
|
1198
|
+
/* @__PURE__ */ jsx6("button", {
|
|
1199
|
+
type: "submit",
|
|
1200
|
+
disabled: busy,
|
|
1201
|
+
className: cx(BUTTON_BASE, VARIANT.default),
|
|
1202
|
+
children: busy ? "Please wait…" : mode === "register" ? "Create account" : mode === "mfa" ? "Verify" : "Sign in"
|
|
1203
|
+
}),
|
|
1204
|
+
allowRegister && mode !== "mfa" ? /* @__PURE__ */ jsx6("button", {
|
|
1205
|
+
type: "button",
|
|
1206
|
+
className: cx(BUTTON_BASE, VARIANT.ghost),
|
|
1207
|
+
onClick: () => {
|
|
1208
|
+
setError(null);
|
|
1209
|
+
setMode(mode === "register" ? "login" : "register");
|
|
1210
|
+
},
|
|
1211
|
+
children: mode === "register" ? "Have an account? Sign in" : "Create an account"
|
|
1212
|
+
}) : null
|
|
1213
|
+
]
|
|
1214
|
+
})
|
|
1215
|
+
]
|
|
1216
|
+
})
|
|
1217
|
+
});
|
|
596
1218
|
}
|
|
597
1219
|
export {
|
|
598
|
-
|
|
599
|
-
useAction,
|
|
600
|
-
resolveHref,
|
|
601
|
-
formatValue,
|
|
602
|
-
Text,
|
|
603
|
-
Stack,
|
|
604
|
-
RelationSelect,
|
|
605
|
-
Heading,
|
|
606
|
-
GroveUiProvider,
|
|
607
|
-
Grid,
|
|
608
|
-
Form,
|
|
609
|
-
Divider,
|
|
610
|
-
Detail,
|
|
1220
|
+
Button,
|
|
611
1221
|
DataTable,
|
|
612
|
-
|
|
1222
|
+
Detail,
|
|
1223
|
+
Divider,
|
|
1224
|
+
Form,
|
|
1225
|
+
Grid,
|
|
1226
|
+
GroveRoutes,
|
|
1227
|
+
GroveUiProvider,
|
|
1228
|
+
Header,
|
|
1229
|
+
Heading,
|
|
1230
|
+
MarkdownField,
|
|
1231
|
+
RelationSelect,
|
|
1232
|
+
RouteIndex,
|
|
1233
|
+
SignIn,
|
|
1234
|
+
Stack,
|
|
1235
|
+
Text,
|
|
1236
|
+
detectLocale,
|
|
1237
|
+
formatValue,
|
|
1238
|
+
matchRoute,
|
|
1239
|
+
renderMarkdown,
|
|
1240
|
+
resolveHref,
|
|
1241
|
+
resolveRoute,
|
|
1242
|
+
useAction,
|
|
1243
|
+
useGroveUi,
|
|
1244
|
+
useMessages
|
|
613
1245
|
};
|