@protobase/ui 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/dist/activity-chart/activity-chart.js +125 -0
- package/dist/activity-chart/activity-math.js +11 -0
- package/dist/activity-chart/activity-tooltip.js +24 -0
- package/dist/app/action-icon.js +10 -0
- package/dist/app/app.d.ts +24 -0
- package/dist/app/app.js +118 -0
- package/dist/app/batch-failure.js +21 -0
- package/dist/app/breadcrumb-for.js +27 -0
- package/dist/app/create/create-page.js +118 -0
- package/dist/app/create/create-record-form.js +68 -0
- package/dist/app/create/create-values.js +52 -0
- package/dist/app/delete/dialogs.js +109 -0
- package/dist/app/delete/record-diff.js +17 -0
- package/dist/app/delete/use-delete-flow.js +193 -0
- package/dist/app/delete/use-restore.js +9 -0
- package/dist/app/draft-storage.js +18 -0
- package/dist/app/error-banner.js +28 -0
- package/dist/app/field-editor.js +88 -0
- package/dist/app/icons.js +27 -0
- package/dist/app/index.d.ts +2 -0
- package/dist/app/index.html +12 -0
- package/dist/app/list-chart.js +36 -0
- package/dist/app/list-order.js +15 -0
- package/dist/app/list-page.js +213 -0
- package/dist/app/list-search.js +30 -0
- package/dist/app/main.d.ts +1 -0
- package/dist/app/main.js +14 -0
- package/dist/app/menu-icons.js +21 -0
- package/dist/app/meta-gate.js +46 -0
- package/dist/app/nav-from-meta.js +65 -0
- package/dist/app/nav-recent.js +33 -0
- package/dist/app/pages/action-href.js +7 -0
- package/dist/app/pages/blocks/action.js +38 -0
- package/dist/app/pages/blocks/block-props.js +5 -0
- package/dist/app/pages/blocks/card-frame.js +34 -0
- package/dist/app/pages/blocks/card-row.js +55 -0
- package/dist/app/pages/blocks/card.js +19 -0
- package/dist/app/pages/blocks/field.js +45 -0
- package/dist/app/pages/blocks/grid.js +34 -0
- package/dist/app/pages/blocks/link.js +27 -0
- package/dist/app/pages/blocks/modal-form.js +149 -0
- package/dist/app/pages/blocks/page-block.js +20 -0
- package/dist/app/pages/blocks/progress.js +54 -0
- package/dist/app/pages/blocks/record-card.js +56 -0
- package/dist/app/pages/blocks/show.js +43 -0
- package/dist/app/pages/blocks/stat.js +78 -0
- package/dist/app/pages/blocks/table.js +65 -0
- package/dist/app/pages/composed-page.js +19 -0
- package/dist/app/pages/confirm-action.js +20 -0
- package/dist/app/pages/field-value.js +58 -0
- package/dist/app/pages/project-ui.d.ts +25 -0
- package/dist/app/pages/project-ui.js +13 -0
- package/dist/app/pages/record-actions.js +40 -0
- package/dist/app/pages/record-scope.d.ts +16 -0
- package/dist/app/pages/record-scope.js +12 -0
- package/dist/app/pages/record-table.js +110 -0
- package/dist/app/pages/render-node.js +84 -0
- package/dist/app/pages/use-layout-data.js +73 -0
- package/dist/app/pages/use-path-value.js +65 -0
- package/dist/app/pages/use-relation-labels.js +30 -0
- package/dist/app/pages/use-run-action.js +88 -0
- package/dist/app/permissions.js +15 -0
- package/dist/app/record-lines.js +39 -0
- package/dist/app/record-page.js +318 -0
- package/dist/app/related-section.js +126 -0
- package/dist/app/relation-picker.js +69 -0
- package/dist/app/router.js +155 -0
- package/dist/app/search/search-model.js +70 -0
- package/dist/app/search/use-global-search.js +68 -0
- package/dist/app/sensitive-field.js +65 -0
- package/dist/app/use-lines-draft.js +45 -0
- package/dist/app/use-lines.js +19 -0
- package/dist/app/use-live-save.js +54 -0
- package/dist/app/use-nav-recent.js +41 -0
- package/dist/app/use-record-editor.js +154 -0
- package/dist/app/use-related-records.js +31 -0
- package/dist/app/user-menu-from-meta.js +36 -0
- package/dist/app-shell/app-shell.d.ts +4 -0
- package/dist/app-shell/app-shell.js +57 -0
- package/dist/app-shell/breadcrumb.js +37 -0
- package/dist/app-shell/drawer.js +30 -0
- package/dist/app-shell/global-search.d.ts +1 -0
- package/dist/app-shell/global-search.js +145 -0
- package/dist/app-shell/index.d.ts +9 -0
- package/dist/app-shell/logo.js +46 -0
- package/dist/app-shell/nav-open-storage.js +10 -0
- package/dist/app-shell/nav.d.ts +2 -0
- package/dist/app-shell/nav.js +85 -0
- package/dist/app-shell/page-header.d.ts +1 -0
- package/dist/app-shell/page-header.js +17 -0
- package/dist/app-shell/profile-menu.d.ts +2 -0
- package/dist/app-shell/profile-menu.js +115 -0
- package/dist/app-shell/sidebar-group.js +94 -0
- package/dist/app-shell/sidebar-item.js +46 -0
- package/dist/app-shell/sidebar-mode.d.ts +4 -0
- package/dist/app-shell/sidebar-mode.js +13 -0
- package/dist/app-shell/sidebar.d.ts +2 -0
- package/dist/app-shell/sidebar.js +64 -0
- package/dist/app-shell/status-dot.d.ts +1 -0
- package/dist/app-shell/status-dot.js +20 -0
- package/dist/app-shell/top-bar.js +82 -0
- package/dist/auth/auth-gate.js +70 -0
- package/dist/auth/auth-layout.js +29 -0
- package/dist/auth/auth-provider.js +64 -0
- package/dist/auth/first-run-page.js +51 -0
- package/dist/auth/forgot-password-form.js +75 -0
- package/dist/auth/forgot-password-page.js +35 -0
- package/dist/auth/reset-link.js +29 -0
- package/dist/auth/reset-password-form.js +88 -0
- package/dist/auth/reset-password-page.js +43 -0
- package/dist/auth/sign-in-form.js +74 -0
- package/dist/auth/sign-in-page.js +38 -0
- package/dist/auth/user-to-shell.js +18 -0
- package/dist/data/api-provider.d.ts +54 -0
- package/dist/data/api-provider.js +53 -0
- package/dist/data/meta-data.d.ts +14 -0
- package/dist/data/meta-data.js +9 -0
- package/dist/data/query-keys.js +95 -0
- package/dist/data/use-aggregates.d.ts +8 -0
- package/dist/data/use-aggregates.js +43 -0
- package/dist/data/use-list.d.ts +231 -0
- package/dist/data/use-list.js +31 -0
- package/dist/data/use-meta.d.ts +8 -0
- package/dist/data/use-meta.js +35 -0
- package/dist/data/use-record.d.ts +6 -0
- package/dist/data/use-record.js +16 -0
- package/dist/data/use-update-record.d.ts +15 -0
- package/dist/data/use-update-record.js +26 -0
- package/dist/data-grid/cells.js +85 -0
- package/dist/data-grid/column-spec.js +4 -0
- package/dist/data-grid/columns.js +16 -0
- package/dist/data-grid/data-grid.js +238 -0
- package/dist/data-grid/geometry.js +32 -0
- package/dist/data-grid/grid-footer.js +33 -0
- package/dist/data-grid/grid-header.js +69 -0
- package/dist/data-grid/grid-row.js +83 -0
- package/dist/data-grid/grid-template.js +12 -0
- package/dist/data-grid/header-menu.js +52 -0
- package/dist/data-grid/page-cache.js +27 -0
- package/dist/data-grid/pinned-storage.js +13 -0
- package/dist/data-grid/row-menu.js +88 -0
- package/dist/data-grid/use-grid-scroll.js +80 -0
- package/dist/data-grid/use-horizontal-scroll.js +38 -0
- package/dist/data-grid/use-paged-rows.js +50 -0
- package/dist/data-grid/use-pinned-columns.js +16 -0
- package/dist/filter-panel/active-filter-chips.js +34 -0
- package/dist/filter-panel/date-presets.js +20 -0
- package/dist/filter-panel/facet-group.js +48 -0
- package/dist/filter-panel/filter-config.js +18 -0
- package/dist/filter-panel/filter-menu.js +41 -0
- package/dist/filter-panel/filter-panel.js +112 -0
- package/dist/filter-panel/filter-section.js +11 -0
- package/dist/filter-panel/filter-state.js +63 -0
- package/dist/filter-panel/range-filter.js +54 -0
- package/dist/filter-panel/range-math.js +11 -0
- package/dist/filter-panel/range-slider.css +32 -0
- package/dist/filter-panel/toggle-row.js +13 -0
- package/dist/filter-panel/use-filter-state.js +15 -0
- package/dist/format/date.js +37 -0
- package/dist/format/money.js +15 -0
- package/dist/format/number.js +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +10 -0
- package/dist/inline-child-table/columns.js +4 -0
- package/dist/inline-child-table/inline-child-table.js +120 -0
- package/dist/inline-child-table/line-menu.js +54 -0
- package/dist/inline-child-table/line-row.js +91 -0
- package/dist/inline-child-table/number-cell.js +25 -0
- package/dist/inline-child-table/reorder.js +10 -0
- package/dist/inline-child-table/totals-footer.js +42 -0
- package/dist/lib/cn.js +4 -0
- package/dist/lib/guarded.js +12 -0
- package/dist/lib/use-debounced.js +13 -0
- package/dist/lib/use-focus-trap.js +46 -0
- package/dist/lib/use-media-query.js +12 -0
- package/dist/live/columns-from-model.js +93 -0
- package/dist/live/display-value.js +24 -0
- package/dist/live/field-values.js +32 -0
- package/dist/live/filter-string.js +135 -0
- package/dist/live/grid-loader.js +63 -0
- package/dist/live/model-helpers.js +30 -0
- package/dist/live/naming.js +9 -0
- package/dist/live/relation-labels.js +53 -0
- package/dist/live/use-filter-config.js +88 -0
- package/dist/live/use-live-rows.js +45 -0
- package/dist/live/view-fields.js +35 -0
- package/dist/mocks/activity.js +24 -0
- package/dist/mocks/invoices.js +13 -0
- package/dist/mocks/products.js +167 -0
- package/dist/mocks/rng.js +20 -0
- package/dist/mocks/users.js +63 -0
- package/dist/primitives/avatar.js +22 -0
- package/dist/primitives/badge.d.ts +1 -0
- package/dist/primitives/badge.js +25 -0
- package/dist/primitives/button.js +23 -0
- package/dist/primitives/checkbox.js +21 -0
- package/dist/primitives/dialog.js +41 -0
- package/dist/primitives/input.js +16 -0
- package/dist/primitives/kbd.js +8 -0
- package/dist/primitives/segmented.js +14 -0
- package/dist/primitives/sheet.js +44 -0
- package/dist/primitives/skeleton.js +6 -0
- package/dist/primitives/spinner.js +10 -0
- package/dist/primitives/switch.js +14 -0
- package/dist/primitives/tooltip.js +13 -0
- package/dist/record-view/action-rail.js +28 -0
- package/dist/record-view/field.js +62 -0
- package/dist/record-view/record-header.js +50 -0
- package/dist/record-view/record-sidebar.js +58 -0
- package/dist/record-view/section.js +21 -0
- package/dist/styles.css +132 -0
- package/dist/toasts/toast-provider.js +69 -0
- package/dist/toasts/toast.js +45 -0
- package/package.json +45 -3
- package/README.md +0 -3
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { navOpenStorageKey, readNavOpen, writeNavOpen } from "./nav-open-storage.js";
|
|
3
|
+
import { SidebarItem } from "./sidebar-item.js";
|
|
4
|
+
import { StatusDot } from "./status-dot.js";
|
|
5
|
+
import { useId, useState } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { ChevronRight } from "lucide-react";
|
|
8
|
+
//#region src/app-shell/sidebar-group.tsx
|
|
9
|
+
const row = {
|
|
10
|
+
"text-large": "h-8 gap-2.5 px-2.5 text-[13px]",
|
|
11
|
+
"text-small": "h-7 gap-2 px-2 text-xs"
|
|
12
|
+
};
|
|
13
|
+
/** Lines the records up under the entry's icon. */
|
|
14
|
+
const indent = {
|
|
15
|
+
"text-large": "ml-[20px]",
|
|
16
|
+
"text-small": "ml-[17px]"
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* A sidebar entry with a few of its records under it. The label opens the list, the chevron shows or hides the
|
|
20
|
+
* records (remembered in this browser), and "View all" at the end opens the list too.
|
|
21
|
+
*/
|
|
22
|
+
const SidebarGroup = ({ item, recent, mode, active, onNavigate }) => {
|
|
23
|
+
const storageKey = navOpenStorageKey(item.id);
|
|
24
|
+
const [open, setOpen] = useState(() => readNavOpen(localStorage, storageKey));
|
|
25
|
+
const listId = useId();
|
|
26
|
+
const href = item.href ?? `#${item.id}`;
|
|
27
|
+
const toggle = () => {
|
|
28
|
+
writeNavOpen(localStorage, storageKey, !open);
|
|
29
|
+
setOpen(!open);
|
|
30
|
+
};
|
|
31
|
+
const link = (to) => ({
|
|
32
|
+
href: to,
|
|
33
|
+
onClick: onNavigate && ((event) => {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
onNavigate(to);
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
39
|
+
className: "flex flex-col gap-0.5",
|
|
40
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
41
|
+
className: "relative",
|
|
42
|
+
children: [/* @__PURE__ */ jsx(SidebarItem, {
|
|
43
|
+
item,
|
|
44
|
+
mode,
|
|
45
|
+
active,
|
|
46
|
+
onNavigate,
|
|
47
|
+
className: "pr-8"
|
|
48
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
49
|
+
type: "button",
|
|
50
|
+
"aria-expanded": open,
|
|
51
|
+
"aria-controls": listId,
|
|
52
|
+
"aria-label": `Recent ${item.label.toLowerCase()}`,
|
|
53
|
+
onClick: toggle,
|
|
54
|
+
className: "absolute right-1 top-1/2 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded text-faint-foreground hover:bg-muted hover:text-foreground",
|
|
55
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { className: cn("size-3.5 transition-transform", open && "rotate-90") })
|
|
56
|
+
})]
|
|
57
|
+
}), open && /* @__PURE__ */ jsxs("ul", {
|
|
58
|
+
id: listId,
|
|
59
|
+
"aria-label": `Recent ${item.label.toLowerCase()}`,
|
|
60
|
+
className: cn("flex flex-col gap-0.5 border-l pl-1.5", indent[mode]),
|
|
61
|
+
children: [
|
|
62
|
+
recent.state === "loading" && /* @__PURE__ */ jsx("li", {
|
|
63
|
+
className: cn("flex items-center text-faint-foreground", row[mode]),
|
|
64
|
+
children: "Loading..."
|
|
65
|
+
}),
|
|
66
|
+
recent.state === "error" && /* @__PURE__ */ jsx("li", {
|
|
67
|
+
className: cn("flex items-center text-faint-foreground", row[mode]),
|
|
68
|
+
children: "Could not load"
|
|
69
|
+
}),
|
|
70
|
+
recent.state === "ready" && recent.records.map((record) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", {
|
|
71
|
+
...link(record.href),
|
|
72
|
+
"aria-label": `${record.label}, ${record.status}`,
|
|
73
|
+
title: `${record.label} (${record.status})`,
|
|
74
|
+
"aria-current": record.active ? "page" : void 0,
|
|
75
|
+
className: cn("flex min-w-0 items-center rounded-md font-medium transition-colors", row[mode], record.active ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground"),
|
|
76
|
+
children: [/* @__PURE__ */ jsx(StatusDot, {
|
|
77
|
+
tone: record.tone,
|
|
78
|
+
pulse: record.pulse
|
|
79
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
80
|
+
className: "truncate",
|
|
81
|
+
children: record.label
|
|
82
|
+
})]
|
|
83
|
+
}) }, record.id)),
|
|
84
|
+
/* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("a", {
|
|
85
|
+
...link(href),
|
|
86
|
+
className: cn("flex items-center rounded-md text-faint-foreground transition-colors hover:bg-muted hover:text-foreground", row[mode]),
|
|
87
|
+
children: "View all"
|
|
88
|
+
}) })
|
|
89
|
+
]
|
|
90
|
+
})]
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
//#endregion
|
|
94
|
+
export { SidebarGroup };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { Tooltip } from "../primitives/tooltip.js";
|
|
3
|
+
import { isTextMode, showsLabelInline } from "./sidebar-mode.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/app-shell/sidebar-item.tsx
|
|
6
|
+
const layout = {
|
|
7
|
+
"text-large": "h-9 gap-3 px-3 text-sm",
|
|
8
|
+
"text-small": "h-7 gap-2.5 px-2.5 text-xs",
|
|
9
|
+
icon: "size-9 justify-center",
|
|
10
|
+
"icon-label": "w-full flex-col gap-0.5 py-1.5 text-[10px]",
|
|
11
|
+
"icon-tooltip": "size-9 justify-center",
|
|
12
|
+
"icon-expand": "h-8 gap-3 px-3 text-[13px]"
|
|
13
|
+
};
|
|
14
|
+
const SidebarItem = ({ item, mode, active, onNavigate, className }) => {
|
|
15
|
+
const Icon = item.icon;
|
|
16
|
+
const button = /* @__PURE__ */ jsxs("a", {
|
|
17
|
+
onClick: onNavigate && ((event) => {
|
|
18
|
+
event.preventDefault();
|
|
19
|
+
onNavigate(item.href ?? `#${item.id}`);
|
|
20
|
+
}),
|
|
21
|
+
href: item.href ?? `#${item.id}`,
|
|
22
|
+
"aria-label": item.label,
|
|
23
|
+
"aria-current": active ? "page" : void 0,
|
|
24
|
+
className: cn("flex shrink-0 items-center rounded-md font-medium transition-colors", layout[mode], active ? "bg-primary-soft text-primary-text" : "text-muted-foreground hover:bg-muted hover:text-foreground", className),
|
|
25
|
+
children: [
|
|
26
|
+
/* @__PURE__ */ jsx(Icon, {
|
|
27
|
+
className: cn("shrink-0", mode === "text-small" ? "size-4" : "size-[18px]"),
|
|
28
|
+
strokeWidth: active ? 2.2 : 1.9
|
|
29
|
+
}),
|
|
30
|
+
(showsLabelInline(mode) || mode === "icon-label") && /* @__PURE__ */ jsx("span", {
|
|
31
|
+
className: "whitespace-nowrap",
|
|
32
|
+
children: item.label
|
|
33
|
+
}),
|
|
34
|
+
isTextMode(mode) && item.count && /* @__PURE__ */ jsx("span", {
|
|
35
|
+
className: "ml-auto rounded bg-muted px-1.5 text-[11px] tabular-nums text-muted-foreground",
|
|
36
|
+
children: item.count
|
|
37
|
+
})
|
|
38
|
+
]
|
|
39
|
+
});
|
|
40
|
+
return mode === "icon-tooltip" ? /* @__PURE__ */ jsx(Tooltip, {
|
|
41
|
+
label: item.label,
|
|
42
|
+
children: button
|
|
43
|
+
}) : button;
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
export { SidebarItem };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/app-shell/sidebar-mode.ts
|
|
2
|
+
const showsLabelInline = (mode) => mode === "text-large" || mode === "text-small" || mode === "icon-expand";
|
|
3
|
+
const isTextMode = (mode) => mode === "text-large" || mode === "text-small";
|
|
4
|
+
const sidebarWidth = {
|
|
5
|
+
"text-large": "w-60",
|
|
6
|
+
"text-small": "w-52",
|
|
7
|
+
icon: "w-14",
|
|
8
|
+
"icon-label": "w-[76px]",
|
|
9
|
+
"icon-tooltip": "w-14",
|
|
10
|
+
"icon-expand": "w-14"
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { isTextMode, showsLabelInline, sidebarWidth };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { Logo } from "./logo.js";
|
|
3
|
+
import { navGroups } from "./nav.js";
|
|
4
|
+
import { ProfileMenu } from "./profile-menu.js";
|
|
5
|
+
import { isTextMode, sidebarWidth } from "./sidebar-mode.js";
|
|
6
|
+
import { SidebarItem } from "./sidebar-item.js";
|
|
7
|
+
import { SidebarGroup } from "./sidebar-group.js";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
//#region src/app-shell/sidebar.tsx
|
|
10
|
+
const Sidebar = ({ mode, activeItem, user, workspace, forceExpanded, profileMenuOpen, groups = navGroups, userMenu, onNavigate, onSignOut }) => {
|
|
11
|
+
const expanding = mode === "icon-expand";
|
|
12
|
+
const expanded = expanding && forceExpanded;
|
|
13
|
+
const inline = isTextMode(mode) || expanding;
|
|
14
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
15
|
+
"aria-label": "Resources",
|
|
16
|
+
className: cn("relative z-30 shrink-0", sidebarWidth[mode]),
|
|
17
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
18
|
+
className: cn("group/side absolute inset-y-0 left-0 flex flex-col overflow-hidden border-r bg-sidebar", expanding ? "w-14 transition-[width,box-shadow] duration-150 hover:w-60 hover:shadow-pop focus-within:w-60 focus-within:shadow-pop" : "w-full", expanded && "w-60 shadow-pop"),
|
|
19
|
+
children: [
|
|
20
|
+
/* @__PURE__ */ jsx("div", {
|
|
21
|
+
className: cn("flex h-12 shrink-0 items-center px-3.5", mode === "icon-label" && "justify-center px-0"),
|
|
22
|
+
children: /* @__PURE__ */ jsx(Logo, {
|
|
23
|
+
showName: isTextMode(mode) || expanding,
|
|
24
|
+
workspace
|
|
25
|
+
})
|
|
26
|
+
}),
|
|
27
|
+
/* @__PURE__ */ jsx("div", {
|
|
28
|
+
className: cn("flex-1 overflow-y-auto overflow-x-hidden pb-2", mode === "icon-label" ? "px-2" : "px-2"),
|
|
29
|
+
children: groups.map((group, index) => /* @__PURE__ */ jsxs("div", {
|
|
30
|
+
className: cn("flex flex-col gap-0.5", index > 0 && "mt-3"),
|
|
31
|
+
children: [inline ? /* @__PURE__ */ jsx("div", {
|
|
32
|
+
className: cn("px-3 pb-1 pt-1 text-[11px] font-medium uppercase tracking-wide text-faint-foreground", expanding && "h-4 whitespace-nowrap py-0 opacity-0 transition-opacity group-hover/side:opacity-100 group-focus-within/side:opacity-100", expanded && "opacity-100"),
|
|
33
|
+
children: group.label
|
|
34
|
+
}) : index > 0 && /* @__PURE__ */ jsx("div", { className: "mx-auto mb-1 h-px w-6 bg-border" }), group.items.map((item) => item.recent && isTextMode(mode) ? /* @__PURE__ */ jsx(SidebarGroup, {
|
|
35
|
+
item,
|
|
36
|
+
recent: item.recent,
|
|
37
|
+
mode,
|
|
38
|
+
active: item.id === activeItem,
|
|
39
|
+
onNavigate
|
|
40
|
+
}, item.id) : /* @__PURE__ */ jsx(SidebarItem, {
|
|
41
|
+
item,
|
|
42
|
+
mode,
|
|
43
|
+
active: item.id === activeItem,
|
|
44
|
+
onNavigate
|
|
45
|
+
}, item.id))]
|
|
46
|
+
}, group.label))
|
|
47
|
+
}),
|
|
48
|
+
/* @__PURE__ */ jsx("div", {
|
|
49
|
+
className: cn("shrink-0 border-t p-2", mode !== "text-large" && mode !== "text-small" && !expanding && "flex justify-center"),
|
|
50
|
+
children: /* @__PURE__ */ jsx(ProfileMenu, {
|
|
51
|
+
user,
|
|
52
|
+
compact: !inline,
|
|
53
|
+
open: profileMenuOpen,
|
|
54
|
+
items: userMenu,
|
|
55
|
+
onNavigate,
|
|
56
|
+
onSignOut
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
]
|
|
60
|
+
})
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
//#endregion
|
|
64
|
+
export { Sidebar };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@protobase/schema";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region src/app-shell/status-dot.tsx
|
|
4
|
+
const colour = {
|
|
5
|
+
neutral: "bg-faint-foreground",
|
|
6
|
+
info: "bg-primary",
|
|
7
|
+
success: "bg-success",
|
|
8
|
+
warning: "bg-warning",
|
|
9
|
+
danger: "bg-danger"
|
|
10
|
+
};
|
|
11
|
+
/** A record's status as a coloured dot; `pulse` animates a ring around it for states in progress (still under reduced motion). */
|
|
12
|
+
const StatusDot = ({ tone, pulse = false, className }) => /* @__PURE__ */ jsxs("span", {
|
|
13
|
+
"aria-hidden": true,
|
|
14
|
+
className: cn("relative flex size-2 shrink-0", className),
|
|
15
|
+
"data-tone": tone,
|
|
16
|
+
"data-pulse": pulse || void 0,
|
|
17
|
+
children: [pulse && /* @__PURE__ */ jsx("span", { className: cn("absolute inset-0 animate-ping rounded-full opacity-60 motion-reduce:animate-none", colour[tone]) }), /* @__PURE__ */ jsx("span", { className: cn("relative size-2 rounded-full", colour[tone]) })]
|
|
18
|
+
});
|
|
19
|
+
//#endregion
|
|
20
|
+
export { StatusDot };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { Breadcrumb } from "./breadcrumb.js";
|
|
3
|
+
import { GlobalSearch } from "./global-search.js";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { Menu, Search, X } from "lucide-react";
|
|
7
|
+
//#region src/app-shell/top-bar.tsx
|
|
8
|
+
const iconButton = "inline-flex size-11 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted md:hidden";
|
|
9
|
+
const TopBar = ({ breadcrumb, onNavigate, actions, onMenu, menuOpen, menuControls, search }) => {
|
|
10
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
11
|
+
const desktopSearch = useRef(null);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!search) return;
|
|
14
|
+
const onKey = (event) => {
|
|
15
|
+
if (event.key.toLowerCase() !== "k" || !(event.metaKey || event.ctrlKey)) return;
|
|
16
|
+
event.preventDefault();
|
|
17
|
+
const input = desktopSearch.current;
|
|
18
|
+
if (input && input.offsetParent !== null) input.focus();
|
|
19
|
+
else setSearchOpen(true);
|
|
20
|
+
};
|
|
21
|
+
window.addEventListener("keydown", onKey);
|
|
22
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
23
|
+
}, [search]);
|
|
24
|
+
return /* @__PURE__ */ jsxs("header", {
|
|
25
|
+
className: "relative flex h-12 shrink-0 items-center gap-1 border-b bg-background px-2 md:gap-4 md:px-5",
|
|
26
|
+
children: [
|
|
27
|
+
onMenu && /* @__PURE__ */ jsx("button", {
|
|
28
|
+
type: "button",
|
|
29
|
+
"aria-label": "Open navigation",
|
|
30
|
+
"aria-expanded": Boolean(menuOpen),
|
|
31
|
+
"aria-controls": menuControls,
|
|
32
|
+
onClick: onMenu,
|
|
33
|
+
className: cn(iconButton, "-ml-0.5"),
|
|
34
|
+
children: /* @__PURE__ */ jsx(Menu, { className: "size-5" })
|
|
35
|
+
}),
|
|
36
|
+
/* @__PURE__ */ jsx(Breadcrumb, {
|
|
37
|
+
parts: breadcrumb,
|
|
38
|
+
onNavigate
|
|
39
|
+
}),
|
|
40
|
+
/* @__PURE__ */ jsxs("div", {
|
|
41
|
+
className: "ml-auto flex items-center gap-1 md:gap-3",
|
|
42
|
+
children: [search && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
|
|
43
|
+
className: "hidden md:block",
|
|
44
|
+
children: /* @__PURE__ */ jsx(GlobalSearch, {
|
|
45
|
+
ref: desktopSearch,
|
|
46
|
+
search
|
|
47
|
+
})
|
|
48
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
49
|
+
type: "button",
|
|
50
|
+
"aria-label": "Search",
|
|
51
|
+
"aria-expanded": searchOpen,
|
|
52
|
+
onClick: () => setSearchOpen(true),
|
|
53
|
+
className: iconButton,
|
|
54
|
+
children: /* @__PURE__ */ jsx(Search, { className: "size-5" })
|
|
55
|
+
})] }), actions]
|
|
56
|
+
}),
|
|
57
|
+
search && searchOpen && /* @__PURE__ */ jsxs("div", {
|
|
58
|
+
className: "absolute inset-0 z-20 flex items-center gap-1 bg-background px-3 md:hidden",
|
|
59
|
+
children: [/* @__PURE__ */ jsx(GlobalSearch, {
|
|
60
|
+
className: "min-w-0 flex-1",
|
|
61
|
+
autoFocus: true,
|
|
62
|
+
showShortcut: false,
|
|
63
|
+
search: {
|
|
64
|
+
...search,
|
|
65
|
+
onSelect: (href) => {
|
|
66
|
+
search.onSelect(href);
|
|
67
|
+
setSearchOpen(false);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
71
|
+
type: "button",
|
|
72
|
+
"aria-label": "Close search",
|
|
73
|
+
onClick: () => setSearchOpen(false),
|
|
74
|
+
className: iconButton,
|
|
75
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-5" })
|
|
76
|
+
})]
|
|
77
|
+
})
|
|
78
|
+
]
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
export { TopBar };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Spinner } from "../primitives/spinner.js";
|
|
2
|
+
import { useAuth } from "./auth-provider.js";
|
|
3
|
+
import { Button } from "../primitives/button.js";
|
|
4
|
+
import { FirstRunPage } from "./first-run-page.js";
|
|
5
|
+
import { readResetLink, withoutResetLink } from "./reset-link.js";
|
|
6
|
+
import { ForgotPasswordPage } from "./forgot-password-page.js";
|
|
7
|
+
import { ResetPasswordPage } from "./reset-password-page.js";
|
|
8
|
+
import { SignInPage } from "./sign-in-page.js";
|
|
9
|
+
import { useEffect, useState } from "react";
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
//#region src/auth/auth-gate.tsx
|
|
12
|
+
const firstPage = () => {
|
|
13
|
+
const link = readResetLink(window.location.href);
|
|
14
|
+
return link ? {
|
|
15
|
+
kind: "reset-password",
|
|
16
|
+
token: link.token
|
|
17
|
+
} : { kind: "sign-in" };
|
|
18
|
+
};
|
|
19
|
+
const forgetResetLink = () => window.history.replaceState(window.history.state, "", withoutResetLink(window.location.href));
|
|
20
|
+
/** Shows the app when the user is signed in (or the server has no login), and the right page otherwise. */
|
|
21
|
+
const AuthGate = ({ workspace, children }) => {
|
|
22
|
+
const { state, recheck, passwordReset, signOut } = useAuth();
|
|
23
|
+
const [page, setPage] = useState(firstPage);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (state.kind === "signed-in") setPage((current) => current.kind === "sign-in" && current.notice ? { kind: "sign-in" } : current);
|
|
26
|
+
}, [state.kind]);
|
|
27
|
+
const leaveResetPage = (next) => {
|
|
28
|
+
forgetResetLink();
|
|
29
|
+
setPage(next);
|
|
30
|
+
};
|
|
31
|
+
const passwordChanged = async () => {
|
|
32
|
+
if (state.kind === "signed-in") await signOut();
|
|
33
|
+
leaveResetPage({
|
|
34
|
+
kind: "sign-in",
|
|
35
|
+
notice: "Your password is changed. Sign in with the new one."
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
switch (state.kind) {
|
|
39
|
+
case "loading": return /* @__PURE__ */ jsxs("div", {
|
|
40
|
+
className: "flex h-full min-h-48 items-center justify-center gap-2 text-[13px] text-muted-foreground",
|
|
41
|
+
children: [/* @__PURE__ */ jsx(Spinner, {}), " Loading"]
|
|
42
|
+
});
|
|
43
|
+
case "error": return /* @__PURE__ */ jsxs("div", {
|
|
44
|
+
className: "flex h-full min-h-48 flex-col items-center justify-center gap-3 p-8 text-center",
|
|
45
|
+
children: [/* @__PURE__ */ jsxs("p", {
|
|
46
|
+
className: "text-[13px] text-muted-foreground",
|
|
47
|
+
children: ["Cannot reach the server: ", state.message]
|
|
48
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
49
|
+
onClick: recheck,
|
|
50
|
+
children: "Try again"
|
|
51
|
+
})]
|
|
52
|
+
});
|
|
53
|
+
case "needs-admin": return /* @__PURE__ */ jsx(FirstRunPage, { onCheckAgain: recheck });
|
|
54
|
+
}
|
|
55
|
+
if (page.kind === "reset-password") return /* @__PURE__ */ jsx(ResetPasswordPage, {
|
|
56
|
+
token: page.token,
|
|
57
|
+
onDone: () => void passwordChanged(),
|
|
58
|
+
onRequestNewLink: passwordReset ? () => leaveResetPage({ kind: "forgot-password" }) : void 0,
|
|
59
|
+
onBack: () => leaveResetPage({ kind: "sign-in" })
|
|
60
|
+
});
|
|
61
|
+
if (state.kind === "signed-in") return children;
|
|
62
|
+
if (page.kind === "forgot-password") return /* @__PURE__ */ jsx(ForgotPasswordPage, { onBack: () => setPage({ kind: "sign-in" }) });
|
|
63
|
+
return /* @__PURE__ */ jsx(SignInPage, {
|
|
64
|
+
workspace,
|
|
65
|
+
notice: page.notice,
|
|
66
|
+
onForgotPassword: passwordReset ? () => setPage({ kind: "forgot-password" }) : void 0
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
//#endregion
|
|
70
|
+
export { AuthGate };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { LogoMark } from "../app-shell/logo.js";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region src/auth/auth-layout.tsx
|
|
4
|
+
/** The frame of the sign-in and first-run pages: logo, title and a card. */
|
|
5
|
+
const AuthLayout = ({ title, description, children }) => /* @__PURE__ */ jsx("div", {
|
|
6
|
+
className: "flex min-h-full w-full items-center justify-center bg-surface px-4 py-10",
|
|
7
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
8
|
+
className: "w-full max-w-sm",
|
|
9
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
10
|
+
className: "mb-6 flex flex-col items-center text-center",
|
|
11
|
+
children: [
|
|
12
|
+
/* @__PURE__ */ jsx(LogoMark, { className: "size-10" }),
|
|
13
|
+
/* @__PURE__ */ jsx("h1", {
|
|
14
|
+
className: "mt-4 text-xl font-semibold tracking-tight",
|
|
15
|
+
children: title
|
|
16
|
+
}),
|
|
17
|
+
description && /* @__PURE__ */ jsx("p", {
|
|
18
|
+
className: "mt-1 text-[13px] text-muted-foreground",
|
|
19
|
+
children: description
|
|
20
|
+
})
|
|
21
|
+
]
|
|
22
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
23
|
+
className: "rounded-lg border bg-background p-5 shadow-sm",
|
|
24
|
+
children
|
|
25
|
+
})]
|
|
26
|
+
})
|
|
27
|
+
});
|
|
28
|
+
//#endregion
|
|
29
|
+
export { AuthLayout };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createAuthSession } from "@protobase/client";
|
|
4
|
+
//#region src/auth/auth-provider.tsx
|
|
5
|
+
const AuthContext = createContext(null);
|
|
6
|
+
const useAuth = () => {
|
|
7
|
+
const api = useContext(AuthContext);
|
|
8
|
+
if (!api) throw new Error("useAuth must be used inside AuthProvider");
|
|
9
|
+
return api;
|
|
10
|
+
};
|
|
11
|
+
/** Finds out whether the server has a login and who is signed in, and keeps that state for the app. */
|
|
12
|
+
const AuthProvider = ({ session: given, children }) => {
|
|
13
|
+
const [state, setState] = useState({ kind: "loading" });
|
|
14
|
+
const [passwordReset, setPasswordReset] = useState(false);
|
|
15
|
+
const markSignedOut = useCallback(() => setState((current) => current.kind === "signed-in" ? { kind: "signed-out" } : current), []);
|
|
16
|
+
const signedOut = useRef(markSignedOut);
|
|
17
|
+
signedOut.current = markSignedOut;
|
|
18
|
+
const session = useMemo(() => given ?? createAuthSession({ onSignedOut: () => signedOut.current() }), [given]);
|
|
19
|
+
const recheck = useCallback(() => {
|
|
20
|
+
setState({ kind: "loading" });
|
|
21
|
+
session.status().then(async (status) => {
|
|
22
|
+
setPasswordReset(status.passwordReset);
|
|
23
|
+
if (status.needsAdmin) return setState({ kind: "needs-admin" });
|
|
24
|
+
const user = await session.session();
|
|
25
|
+
setState(user ? {
|
|
26
|
+
kind: "signed-in",
|
|
27
|
+
user
|
|
28
|
+
} : { kind: "signed-out" });
|
|
29
|
+
}, (error) => setState({
|
|
30
|
+
kind: "error",
|
|
31
|
+
message: error instanceof Error ? error.message : "The server did not answer"
|
|
32
|
+
}));
|
|
33
|
+
}, [session]);
|
|
34
|
+
useEffect(recheck, [recheck]);
|
|
35
|
+
const api = useMemo(() => ({
|
|
36
|
+
state,
|
|
37
|
+
session,
|
|
38
|
+
passwordReset,
|
|
39
|
+
markSignedOut,
|
|
40
|
+
recheck,
|
|
41
|
+
signIn: async (email, password) => {
|
|
42
|
+
setState({
|
|
43
|
+
kind: "signed-in",
|
|
44
|
+
user: await session.signIn(email, password)
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
signOut: async () => {
|
|
48
|
+
await session.signOut();
|
|
49
|
+
setState({ kind: "signed-out" });
|
|
50
|
+
}
|
|
51
|
+
}), [
|
|
52
|
+
state,
|
|
53
|
+
session,
|
|
54
|
+
passwordReset,
|
|
55
|
+
markSignedOut,
|
|
56
|
+
recheck
|
|
57
|
+
]);
|
|
58
|
+
return /* @__PURE__ */ jsx(AuthContext.Provider, {
|
|
59
|
+
value: api,
|
|
60
|
+
children
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
//#endregion
|
|
64
|
+
export { AuthProvider, useAuth };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Button } from "../primitives/button.js";
|
|
2
|
+
import { AuthLayout } from "./auth-layout.js";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { Check, Copy } from "lucide-react";
|
|
6
|
+
//#region src/auth/first-run-page.tsx
|
|
7
|
+
const createAdminCommand = "protobase users create you@example.com";
|
|
8
|
+
/**
|
|
9
|
+
* Shown while the admin store has no user. Accounts are not created over the web (a fresh URL must not be claimable
|
|
10
|
+
* by whoever reaches it first), so this says how to create the first admin on the host.
|
|
11
|
+
*/
|
|
12
|
+
const FirstRunPage = ({ command = createAdminCommand, onCheckAgain, busy }) => {
|
|
13
|
+
const [copied, setCopied] = useState(false);
|
|
14
|
+
return /* @__PURE__ */ jsxs(AuthLayout, {
|
|
15
|
+
title: "No admin yet",
|
|
16
|
+
description: "This Protobase has no users to sign in with.",
|
|
17
|
+
children: [
|
|
18
|
+
/* @__PURE__ */ jsx("p", {
|
|
19
|
+
className: "text-[13px] text-muted-foreground",
|
|
20
|
+
children: "Run this in the terminal where Protobase runs, then reload. The first admin is created on the server, not in the browser, so nobody else can claim a fresh URL."
|
|
21
|
+
}),
|
|
22
|
+
/* @__PURE__ */ jsxs("div", {
|
|
23
|
+
className: "mt-4 flex items-center gap-2 rounded-md border bg-surface py-1.5 pl-3 pr-1.5",
|
|
24
|
+
children: [/* @__PURE__ */ jsx("code", {
|
|
25
|
+
className: "min-w-0 flex-1 break-all font-mono text-xs",
|
|
26
|
+
children: command
|
|
27
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
28
|
+
type: "button",
|
|
29
|
+
"aria-label": "Copy command",
|
|
30
|
+
onClick: () => {
|
|
31
|
+
navigator.clipboard.writeText(command).then(() => {
|
|
32
|
+
setCopied(true);
|
|
33
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
className: "inline-flex size-8 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted pointer-coarse:size-11",
|
|
37
|
+
children: copied ? /* @__PURE__ */ jsx(Check, { className: "size-4 text-success" }) : /* @__PURE__ */ jsx(Copy, { className: "size-4" })
|
|
38
|
+
})]
|
|
39
|
+
}),
|
|
40
|
+
/* @__PURE__ */ jsx(Button, {
|
|
41
|
+
variant: "primary",
|
|
42
|
+
className: "mt-4 min-h-10 w-full",
|
|
43
|
+
loading: busy,
|
|
44
|
+
onClick: onCheckAgain,
|
|
45
|
+
children: "Reload"
|
|
46
|
+
})
|
|
47
|
+
]
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
export { FirstRunPage, createAdminCommand };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Input } from "../primitives/input.js";
|
|
2
|
+
import { Button } from "../primitives/button.js";
|
|
3
|
+
import { AuthLayout } from "./auth-layout.js";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/auth/forgot-password-form.tsx
|
|
7
|
+
const ForgotPasswordForm = ({ onSubmit, onBack, busy, error, sentTo }) => {
|
|
8
|
+
const [email, setEmail] = useState("");
|
|
9
|
+
if (sentTo) return /* @__PURE__ */ jsxs(AuthLayout, {
|
|
10
|
+
title: "Check your email",
|
|
11
|
+
children: [/* @__PURE__ */ jsxs("p", {
|
|
12
|
+
className: "text-[13px] text-muted-foreground",
|
|
13
|
+
children: [
|
|
14
|
+
"If ",
|
|
15
|
+
/* @__PURE__ */ jsx("span", {
|
|
16
|
+
className: "font-medium text-foreground",
|
|
17
|
+
children: sentTo
|
|
18
|
+
}),
|
|
19
|
+
" has an account, an email with a link to choose a new password is on its way. The link works once, for a limited time."
|
|
20
|
+
]
|
|
21
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
22
|
+
variant: "primary",
|
|
23
|
+
className: "mt-4 min-h-10 w-full",
|
|
24
|
+
onClick: onBack,
|
|
25
|
+
children: "Back to sign in"
|
|
26
|
+
})]
|
|
27
|
+
});
|
|
28
|
+
return /* @__PURE__ */ jsx(AuthLayout, {
|
|
29
|
+
title: "Reset your password",
|
|
30
|
+
description: "We email you a link to choose a new one.",
|
|
31
|
+
children: /* @__PURE__ */ jsxs("form", {
|
|
32
|
+
className: "flex flex-col gap-4",
|
|
33
|
+
onSubmit: (event) => {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
onSubmit(email.trim());
|
|
36
|
+
},
|
|
37
|
+
children: [
|
|
38
|
+
/* @__PURE__ */ jsxs("label", {
|
|
39
|
+
className: "flex flex-col gap-1.5 text-[13px] font-medium",
|
|
40
|
+
children: ["Email", /* @__PURE__ */ jsx(Input, {
|
|
41
|
+
type: "email",
|
|
42
|
+
name: "email",
|
|
43
|
+
autoComplete: "username",
|
|
44
|
+
required: true,
|
|
45
|
+
autoFocus: true,
|
|
46
|
+
value: email,
|
|
47
|
+
invalid: Boolean(error),
|
|
48
|
+
onChange: (event) => setEmail(event.target.value),
|
|
49
|
+
className: "min-h-6"
|
|
50
|
+
})]
|
|
51
|
+
}),
|
|
52
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
53
|
+
role: "alert",
|
|
54
|
+
className: "text-xs font-medium text-danger-text",
|
|
55
|
+
children: error
|
|
56
|
+
}),
|
|
57
|
+
/* @__PURE__ */ jsx(Button, {
|
|
58
|
+
variant: "primary",
|
|
59
|
+
type: "submit",
|
|
60
|
+
loading: busy,
|
|
61
|
+
className: "min-h-10",
|
|
62
|
+
children: "Send reset link"
|
|
63
|
+
}),
|
|
64
|
+
/* @__PURE__ */ jsx(Button, {
|
|
65
|
+
variant: "ghost",
|
|
66
|
+
onClick: onBack,
|
|
67
|
+
className: "min-h-10",
|
|
68
|
+
children: "Back to sign in"
|
|
69
|
+
})
|
|
70
|
+
]
|
|
71
|
+
})
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
export { ForgotPasswordForm };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useAuth } from "./auth-provider.js";
|
|
2
|
+
import { ForgotPasswordForm } from "./forgot-password-form.js";
|
|
3
|
+
import { resetLinkTarget } from "./reset-link.js";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { AuthError } from "@protobase/client";
|
|
7
|
+
//#region src/auth/forgot-password-page.tsx
|
|
8
|
+
/** Asks the server to email a reset link that leads back to this page. */
|
|
9
|
+
const ForgotPasswordPage = ({ onBack }) => {
|
|
10
|
+
const { session } = useAuth();
|
|
11
|
+
const [busy, setBusy] = useState(false);
|
|
12
|
+
const [error, setError] = useState();
|
|
13
|
+
const [sentTo, setSentTo] = useState();
|
|
14
|
+
const submit = async (email) => {
|
|
15
|
+
setBusy(true);
|
|
16
|
+
setError(void 0);
|
|
17
|
+
try {
|
|
18
|
+
await session.requestPasswordReset(email, resetLinkTarget(window.location.href));
|
|
19
|
+
setSentTo(email);
|
|
20
|
+
} catch (failure) {
|
|
21
|
+
if (!(failure instanceof AuthError)) throw failure;
|
|
22
|
+
setError(failure.status === 429 ? "Too many requests. Wait a minute and try again." : failure.message);
|
|
23
|
+
}
|
|
24
|
+
setBusy(false);
|
|
25
|
+
};
|
|
26
|
+
return /* @__PURE__ */ jsx(ForgotPasswordForm, {
|
|
27
|
+
busy,
|
|
28
|
+
error,
|
|
29
|
+
sentTo,
|
|
30
|
+
onBack,
|
|
31
|
+
onSubmit: (email) => void submit(email)
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
export { ForgotPasswordPage };
|