@protobase/ui 0.3.5 → 0.5.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.
Files changed (100) hide show
  1. package/dist/account/account-pages.js +20 -0
  2. package/dist/account/account-section.js +31 -0
  3. package/dist/account/account-security-page.js +59 -0
  4. package/dist/account/backup-codes.js +44 -0
  5. package/dist/account/passkeys-section.js +141 -0
  6. package/dist/account/password-dialog.js +58 -0
  7. package/dist/account/sign-in-policy-form.js +108 -0
  8. package/dist/account/sign-in-policy-labels.js +76 -0
  9. package/dist/account/sign-in-policy-page.js +72 -0
  10. package/dist/account/staff-sign-in-log.js +62 -0
  11. package/dist/account/totp-qr.js +37 -0
  12. package/dist/account/two-factor-section.js +123 -0
  13. package/dist/account/two-factor-setup.js +188 -0
  14. package/dist/activity-chart/activity-chart.js +1 -1
  15. package/dist/activity-chart/activity-tooltip.js +1 -1
  16. package/dist/app/app.js +37 -15
  17. package/dist/app/assistant-shell.js +3 -2
  18. package/dist/app/create/create-page.js +1 -1
  19. package/dist/app/delete/use-delete-flow.js +1 -1
  20. package/dist/app/error-banner.js +1 -1
  21. package/dist/app/list-page.js +1 -1
  22. package/dist/app/list-search.js +1 -1
  23. package/dist/app/meta-gate.js +1 -1
  24. package/dist/app/pages/blocks/modal-form.js +3 -3
  25. package/dist/app/pages/merge-ui.d.ts +9 -0
  26. package/dist/app/pages/merge-ui.js +45 -0
  27. package/dist/app/pages/project-ui-provider.js +15 -0
  28. package/dist/app/pages/project-ui.d.ts +6 -3
  29. package/dist/app/pages/project-ui.js +3 -7
  30. package/dist/app/pages/record-table.js +1 -1
  31. package/dist/app/pages/use-run-action.js +1 -1
  32. package/dist/app/record-page.js +5 -5
  33. package/dist/app/runtime/runtime-update.js +1 -1
  34. package/dist/app/sensitive-field.js +1 -1
  35. package/dist/app/staff-banner.js +67 -0
  36. package/dist/app/use-record-editor.js +1 -1
  37. package/dist/app-shell/breadcrumb.js +1 -1
  38. package/dist/app-shell/global-search.js +1 -1
  39. package/dist/app-shell/profile-menu.js +1 -1
  40. package/dist/app-shell/sidebar-group.js +1 -1
  41. package/dist/app-shell/top-bar.js +1 -1
  42. package/dist/assistant-dock/assistant-dock.d.ts +8 -2
  43. package/dist/assistant-dock/assistant-dock.js +20 -13
  44. package/dist/assistant-dock/index.d.ts +3 -1
  45. package/dist/assistant-dock/part-view.d.ts +1 -1
  46. package/dist/assistant-dock/part-view.js +3 -1
  47. package/dist/assistant-dock/use-backend-data.d.ts +23 -0
  48. package/dist/assistant-dock/use-backend-data.js +67 -0
  49. package/dist/assistant-dock/widget-view.d.ts +1 -0
  50. package/dist/assistant-dock/widget-view.js +44 -0
  51. package/dist/auth/auth-gate.js +42 -4
  52. package/dist/auth/auth-messages.js +48 -0
  53. package/dist/auth/auth-provider.js +40 -14
  54. package/dist/auth/code-input.js +17 -0
  55. package/dist/auth/email-code-form.js +81 -0
  56. package/dist/auth/email-code-page.js +40 -0
  57. package/dist/auth/first-run-page.js +1 -1
  58. package/dist/auth/forgot-password-page.js +1 -1
  59. package/dist/auth/reset-password-page.js +1 -1
  60. package/dist/auth/setup-required-form.js +43 -0
  61. package/dist/auth/setup-required-page.js +51 -0
  62. package/dist/auth/sign-in-form.js +53 -20
  63. package/dist/auth/sign-in-page.js +14 -13
  64. package/dist/auth/staff-link.js +30 -0
  65. package/dist/auth/staff-sign-in-form.js +93 -0
  66. package/dist/auth/staff-sign-in-page.js +41 -0
  67. package/dist/auth/two-factor-form.js +137 -0
  68. package/dist/auth/two-factor-page.js +39 -0
  69. package/dist/auth/user-to-shell.js +5 -2
  70. package/dist/chat/composer.js +1 -1
  71. package/dist/data/api-provider.d.ts +1 -1
  72. package/dist/data/api-provider.js +1 -1
  73. package/dist/data/use-aggregates.js +1 -1
  74. package/dist/data/use-list.js +1 -1
  75. package/dist/data/use-update-record.js +1 -1
  76. package/dist/data-grid/cells.js +3 -3
  77. package/dist/data-grid/grid-header.js +2 -2
  78. package/dist/data-grid/grid-row.js +1 -1
  79. package/dist/data-grid/header-menu.js +1 -1
  80. package/dist/data-grid/row-menu.js +1 -1
  81. package/dist/diff-view/diff-view.js +1 -1
  82. package/dist/filter-panel/active-filter-chips.js +1 -1
  83. package/dist/filter-panel/facet-group.js +1 -1
  84. package/dist/filter-panel/filter-menu.js +1 -1
  85. package/dist/index.d.ts +3 -1
  86. package/dist/index.js +4 -2
  87. package/dist/inline-child-table/inline-child-table.js +1 -1
  88. package/dist/inline-child-table/line-menu.js +1 -1
  89. package/dist/inline-child-table/line-row.js +2 -2
  90. package/dist/inline-child-table/totals-footer.js +1 -1
  91. package/dist/live/use-filter-config.js +1 -1
  92. package/dist/primitives/checkbox.js +1 -1
  93. package/dist/primitives/sheet.js +1 -1
  94. package/dist/primitives/spinner.js +1 -1
  95. package/dist/record-view/field.js +1 -1
  96. package/dist/record-view/record-sidebar.js +1 -1
  97. package/dist/side-panel/side-panel.js +1 -1
  98. package/dist/step-list/step-list.js +1 -1
  99. package/dist/toasts/toast.js +1 -1
  100. package/package.json +6 -5
@@ -1,4 +1,4 @@
1
- import { ComponentType, ReactNode } from "react";
1
+ import { ComponentType } from "react";
2
2
  import { Client } from "@protobase/client";
3
3
  //#region src/app/pages/project-ui.d.ts
4
4
  /** What a named action's handler gets: the record it runs on (inside a RecordCard or CardRow), the API and the router. */
@@ -14,9 +14,12 @@ export type ActionContext = {
14
14
  export type ActionHandler = (context: ActionContext) => void | Promise<void>;
15
15
  /**
16
16
  * The project's own React code for composed pages: custom components by the name their layout declares
17
- * (`component('UsageChart')`), handlers for named actions that have no built-in behaviour, and the shell's slots.
17
+ * (`component('UsageChart')`) or a widget part names, handlers for named actions that have no built-in behaviour, and
18
+ * the shell's slots. `extends` lists UI configs merged in before it (see `mergeUi`); `name` names it in their errors.
18
19
  */
19
20
  export type ProjectUi = {
21
+ name?: string;
22
+ extends?: ProjectUi[];
20
23
  components?: Record<string, ComponentType<any>>;
21
24
  actions?: Record<string, ActionHandler>;
22
25
  /** Rendered in the shell on every page: `actions` at the end of the top bar, `rightPanel` beside the page. */
@@ -25,6 +28,6 @@ export type ProjectUi = {
25
28
  rightPanel?: ComponentType;
26
29
  };
27
30
  };
28
- /** Typed identity, for `export default defineUi({ components, actions })` in `protobase.ui.tsx`. */
31
+ /** Typed identity, for `export default defineUi({ extends, components, actions })` in `protobase.ui.tsx`. */
29
32
  export declare const defineUi: (ui: ProjectUi) => ProjectUi;
30
33
  //#endregion
@@ -1,13 +1,9 @@
1
1
  import { createContext, useContext } from "react";
2
- import { jsx } from "react/jsx-runtime";
3
2
  //#region src/app/pages/project-ui.tsx
4
- /** Typed identity, for `export default defineUi({ components, actions })` in `protobase.ui.tsx`. */
3
+ /** Typed identity, for `export default defineUi({ extends, components, actions })` in `protobase.ui.tsx`. */
5
4
  const defineUi = (ui) => ui;
5
+ /** The project's UI, merged with the UI configs it extends; `ProjectUiProvider` provides it. */
6
6
  const ProjectUiContext = createContext({});
7
- const ProjectUiProvider = ({ ui, children }) => /* @__PURE__ */ jsx(ProjectUiContext.Provider, {
8
- value: ui ?? {},
9
- children
10
- });
11
7
  const useProjectUi = () => useContext(ProjectUiContext);
12
8
  //#endregion
13
- export { ProjectUiProvider, defineUi, useProjectUi };
9
+ export { ProjectUiContext, defineUi, useProjectUi };
@@ -6,8 +6,8 @@ import { scopeOf } from "./use-layout-data.js";
6
6
  import { FieldValue } from "./field-value.js";
7
7
  import { usePathValue } from "./use-path-value.js";
8
8
  import { useRelationLabels } from "./use-relation-labels.js";
9
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
9
  import { ChevronLeft, ChevronRight } from "lucide-react";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
11
  import { resolveFieldPath } from "@protobase/layout";
12
12
  //#region src/app/pages/record-table.tsx
13
13
  const isPath = (column) => column.includes(".");
@@ -1,10 +1,10 @@
1
1
  import { useClient } from "../../data/api-provider.js";
2
2
  import { useToast } from "../../toasts/toast-provider.js";
3
+ import { useProjectUi } from "./project-ui.js";
3
4
  import { useRouter } from "../router.js";
4
5
  import { useAdminMeta, usePermissions } from "../meta-gate.js";
5
6
  import { can } from "../permissions.js";
6
7
  import { fillHref, isAppPath } from "./action-href.js";
7
- import { useProjectUi } from "./project-ui.js";
8
8
  import { useRefreshResource } from "./use-layout-data.js";
9
9
  import { useState } from "react";
10
10
  //#region src/app/pages/use-run-action.ts
@@ -1,13 +1,13 @@
1
1
  import { Badge } from "../primitives/badge.js";
2
- import { keys } from "../data/query-keys.js";
3
- import { useClient } from "../data/api-provider.js";
4
2
  import { useAuth } from "../auth/auth-provider.js";
3
+ import { formatRelativeTime } from "../format/date.js";
5
4
  import { Button } from "../primitives/button.js";
5
+ import { keys } from "../data/query-keys.js";
6
+ import { useClient } from "../data/api-provider.js";
6
7
  import { useRecord } from "../data/use-record.js";
7
8
  import { recordTitle } from "../live/model-helpers.js";
8
9
  import { humanize } from "../live/naming.js";
9
10
  import { useNavigationGuard, useRouter } from "./router.js";
10
- import { formatRelativeTime } from "../format/date.js";
11
11
  import { displayValue } from "../live/display-value.js";
12
12
  import { ErrorBanner } from "./error-banner.js";
13
13
  import { FieldEditor } from "./field-editor.js";
@@ -29,10 +29,10 @@ import { useRecordEditor } from "./use-record-editor.js";
29
29
  import { useRelatedRecords } from "./use-related-records.js";
30
30
  import { RelatedSection } from "./related-section.js";
31
31
  import { useMemo } from "react";
32
- import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
33
32
  import { Check } from "lucide-react";
34
- import { useQueryClient } from "@tanstack/react-query";
35
33
  import { isApiError } from "@protobase/client";
34
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
35
+ import { useQueryClient } from "@tanstack/react-query";
36
36
  import { enumLabel } from "@protobase/schema";
37
37
  //#region src/app/record-page.tsx
38
38
  const hidden = (model, name) => name === model.tenant || model.primaryKey.includes(name);
@@ -3,8 +3,8 @@ import { Dialog } from "../../primitives/dialog.js";
3
3
  import { nextUpdateText, showsUpdateButton, updateErrorText } from "./runtime-status.js";
4
4
  import { useRuntimeClient, useRuntimeStatus } from "./use-runtime-status.js";
5
5
  import { useState } from "react";
6
- import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
6
  import { ArrowUpCircle } from "lucide-react";
7
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/app/runtime/runtime-update.tsx
9
9
  const Row = ({ label, value }) => /* @__PURE__ */ jsxs("div", {
10
10
  className: "flex justify-between gap-4 py-1 text-[13px]",
@@ -6,8 +6,8 @@ import { displayValue } from "../live/display-value.js";
6
6
  import { toDraft } from "../live/field-values.js";
7
7
  import { ErrorBanner } from "./error-banner.js";
8
8
  import { useState } from "react";
9
- import { jsx, jsxs } from "react/jsx-runtime";
10
9
  import { Eye, EyeOff } from "lucide-react";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
11
  import { useMutation } from "@tanstack/react-query";
12
12
  //#region src/app/sensitive-field.tsx
13
13
  const mask = "••••••••••";
@@ -0,0 +1,67 @@
1
+ import { authMessage } from "../auth/auth-messages.js";
2
+ import { formatDateTime } from "../format/date.js";
3
+ import { Button } from "../primitives/button.js";
4
+ import { useState } from "react";
5
+ import { UserRoundCog } from "lucide-react";
6
+ import { AuthError } from "@protobase/client";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ //#region src/app/staff-banner.tsx
9
+ /** Across the top of the app while staff are signed in as someone: who, as whom, why, until when, and a way to stop. */
10
+ const StaffBanner = ({ signIn, onStop }) => {
11
+ const [stopping, setStopping] = useState(false);
12
+ const [error, setError] = useState();
13
+ const stop = async () => {
14
+ setStopping(true);
15
+ setError(void 0);
16
+ try {
17
+ await onStop();
18
+ } catch (failure) {
19
+ if (!(failure instanceof AuthError)) throw failure;
20
+ setError(authMessage(failure));
21
+ setStopping(false);
22
+ }
23
+ };
24
+ return /* @__PURE__ */ jsxs("div", {
25
+ role: "region",
26
+ "aria-label": "Staff session",
27
+ className: "flex flex-wrap items-center gap-x-3 gap-y-2 border-b border-warning/40 bg-warning-soft px-4 py-2 text-[13px] text-warning-text",
28
+ children: [
29
+ /* @__PURE__ */ jsx(UserRoundCog, {
30
+ "aria-hidden": true,
31
+ className: "size-4 shrink-0"
32
+ }),
33
+ /* @__PURE__ */ jsxs("p", {
34
+ className: "min-w-64 flex-1 break-words",
35
+ children: [
36
+ /* @__PURE__ */ jsx("span", {
37
+ className: "font-semibold",
38
+ children: signIn.staffName ?? signIn.staff
39
+ }),
40
+ " is signed in as ",
41
+ /* @__PURE__ */ jsx("span", {
42
+ className: "font-semibold",
43
+ children: signIn.user
44
+ }),
45
+ " until",
46
+ " ",
47
+ formatDateTime(Date.parse(signIn.expiresAt)),
48
+ ". Reason: ",
49
+ signIn.reason
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
+ size: "sm",
59
+ loading: stopping,
60
+ onClick: () => void stop(),
61
+ children: "Stop staff session"
62
+ })
63
+ ]
64
+ });
65
+ };
66
+ //#endregion
67
+ export { StaffBanner };
@@ -6,8 +6,8 @@ import { readDraft, writeDraft } from "./draft-storage.js";
6
6
  import { isConflict, useUpdateRecord } from "../data/use-update-record.js";
7
7
  import { nameFailingOperation } from "./batch-failure.js";
8
8
  import { useEffect, useState } from "react";
9
- import { useQueryClient } from "@tanstack/react-query";
10
9
  import { isApiError } from "@protobase/client";
10
+ import { useQueryClient } from "@tanstack/react-query";
11
11
  //#region src/app/use-record-editor.ts
12
12
  const without = (source, name) => Object.fromEntries(Object.entries(source).filter(([key]) => key !== name));
13
13
  /**
@@ -1,6 +1,6 @@
1
1
  import { cn } from "../lib/cn.js";
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
2
  import { ChevronRight } from "lucide-react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/app-shell/breadcrumb.tsx
5
5
  const inApp = (event) => event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
6
6
  /** The trail of where you are; the last crumb, the current page, shows on phones too. */
@@ -3,8 +3,8 @@ import { Input } from "../primitives/input.js";
3
3
  import { Kbd } from "../primitives/kbd.js";
4
4
  import { Spinner } from "../primitives/spinner.js";
5
5
  import { useId, useRef, useState } from "react";
6
- import { jsx, jsxs } from "react/jsx-runtime";
7
6
  import { Search, X } from "lucide-react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/app-shell/global-search.tsx
9
9
  /** Moves the highlighted result by `delta`, wrapping around; from nothing (`-1`) it goes to the first or the last. */
10
10
  const moveActive = (active, count, delta) => {
@@ -2,8 +2,8 @@ import { cn } from "../lib/cn.js";
2
2
  import { Avatar } from "../primitives/avatar.js";
3
3
  import { Badge } from "../primitives/badge.js";
4
4
  import { useEffect, useRef, useState } from "react";
5
- import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
5
  import { ChevronsUpDown, LogOut } from "lucide-react";
6
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
7
  //#region src/app-shell/profile-menu.tsx
8
8
  const menuRow = "flex min-h-11 w-full items-center gap-2.5 px-3 py-1.5 text-left text-[13px] text-muted-foreground hover:bg-muted md:min-h-0";
9
9
  /** The person at the bottom of the sidebar; the menu is fixed to the viewport so a narrow sidebar cannot clip it. */
@@ -3,8 +3,8 @@ import { navOpenStorageKey, readNavOpen, writeNavOpen } from "./nav-open-storage
3
3
  import { SidebarItem } from "./sidebar-item.js";
4
4
  import { StatusDot } from "./status-dot.js";
5
5
  import { useId, useState } from "react";
6
- import { jsx, jsxs } from "react/jsx-runtime";
7
6
  import { ChevronRight } from "lucide-react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/app-shell/sidebar-group.tsx
9
9
  const row = {
10
10
  "text-large": "h-8 gap-2.5 px-2.5 text-[13px]",
@@ -2,8 +2,8 @@ import { cn } from "../lib/cn.js";
2
2
  import { Breadcrumb } from "./breadcrumb.js";
3
3
  import { GlobalSearch } from "./global-search.js";
4
4
  import { useEffect, useRef, useState } from "react";
5
- import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
5
  import { Menu, Search, X } from "lucide-react";
6
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
7
  //#region src/app-shell/top-bar.tsx
8
8
  const iconButton = "inline-flex size-11 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted md:hidden";
9
9
  const TopBar = ({ breadcrumb, onNavigate, actions, onMenu, menuOpen, menuControls, search }) => {
@@ -1,3 +1,4 @@
1
+ import { AssistantClient } from "@protobase/client";
1
2
  import { AssistantState } from "@protobase/schema";
2
3
  //#region src/assistant-dock/assistant-dock.d.ts
3
4
  export type AssistantDockProps = {
@@ -9,7 +10,12 @@ export type AssistantDockProps = {
9
10
  onSend?: (text: string) => void;
10
11
  onAction?: (partId: string, actionId: string) => void;
11
12
  onClose?: () => void;
13
+ /** The backend's client, which widgets read and post through with `useBackendData`. */
14
+ client?: AssistantClient;
12
15
  };
13
- /** The assistant beside the app: renders a backend's chat, its cards and status line, and sends messages and clicks back. */
14
- export declare const AssistantDock: ({ state, offline, error, onSend, onAction, onClose }: AssistantDockProps) => import("react").JSX.Element;
16
+ /**
17
+ * The assistant beside the app: renders a backend's chat, its cards, the app's widgets and status line, and sends
18
+ * messages and clicks back.
19
+ */
20
+ export declare const AssistantDock: ({ state, offline, error, onSend, onAction, onClose, client }: AssistantDockProps) => import("react").JSX.Element;
15
21
  //#endregion
@@ -3,13 +3,17 @@ import { ChatThread } from "../chat/chat-thread.js";
3
3
  import { Composer } from "../chat/composer.js";
4
4
  import { SidePanel } from "../side-panel/side-panel.js";
5
5
  import { PartView } from "./part-view.js";
6
+ import { AssistantBackendContext } from "./use-backend-data.js";
6
7
  import { useEffect, useRef } from "react";
7
- import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
8
  import { Sparkles } from "lucide-react";
9
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
10
  //#region src/assistant-dock/assistant-dock.tsx
10
11
  const defaultPlaceholder = "Ask a question...";
11
- /** The assistant beside the app: renders a backend's chat, its cards and status line, and sends messages and clicks back. */
12
- const AssistantDock = ({ state, offline, error, onSend, onAction, onClose }) => {
12
+ /**
13
+ * The assistant beside the app: renders a backend's chat, its cards, the app's widgets and status line, and sends
14
+ * messages and clicks back.
15
+ */
16
+ const AssistantDock = ({ state, offline, error, onSend, onAction, onClose, client }) => {
13
17
  const input = useRef(null);
14
18
  useEffect(() => {
15
19
  if (state.placeholder) input.current?.focus();
@@ -29,16 +33,19 @@ const AssistantDock = ({ state, offline, error, onSend, onAction, onClose }) =>
29
33
  busy: state.replying,
30
34
  placeholder: state.placeholder ?? defaultPlaceholder
31
35
  })] }),
32
- children: /* @__PURE__ */ jsx(ChatThread, {
33
- length: state.messages.reduce((sum, message) => sum + message.parts.length, 0),
34
- empty: "Ask a question about this app or its data.",
35
- children: state.messages.map((message) => /* @__PURE__ */ jsx(ChatMessage, {
36
- from: message.from,
37
- children: message.parts.map((part) => /* @__PURE__ */ jsx(PartView, {
38
- part,
39
- onAction
40
- }, part.id))
41
- }, message.id))
36
+ children: /* @__PURE__ */ jsx(AssistantBackendContext.Provider, {
37
+ value: client,
38
+ children: /* @__PURE__ */ jsx(ChatThread, {
39
+ length: state.messages.reduce((sum, message) => sum + message.parts.length, 0),
40
+ empty: "Ask a question about this app or its data.",
41
+ children: state.messages.map((message) => /* @__PURE__ */ jsx(ChatMessage, {
42
+ from: message.from,
43
+ children: message.parts.map((part) => /* @__PURE__ */ jsx(PartView, {
44
+ part,
45
+ onAction
46
+ }, part.id))
47
+ }, message.id))
48
+ })
42
49
  })
43
50
  });
44
51
  };
@@ -2,4 +2,6 @@ import { AssistantDock, AssistantDockProps } from "./assistant-dock.js";
2
2
  import { PartView } from "./part-view.js";
3
3
  import { useAssistant } from "./use-assistant.js";
4
4
  import { useAssistantClient } from "./use-assistant-client.js";
5
- export { AssistantDock, type AssistantDockProps, PartView, useAssistant, useAssistantClient };
5
+ import { BackendData, BackendRefresh, useBackendData } from "./use-backend-data.js";
6
+ import "./widget-view.js";
7
+ export { AssistantDock, type AssistantDockProps, type BackendData, type BackendRefresh, PartView, useAssistant, useAssistantClient, useBackendData };
@@ -1,6 +1,6 @@
1
1
  import { AssistantPart } from "@protobase/schema";
2
2
  //#region src/assistant-dock/part-view.d.ts
3
- /** One part of a message, drawn with the primitive it names. */
3
+ /** One part of a message, drawn with the primitive it names, or a widget with the app's component it names. */
4
4
  export declare const PartView: ({ part, onAction }: {
5
5
  part: AssistantPart;
6
6
  onAction?: (partId: string, actionId: string) => void;
@@ -3,9 +3,10 @@ import { FieldList } from "../action-card/field-list.js";
3
3
  import { CompactTable } from "../compact-table/compact-table.js";
4
4
  import { DiffView } from "../diff-view/diff-view.js";
5
5
  import { StepList } from "../step-list/step-list.js";
6
+ import { WidgetView } from "./widget-view.js";
6
7
  import { jsx, jsxs } from "react/jsx-runtime";
7
8
  //#region src/assistant-dock/part-view.tsx
8
- /** One part of a message, drawn with the primitive it names. */
9
+ /** One part of a message, drawn with the primitive it names, or a widget with the app's component it names. */
9
10
  const PartView = ({ part, onAction }) => {
10
11
  switch (part.type) {
11
12
  case "text": return /* @__PURE__ */ jsx("p", { children: part.text });
@@ -41,6 +42,7 @@ const PartView = ({ part, onAction }) => {
41
42
  }, `${index}-${diff.path}`))
42
43
  ]
43
44
  });
45
+ case "widget": return /* @__PURE__ */ jsx(WidgetView, { part });
44
46
  }
45
47
  };
46
48
  //#endregion
@@ -0,0 +1,23 @@
1
+ import { AssistantClient } from "@protobase/client";
2
+ //#region src/assistant-dock/use-backend-data.d.ts
3
+ /** How long to wait before reading again: a number of milliseconds while the data can still change, `false` once it cannot. */
4
+ export type BackendRefresh<T> = number | false | ((data: T | undefined) => number | false);
5
+ export type BackendData<T> = {
6
+ /** The last answer read or posted; `undefined` until the first one. */
7
+ data?: T;
8
+ /** Why the last read or post failed, such as an `ApiError` with the backend's detail; cleared by the next success. */
9
+ error?: Error;
10
+ /** Reads again now. */
11
+ reload: () => void;
12
+ /** Posts `body` as JSON to `path` on the backend; its answer replaces `data`. */
13
+ post: (path: string, body?: unknown) => Promise<void>;
14
+ };
15
+ /**
16
+ * JSON from the assistant backend at `path` (see `AssistantClient.fetch`), read as the signed-in user each time the
17
+ * widget mounts, so a widget in an old chat shows the data as it is now; with `refreshMs`, read again while it can
18
+ * still change. Only inside the dock, which provides the backend.
19
+ */
20
+ export declare const useBackendData: <T = unknown>(path: string, { refreshMs }?: {
21
+ refreshMs?: BackendRefresh<T>;
22
+ }) => BackendData<T>;
23
+ //#endregion
@@ -0,0 +1,67 @@
1
+ import { createContext, useContext, useEffect, useRef, useState } from "react";
2
+ //#region src/assistant-dock/use-backend-data.ts
3
+ /** The assistant backend the dock talks to, for the widgets it draws; `AssistantDock` provides it from its `client`. */
4
+ const AssistantBackendContext = createContext(void 0);
5
+ const asError = (reason) => reason instanceof Error ? reason : /* @__PURE__ */ new Error("The assistant backend did not answer");
6
+ const refreshOf = (refresh, data) => typeof refresh === "function" ? refresh(data) : refresh;
7
+ /**
8
+ * JSON from the assistant backend at `path` (see `AssistantClient.fetch`), read as the signed-in user each time the
9
+ * widget mounts, so a widget in an old chat shows the data as it is now; with `refreshMs`, read again while it can
10
+ * still change. Only inside the dock, which provides the backend.
11
+ */
12
+ const useBackendData = (path, { refreshMs = false } = {}) => {
13
+ const client = useContext(AssistantBackendContext);
14
+ const [state, setState] = useState({});
15
+ const [reads, setReads] = useState(0);
16
+ const refresh = useRef(refreshMs);
17
+ refresh.current = refreshMs;
18
+ const latest = useRef(void 0);
19
+ useEffect(() => {
20
+ if (!client) {
21
+ setState({ error: /* @__PURE__ */ new Error("useBackendData works only in a widget the assistant dock draws") });
22
+ return;
23
+ }
24
+ let active = true;
25
+ let timer;
26
+ const read = async () => {
27
+ const answer = await client.fetch(path).then((response) => response.json()).then((data) => ({ data }), (reason) => ({
28
+ data: latest.current,
29
+ error: asError(reason)
30
+ }));
31
+ if (!active) return;
32
+ latest.current = answer.data;
33
+ setState(answer);
34
+ const wait = refreshOf(refresh.current, answer.data);
35
+ if (wait !== false) timer = setTimeout(read, wait);
36
+ };
37
+ read();
38
+ return () => {
39
+ active = false;
40
+ clearTimeout(timer);
41
+ };
42
+ }, [
43
+ client,
44
+ path,
45
+ reads
46
+ ]);
47
+ return {
48
+ ...state,
49
+ reload: () => setReads((count) => count + 1),
50
+ post: async (to, body) => {
51
+ if (!client) throw new Error("useBackendData works only in a widget the assistant dock draws");
52
+ const init = {
53
+ method: "POST",
54
+ headers: { "content-type": "application/json" },
55
+ body: JSON.stringify(body ?? {})
56
+ };
57
+ const answer = await client.fetch(to, init).then((response) => response.json()).then((data) => ({ data }), (reason) => ({
58
+ data: latest.current,
59
+ error: asError(reason)
60
+ }));
61
+ latest.current = answer.data;
62
+ setState(answer);
63
+ }
64
+ };
65
+ };
66
+ //#endregion
67
+ export { AssistantBackendContext, useBackendData };
@@ -0,0 +1 @@
1
+ import "@protobase/schema";
@@ -0,0 +1,44 @@
1
+ import { ActionCard } from "../action-card/action-card.js";
2
+ import { useProjectUi } from "../app/pages/project-ui.js";
3
+ import { Component } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region src/assistant-dock/widget-view.tsx
6
+ /** The part's fallback as a plain card with `note`, or a muted line naming the component when it has none. */
7
+ const Fallback = ({ part, note }) => part.fallback ? /* @__PURE__ */ jsx(ActionCard, {
8
+ title: part.fallback.title,
9
+ note,
10
+ children: part.fallback.body && /* @__PURE__ */ jsx("p", {
11
+ className: "text-[13px]",
12
+ children: part.fallback.body
13
+ })
14
+ }) : /* @__PURE__ */ jsx("p", {
15
+ className: "text-xs text-muted-foreground",
16
+ children: `${part.name}: ${note}`
17
+ });
18
+ var WidgetBoundary = class extends Component {
19
+ state = { failed: false };
20
+ static getDerivedStateFromError() {
21
+ return { failed: true };
22
+ }
23
+ render() {
24
+ return this.state.failed ? /* @__PURE__ */ jsx(Fallback, {
25
+ part: this.props.part,
26
+ note: "It could not be drawn."
27
+ }) : this.props.children;
28
+ }
29
+ };
30
+ /** A widget part, drawn by the app's component of its name with its props; the fallback when the app has none. */
31
+ const WidgetView = ({ part }) => {
32
+ const { components = {} } = useProjectUi();
33
+ const Widget = Object.hasOwn(components, part.name) ? components[part.name] : void 0;
34
+ if (!Widget) return /* @__PURE__ */ jsx(Fallback, {
35
+ part,
36
+ note: "This version of the app cannot show it live."
37
+ });
38
+ return /* @__PURE__ */ jsx(WidgetBoundary, {
39
+ part,
40
+ children: /* @__PURE__ */ jsx(Widget, { ...part.props })
41
+ });
42
+ };
43
+ //#endregion
44
+ export { WidgetView };
@@ -1,28 +1,42 @@
1
1
  import { Spinner } from "../primitives/spinner.js";
2
2
  import { useAuth } from "./auth-provider.js";
3
3
  import { Button } from "../primitives/button.js";
4
+ import { EmailCodePage } from "./email-code-page.js";
4
5
  import { FirstRunPage } from "./first-run-page.js";
5
6
  import { readResetLink, withoutResetLink } from "./reset-link.js";
6
7
  import { ForgotPasswordPage } from "./forgot-password-page.js";
7
8
  import { ResetPasswordPage } from "./reset-password-page.js";
9
+ import { SetupRequiredPage } from "./setup-required-page.js";
8
10
  import { SignInPage } from "./sign-in-page.js";
11
+ import { readStaffLink, withoutStaffLink } from "./staff-link.js";
12
+ import { StaffSignInPage } from "./staff-sign-in-page.js";
13
+ import { TwoFactorPage } from "./two-factor-page.js";
9
14
  import { useEffect, useState } from "react";
10
15
  import { jsx, jsxs } from "react/jsx-runtime";
11
16
  //#region src/auth/auth-gate.tsx
12
17
  const firstPage = () => {
13
18
  const link = readResetLink(window.location.href);
14
- return link ? {
19
+ if (link) return {
15
20
  kind: "reset-password",
16
21
  token: link.token
22
+ };
23
+ const staff = readStaffLink(window.location.href);
24
+ return staff ? {
25
+ kind: "staff-sign-in",
26
+ link: staff
17
27
  } : { kind: "sign-in" };
18
28
  };
19
29
  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. */
30
+ const forgetStaffLink = () => window.history.replaceState(window.history.state, "", withoutStaffLink(window.location.href));
31
+ /**
32
+ * Shows the app when the user is signed in (or the server has no login), and the right page otherwise: the sign-in
33
+ * steps, or the page an emailed reset link or a staff link opened.
34
+ */
21
35
  const AuthGate = ({ workspace, children }) => {
22
36
  const { state, recheck, passwordReset, signOut } = useAuth();
23
37
  const [page, setPage] = useState(firstPage);
24
38
  useEffect(() => {
25
- if (state.kind === "signed-in") setPage((current) => current.kind === "sign-in" && current.notice ? { kind: "sign-in" } : current);
39
+ if (state.kind === "signed-in" || state.kind === "two-factor" || state.kind === "setup-required") setPage((current) => current.kind === "email-code" || current.kind === "sign-in" && current.notice ? { kind: "sign-in" } : current);
26
40
  }, [state.kind]);
27
41
  const leaveResetPage = (next) => {
28
42
  forgetResetLink();
@@ -35,6 +49,10 @@ const AuthGate = ({ workspace, children }) => {
35
49
  notice: "Your password is changed. Sign in with the new one."
36
50
  });
37
51
  };
52
+ const backToSignIn = () => {
53
+ setPage({ kind: "sign-in" });
54
+ signOut();
55
+ };
38
56
  switch (state.kind) {
39
57
  case "loading": return /* @__PURE__ */ jsxs("div", {
40
58
  className: "flex h-full min-h-48 items-center justify-center gap-2 text-[13px] text-muted-foreground",
@@ -58,12 +76,32 @@ const AuthGate = ({ workspace, children }) => {
58
76
  onRequestNewLink: passwordReset ? () => leaveResetPage({ kind: "forgot-password" }) : void 0,
59
77
  onBack: () => leaveResetPage({ kind: "sign-in" })
60
78
  });
79
+ if (page.kind === "staff-sign-in") return /* @__PURE__ */ jsx(StaffSignInPage, {
80
+ link: page.link,
81
+ onBack: () => {
82
+ forgetStaffLink();
83
+ setPage({ kind: "sign-in" });
84
+ }
85
+ });
61
86
  if (state.kind === "signed-in") return children;
87
+ if (state.kind === "two-factor") return /* @__PURE__ */ jsx(TwoFactorPage, {
88
+ methods: state.methods,
89
+ onBack: backToSignIn
90
+ });
91
+ if (state.kind === "setup-required") return /* @__PURE__ */ jsx(SetupRequiredPage, { missing: state.missing }, state.missing.join());
62
92
  if (page.kind === "forgot-password") return /* @__PURE__ */ jsx(ForgotPasswordPage, { onBack: () => setPage({ kind: "sign-in" }) });
93
+ if (page.kind === "email-code") return /* @__PURE__ */ jsx(EmailCodePage, {
94
+ email: page.email,
95
+ onBack: () => setPage({ kind: "sign-in" })
96
+ });
63
97
  return /* @__PURE__ */ jsx(SignInPage, {
64
98
  workspace,
65
99
  notice: page.notice,
66
- onForgotPassword: passwordReset ? () => setPage({ kind: "forgot-password" }) : void 0
100
+ onForgotPassword: passwordReset ? () => setPage({ kind: "forgot-password" }) : void 0,
101
+ onCodeSent: (email) => setPage({
102
+ kind: "email-code",
103
+ email
104
+ })
67
105
  });
68
106
  };
69
107
  //#endregion