@protobase/ui 0.3.4 → 0.4.1

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 (91) 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.d.ts +3 -1
  17. package/dist/app/app.js +50 -18
  18. package/dist/app/assistant-shell.js +1 -1
  19. package/dist/app/create/create-page.js +1 -1
  20. package/dist/app/delete/use-delete-flow.js +1 -1
  21. package/dist/app/error-banner.js +1 -1
  22. package/dist/app/list-page.js +1 -1
  23. package/dist/app/list-search.js +1 -1
  24. package/dist/app/meta-gate.js +1 -1
  25. package/dist/app/pages/blocks/modal-form.js +3 -3
  26. package/dist/app/pages/record-table.js +1 -1
  27. package/dist/app/record-page.js +5 -5
  28. package/dist/app/runtime/runtime-status.js +25 -0
  29. package/dist/app/runtime/runtime-update.js +77 -0
  30. package/dist/app/runtime/use-runtime-status.js +59 -0
  31. package/dist/app/sensitive-field.js +1 -1
  32. package/dist/app/staff-banner.js +67 -0
  33. package/dist/app/use-record-editor.js +1 -1
  34. package/dist/app-shell/breadcrumb.js +1 -1
  35. package/dist/app-shell/global-search.js +1 -1
  36. package/dist/app-shell/profile-menu.js +1 -1
  37. package/dist/app-shell/sidebar-group.js +1 -1
  38. package/dist/app-shell/top-bar.js +1 -1
  39. package/dist/assistant-dock/assistant-dock.js +1 -1
  40. package/dist/auth/auth-gate.js +42 -4
  41. package/dist/auth/auth-messages.js +48 -0
  42. package/dist/auth/auth-provider.js +40 -14
  43. package/dist/auth/code-input.js +17 -0
  44. package/dist/auth/email-code-form.js +81 -0
  45. package/dist/auth/email-code-page.js +40 -0
  46. package/dist/auth/first-run-page.js +1 -1
  47. package/dist/auth/forgot-password-page.js +1 -1
  48. package/dist/auth/reset-password-page.js +1 -1
  49. package/dist/auth/setup-required-form.js +43 -0
  50. package/dist/auth/setup-required-page.js +51 -0
  51. package/dist/auth/sign-in-form.js +53 -20
  52. package/dist/auth/sign-in-page.js +14 -13
  53. package/dist/auth/staff-link.js +30 -0
  54. package/dist/auth/staff-sign-in-form.js +93 -0
  55. package/dist/auth/staff-sign-in-page.js +41 -0
  56. package/dist/auth/two-factor-form.js +137 -0
  57. package/dist/auth/two-factor-page.js +39 -0
  58. package/dist/auth/user-to-shell.js +5 -2
  59. package/dist/chat/composer.js +1 -1
  60. package/dist/data/api-provider.d.ts +1 -1
  61. package/dist/data/api-provider.js +1 -1
  62. package/dist/data/meta-data.d.ts +4 -0
  63. package/dist/data/use-aggregates.js +1 -1
  64. package/dist/data/use-list.js +1 -1
  65. package/dist/data/use-meta.js +2 -1
  66. package/dist/data/use-update-record.js +1 -1
  67. package/dist/data-grid/cells.js +3 -3
  68. package/dist/data-grid/grid-header.js +2 -2
  69. package/dist/data-grid/grid-row.js +1 -1
  70. package/dist/data-grid/header-menu.js +1 -1
  71. package/dist/data-grid/row-menu.js +1 -1
  72. package/dist/diff-view/diff-view.js +1 -1
  73. package/dist/filter-panel/active-filter-chips.js +1 -1
  74. package/dist/filter-panel/facet-group.js +1 -1
  75. package/dist/filter-panel/filter-menu.js +1 -1
  76. package/dist/inline-child-table/inline-child-table.js +1 -1
  77. package/dist/inline-child-table/line-menu.js +1 -1
  78. package/dist/inline-child-table/line-row.js +2 -2
  79. package/dist/inline-child-table/totals-footer.js +1 -1
  80. package/dist/lib/use-visual-viewport.js +27 -0
  81. package/dist/live/use-filter-config.js +1 -1
  82. package/dist/primitives/checkbox.js +1 -1
  83. package/dist/primitives/sheet.js +1 -1
  84. package/dist/primitives/spinner.js +1 -1
  85. package/dist/record-view/field.js +1 -1
  86. package/dist/record-view/record-sidebar.js +1 -1
  87. package/dist/side-panel/side-panel.d.ts +5 -1
  88. package/dist/side-panel/side-panel.js +49 -35
  89. package/dist/step-list/step-list.js +1 -1
  90. package/dist/toasts/toast.js +1 -1
  91. package/package.json +10 -8
@@ -0,0 +1,39 @@
1
+ import { authMessage } from "./auth-messages.js";
2
+ import { useAuth } from "./auth-provider.js";
3
+ import { TwoFactorForm } from "./two-factor-form.js";
4
+ import { useState } from "react";
5
+ import { AuthError } from "@protobase/client";
6
+ import { jsx } from "react/jsx-runtime";
7
+ //#region src/auth/two-factor-page.tsx
8
+ /** The second step after a password or an emailed code, with the methods the server named. */
9
+ const TwoFactorPage = ({ methods, onBack }) => {
10
+ const { verifyTwoFactor, session } = useAuth();
11
+ const [busy, setBusy] = useState(false);
12
+ const [error, setError] = useState();
13
+ const [codeSent, setCodeSent] = useState(false);
14
+ const attempt = async (step) => {
15
+ setBusy(true);
16
+ setError(void 0);
17
+ try {
18
+ await step();
19
+ } catch (failure) {
20
+ if (!(failure instanceof AuthError)) throw failure;
21
+ setError(authMessage(failure));
22
+ }
23
+ setBusy(false);
24
+ };
25
+ return /* @__PURE__ */ jsx(TwoFactorForm, {
26
+ methods,
27
+ busy,
28
+ error,
29
+ codeSent,
30
+ onBack,
31
+ onVerify: (input) => void attempt(() => verifyTwoFactor(input)),
32
+ onSendCode: () => void attempt(async () => {
33
+ await session.sendTwoFactorCode();
34
+ setCodeSent(true);
35
+ })
36
+ });
37
+ };
38
+ //#endregion
39
+ export { TwoFactorPage };
@@ -1,9 +1,12 @@
1
1
  //#region src/auth/user-to-shell.ts
2
2
  const hueOf = (text) => [...text].reduce((sum, char) => (sum * 31 + char.charCodeAt(0)) % 360, 7);
3
3
  const initialsOf = (name) => name.split(/[\s@.]+/).filter(Boolean).slice(0, 2).map((part) => part[0].toUpperCase()).join("");
4
+ const rolesOf = (user) => (user.role ?? "user").split(",").map((role) => role.trim());
5
+ /** The user holds the `admin` role, among others perhaps. */
6
+ const isAdmin = (user) => rolesOf(user).includes("admin");
4
7
  /** What the sidebar's profile menu shows for a signed-in user. */
5
8
  const userToShell = (user) => {
6
- const roles = (user.role ?? "user").split(",").map((role) => role.trim());
9
+ const roles = rolesOf(user);
7
10
  const admin = roles.includes("admin");
8
11
  return {
9
12
  name: user.name || user.email,
@@ -15,4 +18,4 @@ const userToShell = (user) => {
15
18
  };
16
19
  };
17
20
  //#endregion
18
- export { userToShell };
21
+ export { isAdmin, userToShell };
@@ -1,8 +1,8 @@
1
1
  import { Input } from "../primitives/input.js";
2
2
  import { Button } from "../primitives/button.js";
3
3
  import { useState } from "react";
4
- import { jsx, jsxs } from "react/jsx-runtime";
5
4
  import { SendHorizontal } from "lucide-react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
6
  //#region src/chat/composer.tsx
7
7
  /** The text box under a chat. Enter or the button sends the trimmed text and clears the box. */
8
8
  const Composer = ({ onSend, busy, placeholder, inputRef }) => {
@@ -1,6 +1,6 @@
1
1
  import { ReactNode } from "react";
2
- import { QueryClient } from "@tanstack/react-query";
3
2
  import { Client, ClientOptions } from "@protobase/client";
3
+ import { QueryClient } from "@tanstack/react-query";
4
4
  //#region src/data/api-provider.d.ts
5
5
  export declare const useClient: () => {
6
6
  list: <R extends import("@protobase/client").ResourceRef>(resource: R, params?: import("@protobase/client").ListParams) => Promise<import("@protobase/client").ListPage<import("@protobase/client").RecordOf<R>>>;
@@ -1,8 +1,8 @@
1
1
  import { keys } from "./query-keys.js";
2
2
  import { createContext, useContext, useMemo, useRef, useState } from "react";
3
+ import { createClient } from "@protobase/client";
3
4
  import { jsx } from "react/jsx-runtime";
4
5
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
- import { createClient } from "@protobase/client";
6
6
  //#region src/data/api-provider.tsx
7
7
  const ApiContext = createContext(null);
8
8
  const useClient = () => {
@@ -14,5 +14,9 @@ export type MetaData = {
14
14
  assistant?: {
15
15
  url: string;
16
16
  };
17
+ /** The runtime updates endpoint, when the app has one and the user has the admin role. */
18
+ runtime?: {
19
+ url: string;
20
+ };
17
21
  };
18
22
  //#endregion
@@ -1,7 +1,7 @@
1
1
  import { keys } from "./query-keys.js";
2
2
  import { useClient } from "./api-provider.js";
3
- import { useQuery } from "@tanstack/react-query";
4
3
  import { filterText } from "@protobase/client";
4
+ import { useQuery } from "@tanstack/react-query";
5
5
  //#region src/data/use-aggregates.ts
6
6
  const useFacets = (resource, field, filter, enabled = true) => {
7
7
  const client = useClient();
@@ -1,7 +1,7 @@
1
1
  import { keys } from "./query-keys.js";
2
2
  import { useClient } from "./api-provider.js";
3
- import { useInfiniteQuery } from "@tanstack/react-query";
4
3
  import { filterText } from "@protobase/client";
4
+ import { useInfiniteQuery } from "@tanstack/react-query";
5
5
  //#region src/data/use-list.ts
6
6
  /** Infinite pages of a resource, following `next_page_token`. */
7
7
  const useList = (resource, { filter, orderBy, pageSize = 50, fields, startToken = "", enabled = true } = {}) => {
@@ -27,7 +27,8 @@ const useMeta = () => {
27
27
  pages: Object.fromEntries((result.meta.pages ?? []).map((model) => [model.name, model])),
28
28
  permissions: result.meta.permissions ?? {},
29
29
  ...result.meta.userMenu && { userMenu: result.meta.userMenu },
30
- ...result.meta.assistant && { assistant: result.meta.assistant }
30
+ ...result.meta.assistant && { assistant: result.meta.assistant },
31
+ ...result.meta.runtime && { runtime: result.meta.runtime }
31
32
  };
32
33
  }
33
34
  });
@@ -1,7 +1,7 @@
1
1
  import { keys } from "./query-keys.js";
2
2
  import { useClient } from "./api-provider.js";
3
- import { useMutation, useQueryClient } from "@tanstack/react-query";
4
3
  import { PreconditionFailedError } from "@protobase/client";
4
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
5
5
  import { encodeKey } from "@protobase/schema";
6
6
  //#region src/data/use-update-record.ts
7
7
  /**
@@ -1,12 +1,12 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { Avatar } from "../primitives/avatar.js";
3
3
  import { Badge } from "../primitives/badge.js";
4
- import { formatDate, formatDateTime } from "../format/date.js";
5
- import { userById } from "../mocks/users.js";
6
4
  import { formatMoney } from "../format/money.js";
7
5
  import { formatInt, formatPercent, formatSigned } from "../format/number.js";
8
- import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { formatDate, formatDateTime } from "../format/date.js";
7
+ import { userById } from "../mocks/users.js";
9
8
  import { Check } from "lucide-react";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
10
10
  import { enumLabel } from "@protobase/schema";
11
11
  //#region src/data-grid/cells.tsx
12
12
  const muted = (text) => /* @__PURE__ */ jsx("span", {
@@ -1,10 +1,10 @@
1
1
  import { cn } from "../lib/cn.js";
2
- import { gridTemplate } from "./grid-template.js";
3
2
  import { Checkbox } from "../primitives/checkbox.js";
3
+ import { gridTemplate } from "./grid-template.js";
4
4
  import { isRightAligned } from "./column-spec.js";
5
5
  import { HeaderMenu } from "./header-menu.js";
6
- import { jsx, jsxs } from "react/jsx-runtime";
7
6
  import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/data-grid/grid-header.tsx
9
9
  const SortIcon = ({ sorted }) => {
10
10
  if (sorted === "asc") return /* @__PURE__ */ jsx(ArrowUp, { className: "size-3 text-foreground" });
@@ -1,7 +1,7 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { formatInt } from "../format/number.js";
3
- import { gridTemplate } from "./grid-template.js";
4
3
  import { Checkbox } from "../primitives/checkbox.js";
4
+ import { gridTemplate } from "./grid-template.js";
5
5
  import { isRightAligned } from "./column-spec.js";
6
6
  import { Skeleton } from "../primitives/skeleton.js";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
2
  import { MoreHorizontal } from "lucide-react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/data-grid/header-menu.tsx
5
5
  /** The "..." in a column header: for now, pinning the column to the left edge. */
6
6
  const HeaderMenu = ({ header, pinned, onTogglePin }) => {
@@ -1,7 +1,7 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
- import { jsx, jsxs } from "react/jsx-runtime";
4
3
  import { MoreHorizontal } from "lucide-react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
5
  import { createPortal } from "react-dom";
6
6
  //#region src/data-grid/row-menu.tsx
7
7
  /** The actions that apply to a row; a row with none gets no menu at all. */
@@ -1,7 +1,7 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { diffStats, parseDiff } from "./diff.js";
3
- import { jsx, jsxs } from "react/jsx-runtime";
4
3
  import { FileCode2 } from "lucide-react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/diff-view/diff-view.tsx
6
6
  const row = {
7
7
  add: "bg-success-soft text-success-text",
@@ -1,5 +1,5 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
2
1
  import { Lock, X } from "lucide-react";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  //#region src/filter-panel/active-filter-chips.tsx
4
4
  const chipClass = "inline-flex h-6 items-center gap-1 rounded-full border border-primary-border bg-primary-soft pl-2.5 pr-1.5 text-xs font-medium text-primary-text";
5
5
  const ActiveFilterChips = ({ chips, lockedLabels = [], onRemove, onClear, scroll }) => {
@@ -3,8 +3,8 @@ import { formatInt } from "../format/number.js";
3
3
  import { Checkbox } from "../primitives/checkbox.js";
4
4
  import { matchesSearch } from "./filter-state.js";
5
5
  import { useState } from "react";
6
- import { jsx, jsxs } from "react/jsx-runtime";
7
6
  import { Search } from "lucide-react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/filter-panel/facet-group.tsx
9
9
  const FacetGroup = ({ config, selected, onToggle }) => {
10
10
  const [query, setQuery] = useState("");
@@ -1,7 +1,7 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { useEffect, useRef, useState } from "react";
3
- import { jsx, jsxs } from "react/jsx-runtime";
4
3
  import { ChevronDown } from "lucide-react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/filter-panel/filter-menu.tsx
6
6
  const FilterMenu = ({ label, activeCount, width = 240, defaultOpen = false, children }) => {
7
7
  const [open, setOpen] = useState(defaultOpen);
@@ -4,8 +4,8 @@ import { lineGrid } from "./columns.js";
4
4
  import { LineRow } from "./line-row.js";
5
5
  import { reorder } from "./reorder.js";
6
6
  import { TotalsFooter } from "./totals-footer.js";
7
- import { jsx, jsxs } from "react/jsx-runtime";
8
7
  import { Plus } from "lucide-react";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
9
  import { DndContext, KeyboardSensor, MouseSensor, TouchSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
10
10
  import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from "@dnd-kit/sortable";
11
11
  //#region src/inline-child-table/inline-child-table.tsx
@@ -1,7 +1,7 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { useEffect, useRef, useState } from "react";
3
- import { jsx, jsxs } from "react/jsx-runtime";
4
3
  import { MoreHorizontal } from "lucide-react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/inline-child-table/line-menu.tsx
6
6
  /** The "..." at the end of a line: the keyboard-and-screen-reader way to reorder, besides the drag handle. */
7
7
  const LineMenu = ({ items, label }) => {
@@ -1,11 +1,11 @@
1
1
  import { cn } from "../lib/cn.js";
2
+ import { formatMoney } from "../format/money.js";
2
3
  import { lineTotalCents } from "../mocks/invoices.js";
3
4
  import { lineGrid } from "./columns.js";
4
- import { formatMoney } from "../format/money.js";
5
5
  import { LineMenu } from "./line-menu.js";
6
6
  import { NumberCell } from "./number-cell.js";
7
- import { jsx, jsxs } from "react/jsx-runtime";
8
7
  import { GripVertical } from "lucide-react";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
9
  import { useSortable } from "@dnd-kit/sortable";
10
10
  //#region src/inline-child-table/line-row.tsx
11
11
  const parseEuros = (text) => Math.round(Number(text.replace(",", ".").replace(/[^0-9.]/g, "")) * 100);
@@ -1,5 +1,5 @@
1
- import { invoiceTotals } from "../mocks/invoices.js";
2
1
  import { formatMoney } from "../format/money.js";
2
+ import { invoiceTotals } from "../mocks/invoices.js";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/inline-child-table/totals-footer.tsx
5
5
  const TotalsFooter = ({ lines, vatRate }) => {
@@ -0,0 +1,27 @@
1
+ import { useEffect, useState } from "react";
2
+ //#region src/lib/use-visual-viewport.ts
3
+ /**
4
+ * While `active`: the part of the page that is on screen, which shrinks when a phone's on-screen keyboard opens;
5
+ * undefined otherwise, or where the browser has no `visualViewport`.
6
+ */
7
+ const useVisualViewport = (active) => {
8
+ const [box, setBox] = useState();
9
+ useEffect(() => {
10
+ const viewport = window.visualViewport;
11
+ if (!active || !viewport) return setBox(void 0);
12
+ const update = () => setBox({
13
+ top: viewport.offsetTop,
14
+ height: viewport.height
15
+ });
16
+ update();
17
+ viewport.addEventListener("resize", update);
18
+ viewport.addEventListener("scroll", update);
19
+ return () => {
20
+ viewport.removeEventListener("resize", update);
21
+ viewport.removeEventListener("scroll", update);
22
+ };
23
+ }, [active]);
24
+ return active ? box : void 0;
25
+ };
26
+ //#endregion
27
+ export { useVisualViewport };
@@ -1,7 +1,7 @@
1
+ import { formatMoney } from "../format/money.js";
1
2
  import { keys } from "../data/query-keys.js";
2
3
  import { useClient } from "../data/api-provider.js";
3
4
  import { humanize } from "./naming.js";
4
- import { formatMoney } from "../format/money.js";
5
5
  import { stateToText } from "./filter-string.js";
6
6
  import { useHistogram } from "../data/use-aggregates.js";
7
7
  import { fetchLabels } from "./relation-labels.js";
@@ -1,6 +1,6 @@
1
1
  import { cn } from "../lib/cn.js";
2
- import { jsx } from "react/jsx-runtime";
3
2
  import { Check, Minus } from "lucide-react";
3
+ import { jsx } from "react/jsx-runtime";
4
4
  //#region src/primitives/checkbox.tsx
5
5
  const Checkbox = ({ checked, indeterminate, onChange, label, className }) => /* @__PURE__ */ jsx("button", {
6
6
  type: "button",
@@ -1,7 +1,7 @@
1
1
  import { useFocusTrap } from "../lib/use-focus-trap.js";
2
2
  import { useRef } from "react";
3
- import { jsx, jsxs } from "react/jsx-runtime";
4
3
  import { X } from "lucide-react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/primitives/sheet.tsx
6
6
  /** A panel that rises from the bottom of the screen, for the filter panel on phones. */
7
7
  const Sheet = ({ title, open, onClose, children }) => {
@@ -1,6 +1,6 @@
1
1
  import { cn } from "../lib/cn.js";
2
- import { jsx } from "react/jsx-runtime";
3
2
  import { Loader2 } from "lucide-react";
3
+ import { jsx } from "react/jsx-runtime";
4
4
  //#region src/primitives/spinner.tsx
5
5
  const Spinner = ({ className }) => /* @__PURE__ */ jsx(Loader2, {
6
6
  "aria-label": "Loading",
@@ -1,7 +1,7 @@
1
1
  import { Avatar } from "../primitives/avatar.js";
2
2
  import { Button } from "../primitives/button.js";
3
- import { jsx, jsxs } from "react/jsx-runtime";
4
3
  import { TriangleAlert } from "lucide-react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/record-view/field.tsx
6
6
  const Field = ({ label, required, help, example, error, conflict, children }) => /* @__PURE__ */ jsxs("div", { children: [
7
7
  /* @__PURE__ */ jsxs("div", {
@@ -1,5 +1,5 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
2
1
  import { ChevronRight } from "lucide-react";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  //#region src/record-view/record-sidebar.tsx
4
4
  const MetadataList = ({ rows }) => /* @__PURE__ */ jsx("dl", {
5
5
  className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-[13px]",
@@ -12,6 +12,10 @@ export type SidePanelProps = {
12
12
  /** The content; it scrolls on its own, so give it `min-h-0 flex-1`. */
13
13
  children: ReactNode;
14
14
  };
15
- /** A full-height panel beside the page, for the shell's `rightPanel`: a header with a close button, content and a footer. */
15
+ /**
16
+ * A full-height panel beside the page, for the shell's `rightPanel`: a header with a close button, content and a footer.
17
+ * Below `md` there is no room beside the page, so it covers the screen instead, shrinking with the on-screen keyboard
18
+ * so the footer stays in view.
19
+ */
16
20
  export declare const SidePanel: ({ title, icon, status, onClose, footer, children }: SidePanelProps) => import("react").JSX.Element;
17
21
  //#endregion
@@ -1,40 +1,54 @@
1
+ import { useIsDesktop } from "../lib/use-media-query.js";
1
2
  import { Button } from "../primitives/button.js";
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useVisualViewport } from "../lib/use-visual-viewport.js";
3
4
  import { X } from "lucide-react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
4
6
  //#region src/side-panel/side-panel.tsx
5
- /** A full-height panel beside the page, for the shell's `rightPanel`: a header with a close button, content and a footer. */
6
- const SidePanel = ({ title, icon, status, onClose, footer, children }) => /* @__PURE__ */ jsxs("aside", {
7
- "aria-label": title,
8
- className: "flex h-full w-[380px] max-w-full shrink-0 flex-col border-l border-border bg-surface",
9
- children: [
10
- /* @__PURE__ */ jsxs("header", {
11
- className: "flex h-12 shrink-0 items-center gap-2 border-b border-border bg-background px-4",
12
- children: [
13
- icon,
14
- /* @__PURE__ */ jsx("h2", {
15
- className: "flex-1 text-[13px] font-semibold",
16
- children: title
17
- }),
18
- status && /* @__PURE__ */ jsx("span", {
19
- className: "truncate text-xs font-medium text-muted-foreground",
20
- children: status
21
- }),
22
- onClose && /* @__PURE__ */ jsx(Button, {
23
- variant: "ghost",
24
- size: "sm",
25
- className: "w-7 px-0",
26
- "aria-label": "Close",
27
- onClick: onClose,
28
- children: /* @__PURE__ */ jsx(X, { className: "size-4" })
29
- })
30
- ]
31
- }),
32
- children,
33
- footer && /* @__PURE__ */ jsx("footer", {
34
- className: "shrink-0 space-y-2 border-t border-border bg-background p-3",
35
- children: footer
36
- })
37
- ]
38
- });
7
+ /**
8
+ * A full-height panel beside the page, for the shell's `rightPanel`: a header with a close button, content and a footer.
9
+ * Below `md` there is no room beside the page, so it covers the screen instead, shrinking with the on-screen keyboard
10
+ * so the footer stays in view.
11
+ */
12
+ const SidePanel = ({ title, icon, status, onClose, footer, children }) => {
13
+ const screen = useVisualViewport(!useIsDesktop());
14
+ return /* @__PURE__ */ jsxs("aside", {
15
+ "aria-label": title,
16
+ className: "flex h-full w-[380px] max-w-full shrink-0 flex-col border-l border-border bg-surface max-md:fixed max-md:inset-0 max-md:z-40 max-md:w-full max-md:border-l-0",
17
+ style: screen && {
18
+ top: screen.top,
19
+ bottom: "auto",
20
+ height: screen.height
21
+ },
22
+ children: [
23
+ /* @__PURE__ */ jsxs("header", {
24
+ className: "flex h-12 shrink-0 items-center gap-2 border-b border-border bg-background px-4",
25
+ children: [
26
+ icon,
27
+ /* @__PURE__ */ jsx("h2", {
28
+ className: "flex-1 text-[13px] font-semibold",
29
+ children: title
30
+ }),
31
+ status && /* @__PURE__ */ jsx("span", {
32
+ className: "truncate text-xs font-medium text-muted-foreground",
33
+ children: status
34
+ }),
35
+ onClose && /* @__PURE__ */ jsx(Button, {
36
+ variant: "ghost",
37
+ size: "sm",
38
+ className: "w-7 px-0",
39
+ "aria-label": "Close",
40
+ onClick: onClose,
41
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
42
+ })
43
+ ]
44
+ }),
45
+ children,
46
+ footer && /* @__PURE__ */ jsx("footer", {
47
+ className: "shrink-0 space-y-2 border-t border-border bg-background p-3",
48
+ children: footer
49
+ })
50
+ ]
51
+ });
52
+ };
39
53
  //#endregion
40
54
  export { SidePanel };
@@ -1,7 +1,7 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { Spinner } from "../primitives/spinner.js";
3
- import { jsx, jsxs } from "react/jsx-runtime";
4
3
  import { Check, Circle, X } from "lucide-react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/step-list/step-list.tsx
6
6
  const icons = {
7
7
  pending: /* @__PURE__ */ jsx(Circle, { className: "size-3 text-faint-foreground" }),
@@ -1,7 +1,7 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { Spinner } from "../primitives/spinner.js";
3
- import { jsx, jsxs } from "react/jsx-runtime";
4
3
  import { CheckCircle2, X, XCircle } from "lucide-react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/toasts/toast.tsx
6
6
  const icons = {
7
7
  loading: /* @__PURE__ */ jsx(Spinner, { className: "text-primary" }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/ui",
3
- "version": "0.3.4",
3
+ "version": "0.4.1",
4
4
  "description": "The Protobase admin app and the React hooks and components for custom UI.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -24,12 +24,19 @@
24
24
  "files": [
25
25
  "dist"
26
26
  ],
27
+ "scripts": {
28
+ "build": "tsdown",
29
+ "prepack": "tsdown"
30
+ },
27
31
  "publishConfig": {
28
32
  "access": "public"
29
33
  },
30
34
  "dependencies": {
31
35
  "@dnd-kit/core": "^6.3.1",
32
36
  "@dnd-kit/sortable": "^10.0.0",
37
+ "@protobase/client": "0.4.1",
38
+ "@protobase/layout": "0.4.1",
39
+ "@protobase/schema": "0.4.1",
33
40
  "@tanstack/react-query": "^5.104.1",
34
41
  "@tanstack/react-table": "^9.2.6",
35
42
  "@tanstack/react-virtual": "^3.14.13",
@@ -38,11 +45,6 @@
38
45
  "react-dom": "^19.3.0",
39
46
  "recharts": "^3.10.1",
40
47
  "tailwindcss": "^4.3.3",
41
- "@protobase/client": "0.3.4",
42
- "@protobase/layout": "0.3.4",
43
- "@protobase/schema": "0.3.4"
44
- },
45
- "scripts": {
46
- "build": "tsdown"
48
+ "uqr": "^0.1.3"
47
49
  }
48
- }
50
+ }