@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.
Files changed (216) hide show
  1. package/LICENSE +202 -0
  2. package/dist/activity-chart/activity-chart.js +125 -0
  3. package/dist/activity-chart/activity-math.js +11 -0
  4. package/dist/activity-chart/activity-tooltip.js +24 -0
  5. package/dist/app/action-icon.js +10 -0
  6. package/dist/app/app.d.ts +24 -0
  7. package/dist/app/app.js +118 -0
  8. package/dist/app/batch-failure.js +21 -0
  9. package/dist/app/breadcrumb-for.js +27 -0
  10. package/dist/app/create/create-page.js +118 -0
  11. package/dist/app/create/create-record-form.js +68 -0
  12. package/dist/app/create/create-values.js +52 -0
  13. package/dist/app/delete/dialogs.js +109 -0
  14. package/dist/app/delete/record-diff.js +17 -0
  15. package/dist/app/delete/use-delete-flow.js +193 -0
  16. package/dist/app/delete/use-restore.js +9 -0
  17. package/dist/app/draft-storage.js +18 -0
  18. package/dist/app/error-banner.js +28 -0
  19. package/dist/app/field-editor.js +88 -0
  20. package/dist/app/icons.js +27 -0
  21. package/dist/app/index.d.ts +2 -0
  22. package/dist/app/index.html +12 -0
  23. package/dist/app/list-chart.js +36 -0
  24. package/dist/app/list-order.js +15 -0
  25. package/dist/app/list-page.js +213 -0
  26. package/dist/app/list-search.js +30 -0
  27. package/dist/app/main.d.ts +1 -0
  28. package/dist/app/main.js +14 -0
  29. package/dist/app/menu-icons.js +21 -0
  30. package/dist/app/meta-gate.js +46 -0
  31. package/dist/app/nav-from-meta.js +65 -0
  32. package/dist/app/nav-recent.js +33 -0
  33. package/dist/app/pages/action-href.js +7 -0
  34. package/dist/app/pages/blocks/action.js +38 -0
  35. package/dist/app/pages/blocks/block-props.js +5 -0
  36. package/dist/app/pages/blocks/card-frame.js +34 -0
  37. package/dist/app/pages/blocks/card-row.js +55 -0
  38. package/dist/app/pages/blocks/card.js +19 -0
  39. package/dist/app/pages/blocks/field.js +45 -0
  40. package/dist/app/pages/blocks/grid.js +34 -0
  41. package/dist/app/pages/blocks/link.js +27 -0
  42. package/dist/app/pages/blocks/modal-form.js +149 -0
  43. package/dist/app/pages/blocks/page-block.js +20 -0
  44. package/dist/app/pages/blocks/progress.js +54 -0
  45. package/dist/app/pages/blocks/record-card.js +56 -0
  46. package/dist/app/pages/blocks/show.js +43 -0
  47. package/dist/app/pages/blocks/stat.js +78 -0
  48. package/dist/app/pages/blocks/table.js +65 -0
  49. package/dist/app/pages/composed-page.js +19 -0
  50. package/dist/app/pages/confirm-action.js +20 -0
  51. package/dist/app/pages/field-value.js +58 -0
  52. package/dist/app/pages/project-ui.d.ts +25 -0
  53. package/dist/app/pages/project-ui.js +13 -0
  54. package/dist/app/pages/record-actions.js +40 -0
  55. package/dist/app/pages/record-scope.d.ts +16 -0
  56. package/dist/app/pages/record-scope.js +12 -0
  57. package/dist/app/pages/record-table.js +110 -0
  58. package/dist/app/pages/render-node.js +84 -0
  59. package/dist/app/pages/use-layout-data.js +73 -0
  60. package/dist/app/pages/use-path-value.js +65 -0
  61. package/dist/app/pages/use-relation-labels.js +30 -0
  62. package/dist/app/pages/use-run-action.js +88 -0
  63. package/dist/app/permissions.js +15 -0
  64. package/dist/app/record-lines.js +39 -0
  65. package/dist/app/record-page.js +318 -0
  66. package/dist/app/related-section.js +126 -0
  67. package/dist/app/relation-picker.js +69 -0
  68. package/dist/app/router.js +155 -0
  69. package/dist/app/search/search-model.js +70 -0
  70. package/dist/app/search/use-global-search.js +68 -0
  71. package/dist/app/sensitive-field.js +65 -0
  72. package/dist/app/use-lines-draft.js +45 -0
  73. package/dist/app/use-lines.js +19 -0
  74. package/dist/app/use-live-save.js +54 -0
  75. package/dist/app/use-nav-recent.js +41 -0
  76. package/dist/app/use-record-editor.js +154 -0
  77. package/dist/app/use-related-records.js +31 -0
  78. package/dist/app/user-menu-from-meta.js +36 -0
  79. package/dist/app-shell/app-shell.d.ts +4 -0
  80. package/dist/app-shell/app-shell.js +57 -0
  81. package/dist/app-shell/breadcrumb.js +37 -0
  82. package/dist/app-shell/drawer.js +30 -0
  83. package/dist/app-shell/global-search.d.ts +1 -0
  84. package/dist/app-shell/global-search.js +145 -0
  85. package/dist/app-shell/index.d.ts +9 -0
  86. package/dist/app-shell/logo.js +46 -0
  87. package/dist/app-shell/nav-open-storage.js +10 -0
  88. package/dist/app-shell/nav.d.ts +2 -0
  89. package/dist/app-shell/nav.js +85 -0
  90. package/dist/app-shell/page-header.d.ts +1 -0
  91. package/dist/app-shell/page-header.js +17 -0
  92. package/dist/app-shell/profile-menu.d.ts +2 -0
  93. package/dist/app-shell/profile-menu.js +115 -0
  94. package/dist/app-shell/sidebar-group.js +94 -0
  95. package/dist/app-shell/sidebar-item.js +46 -0
  96. package/dist/app-shell/sidebar-mode.d.ts +4 -0
  97. package/dist/app-shell/sidebar-mode.js +13 -0
  98. package/dist/app-shell/sidebar.d.ts +2 -0
  99. package/dist/app-shell/sidebar.js +64 -0
  100. package/dist/app-shell/status-dot.d.ts +1 -0
  101. package/dist/app-shell/status-dot.js +20 -0
  102. package/dist/app-shell/top-bar.js +82 -0
  103. package/dist/auth/auth-gate.js +70 -0
  104. package/dist/auth/auth-layout.js +29 -0
  105. package/dist/auth/auth-provider.js +64 -0
  106. package/dist/auth/first-run-page.js +51 -0
  107. package/dist/auth/forgot-password-form.js +75 -0
  108. package/dist/auth/forgot-password-page.js +35 -0
  109. package/dist/auth/reset-link.js +29 -0
  110. package/dist/auth/reset-password-form.js +88 -0
  111. package/dist/auth/reset-password-page.js +43 -0
  112. package/dist/auth/sign-in-form.js +74 -0
  113. package/dist/auth/sign-in-page.js +38 -0
  114. package/dist/auth/user-to-shell.js +18 -0
  115. package/dist/data/api-provider.d.ts +54 -0
  116. package/dist/data/api-provider.js +53 -0
  117. package/dist/data/meta-data.d.ts +14 -0
  118. package/dist/data/meta-data.js +9 -0
  119. package/dist/data/query-keys.js +95 -0
  120. package/dist/data/use-aggregates.d.ts +8 -0
  121. package/dist/data/use-aggregates.js +43 -0
  122. package/dist/data/use-list.d.ts +231 -0
  123. package/dist/data/use-list.js +31 -0
  124. package/dist/data/use-meta.d.ts +8 -0
  125. package/dist/data/use-meta.js +35 -0
  126. package/dist/data/use-record.d.ts +6 -0
  127. package/dist/data/use-record.js +16 -0
  128. package/dist/data/use-update-record.d.ts +15 -0
  129. package/dist/data/use-update-record.js +26 -0
  130. package/dist/data-grid/cells.js +85 -0
  131. package/dist/data-grid/column-spec.js +4 -0
  132. package/dist/data-grid/columns.js +16 -0
  133. package/dist/data-grid/data-grid.js +238 -0
  134. package/dist/data-grid/geometry.js +32 -0
  135. package/dist/data-grid/grid-footer.js +33 -0
  136. package/dist/data-grid/grid-header.js +69 -0
  137. package/dist/data-grid/grid-row.js +83 -0
  138. package/dist/data-grid/grid-template.js +12 -0
  139. package/dist/data-grid/header-menu.js +52 -0
  140. package/dist/data-grid/page-cache.js +27 -0
  141. package/dist/data-grid/pinned-storage.js +13 -0
  142. package/dist/data-grid/row-menu.js +88 -0
  143. package/dist/data-grid/use-grid-scroll.js +80 -0
  144. package/dist/data-grid/use-horizontal-scroll.js +38 -0
  145. package/dist/data-grid/use-paged-rows.js +50 -0
  146. package/dist/data-grid/use-pinned-columns.js +16 -0
  147. package/dist/filter-panel/active-filter-chips.js +34 -0
  148. package/dist/filter-panel/date-presets.js +20 -0
  149. package/dist/filter-panel/facet-group.js +48 -0
  150. package/dist/filter-panel/filter-config.js +18 -0
  151. package/dist/filter-panel/filter-menu.js +41 -0
  152. package/dist/filter-panel/filter-panel.js +112 -0
  153. package/dist/filter-panel/filter-section.js +11 -0
  154. package/dist/filter-panel/filter-state.js +63 -0
  155. package/dist/filter-panel/range-filter.js +54 -0
  156. package/dist/filter-panel/range-math.js +11 -0
  157. package/dist/filter-panel/range-slider.css +32 -0
  158. package/dist/filter-panel/toggle-row.js +13 -0
  159. package/dist/filter-panel/use-filter-state.js +15 -0
  160. package/dist/format/date.js +37 -0
  161. package/dist/format/money.js +15 -0
  162. package/dist/format/number.js +12 -0
  163. package/dist/index.d.ts +12 -0
  164. package/dist/index.js +10 -0
  165. package/dist/inline-child-table/columns.js +4 -0
  166. package/dist/inline-child-table/inline-child-table.js +120 -0
  167. package/dist/inline-child-table/line-menu.js +54 -0
  168. package/dist/inline-child-table/line-row.js +91 -0
  169. package/dist/inline-child-table/number-cell.js +25 -0
  170. package/dist/inline-child-table/reorder.js +10 -0
  171. package/dist/inline-child-table/totals-footer.js +42 -0
  172. package/dist/lib/cn.js +4 -0
  173. package/dist/lib/guarded.js +12 -0
  174. package/dist/lib/use-debounced.js +13 -0
  175. package/dist/lib/use-focus-trap.js +46 -0
  176. package/dist/lib/use-media-query.js +12 -0
  177. package/dist/live/columns-from-model.js +93 -0
  178. package/dist/live/display-value.js +24 -0
  179. package/dist/live/field-values.js +32 -0
  180. package/dist/live/filter-string.js +135 -0
  181. package/dist/live/grid-loader.js +63 -0
  182. package/dist/live/model-helpers.js +30 -0
  183. package/dist/live/naming.js +9 -0
  184. package/dist/live/relation-labels.js +53 -0
  185. package/dist/live/use-filter-config.js +88 -0
  186. package/dist/live/use-live-rows.js +45 -0
  187. package/dist/live/view-fields.js +35 -0
  188. package/dist/mocks/activity.js +24 -0
  189. package/dist/mocks/invoices.js +13 -0
  190. package/dist/mocks/products.js +167 -0
  191. package/dist/mocks/rng.js +20 -0
  192. package/dist/mocks/users.js +63 -0
  193. package/dist/primitives/avatar.js +22 -0
  194. package/dist/primitives/badge.d.ts +1 -0
  195. package/dist/primitives/badge.js +25 -0
  196. package/dist/primitives/button.js +23 -0
  197. package/dist/primitives/checkbox.js +21 -0
  198. package/dist/primitives/dialog.js +41 -0
  199. package/dist/primitives/input.js +16 -0
  200. package/dist/primitives/kbd.js +8 -0
  201. package/dist/primitives/segmented.js +14 -0
  202. package/dist/primitives/sheet.js +44 -0
  203. package/dist/primitives/skeleton.js +6 -0
  204. package/dist/primitives/spinner.js +10 -0
  205. package/dist/primitives/switch.js +14 -0
  206. package/dist/primitives/tooltip.js +13 -0
  207. package/dist/record-view/action-rail.js +28 -0
  208. package/dist/record-view/field.js +62 -0
  209. package/dist/record-view/record-header.js +50 -0
  210. package/dist/record-view/record-sidebar.js +58 -0
  211. package/dist/record-view/section.js +21 -0
  212. package/dist/styles.css +132 -0
  213. package/dist/toasts/toast-provider.js +69 -0
  214. package/dist/toasts/toast.js +45 -0
  215. package/package.json +45 -3
  216. package/README.md +0 -3
@@ -0,0 +1,29 @@
1
+ //#region src/auth/reset-link.ts
2
+ /** Marks the address an emailed reset link leads back to, so the app opens the set-password page there. */
3
+ const resetMarker = "password-reset";
4
+ /** Where the emailed link leads back to: this page, marked. */
5
+ const resetLinkTarget = (href) => {
6
+ const url = new URL(href);
7
+ return `${url.origin}${url.pathname}?${resetMarker}`;
8
+ };
9
+ /**
10
+ * The reset link this page was opened from, or `undefined`. Better Auth adds the `token`, or `error=INVALID_TOKEN`
11
+ * instead for a used or expired link; then `token` is `undefined`.
12
+ */
13
+ const readResetLink = (href) => {
14
+ const params = new URL(href).searchParams;
15
+ if (!params.has("password-reset")) return void 0;
16
+ return { token: params.get("token") || void 0 };
17
+ };
18
+ /** The address without the reset link's parameters, for the address bar once the link is used. */
19
+ const withoutResetLink = (href) => {
20
+ const url = new URL(href);
21
+ for (const name of [
22
+ resetMarker,
23
+ "token",
24
+ "error"
25
+ ]) url.searchParams.delete(name);
26
+ return `${url.pathname}${url.search}${url.hash}`;
27
+ };
28
+ //#endregion
29
+ export { readResetLink, resetLinkTarget, resetMarker, withoutResetLink };
@@ -0,0 +1,88 @@
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
+ const ResetPasswordForm = ({ onSubmit, invalidLink, onRequestNewLink, onBack, busy, error }) => {
7
+ const [password, setPassword] = useState("");
8
+ const [repeat, setRepeat] = useState("");
9
+ const [mismatch, setMismatch] = useState(false);
10
+ if (invalidLink) return /* @__PURE__ */ jsxs(AuthLayout, {
11
+ title: "This link no longer works",
12
+ children: [/* @__PURE__ */ jsx("p", {
13
+ className: "text-[13px] text-muted-foreground",
14
+ children: "A reset link works once, for a limited time. Ask for a new one to choose your password."
15
+ }), /* @__PURE__ */ jsxs("div", {
16
+ className: "mt-4 flex flex-col gap-2",
17
+ children: [onRequestNewLink && /* @__PURE__ */ jsx(Button, {
18
+ variant: "primary",
19
+ className: "min-h-10",
20
+ onClick: onRequestNewLink,
21
+ children: "Send a new link"
22
+ }), /* @__PURE__ */ jsx(Button, {
23
+ variant: onRequestNewLink ? "ghost" : "primary",
24
+ className: "min-h-10",
25
+ onClick: onBack,
26
+ children: "Back to sign in"
27
+ })]
28
+ })]
29
+ });
30
+ const shown = mismatch ? "The passwords are not the same." : error;
31
+ return /* @__PURE__ */ jsx(AuthLayout, {
32
+ title: "Choose a new password",
33
+ description: `At least 12 characters.`,
34
+ children: /* @__PURE__ */ jsxs("form", {
35
+ className: "flex flex-col gap-4",
36
+ onSubmit: (event) => {
37
+ event.preventDefault();
38
+ setMismatch(password !== repeat);
39
+ if (password === repeat) onSubmit(password);
40
+ },
41
+ children: [
42
+ /* @__PURE__ */ jsxs("label", {
43
+ className: "flex flex-col gap-1.5 text-[13px] font-medium",
44
+ children: ["New password", /* @__PURE__ */ jsx(Input, {
45
+ type: "password",
46
+ name: "new-password",
47
+ autoComplete: "new-password",
48
+ required: true,
49
+ minLength: 12,
50
+ autoFocus: true,
51
+ value: password,
52
+ invalid: Boolean(shown),
53
+ onChange: (event) => setPassword(event.target.value),
54
+ className: "min-h-6"
55
+ })]
56
+ }),
57
+ /* @__PURE__ */ jsxs("label", {
58
+ className: "flex flex-col gap-1.5 text-[13px] font-medium",
59
+ children: ["Repeat new password", /* @__PURE__ */ jsx(Input, {
60
+ type: "password",
61
+ name: "repeat-password",
62
+ autoComplete: "new-password",
63
+ required: true,
64
+ minLength: 12,
65
+ value: repeat,
66
+ invalid: Boolean(shown),
67
+ onChange: (event) => setRepeat(event.target.value),
68
+ className: "min-h-6"
69
+ })]
70
+ }),
71
+ shown && /* @__PURE__ */ jsx("p", {
72
+ role: "alert",
73
+ className: "text-xs font-medium text-danger-text",
74
+ children: shown
75
+ }),
76
+ /* @__PURE__ */ jsx(Button, {
77
+ variant: "primary",
78
+ type: "submit",
79
+ loading: busy,
80
+ className: "min-h-10",
81
+ children: "Set new password"
82
+ })
83
+ ]
84
+ })
85
+ });
86
+ };
87
+ //#endregion
88
+ export { ResetPasswordForm };
@@ -0,0 +1,43 @@
1
+ import { useAuth } from "./auth-provider.js";
2
+ import { ResetPasswordForm } from "./reset-password-form.js";
3
+ import { useState } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+ import { AuthError } from "@protobase/client";
6
+ //#region src/auth/reset-password-page.tsx
7
+ /** Better Auth's answers to a new password, in words for the person at the keyboard. */
8
+ const resetPasswordMessage = (error) => {
9
+ if (error.status === 429) return "Too many attempts. Wait a minute and try again.";
10
+ if (error.code === "PASSWORD_TOO_SHORT") return `Use at least 12 characters.`;
11
+ if (error.code === "PASSWORD_TOO_LONG") return "That password is too long.";
12
+ return error.message;
13
+ };
14
+ /** Sets a new password with the token of an emailed reset link. */
15
+ const ResetPasswordPage = ({ token, onDone, onRequestNewLink, onBack }) => {
16
+ const { session } = useAuth();
17
+ const [busy, setBusy] = useState(false);
18
+ const [error, setError] = useState();
19
+ const [invalid, setInvalid] = useState(!token);
20
+ const submit = async (password) => {
21
+ setBusy(true);
22
+ setError(void 0);
23
+ try {
24
+ await session.resetPassword(token, password);
25
+ onDone();
26
+ } catch (failure) {
27
+ if (!(failure instanceof AuthError)) throw failure;
28
+ if (failure.code === "INVALID_TOKEN") setInvalid(true);
29
+ else setError(resetPasswordMessage(failure));
30
+ setBusy(false);
31
+ }
32
+ };
33
+ return /* @__PURE__ */ jsx(ResetPasswordForm, {
34
+ busy,
35
+ error,
36
+ invalidLink: invalid,
37
+ onRequestNewLink,
38
+ onBack,
39
+ onSubmit: (password) => void submit(password)
40
+ });
41
+ };
42
+ //#endregion
43
+ export { ResetPasswordPage, resetPasswordMessage };
@@ -0,0 +1,74 @@
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/sign-in-form.tsx
7
+ const SignInForm = ({ onSubmit, busy, error, workspace, notice, onForgotPassword }) => {
8
+ const [email, setEmail] = useState("");
9
+ const [password, setPassword] = useState("");
10
+ return /* @__PURE__ */ jsxs(AuthLayout, {
11
+ title: "Sign in to Protobase",
12
+ description: workspace,
13
+ children: [notice && /* @__PURE__ */ jsx("p", {
14
+ role: "status",
15
+ className: "mb-4 rounded-md bg-success-soft px-3 py-2 text-xs font-medium text-success-text",
16
+ children: notice
17
+ }), /* @__PURE__ */ jsxs("form", {
18
+ className: "flex flex-col gap-4",
19
+ onSubmit: (event) => {
20
+ event.preventDefault();
21
+ onSubmit(email.trim(), password);
22
+ },
23
+ children: [
24
+ /* @__PURE__ */ jsxs("label", {
25
+ className: "flex flex-col gap-1.5 text-[13px] font-medium",
26
+ children: ["Email", /* @__PURE__ */ jsx(Input, {
27
+ type: "email",
28
+ name: "email",
29
+ autoComplete: "username",
30
+ required: true,
31
+ autoFocus: true,
32
+ value: email,
33
+ invalid: Boolean(error),
34
+ onChange: (event) => setEmail(event.target.value),
35
+ className: "min-h-6"
36
+ })]
37
+ }),
38
+ /* @__PURE__ */ jsxs("label", {
39
+ className: "flex flex-col gap-1.5 text-[13px] font-medium",
40
+ children: ["Password", /* @__PURE__ */ jsx(Input, {
41
+ type: "password",
42
+ name: "password",
43
+ autoComplete: "current-password",
44
+ required: true,
45
+ value: password,
46
+ invalid: Boolean(error),
47
+ onChange: (event) => setPassword(event.target.value),
48
+ className: "min-h-6"
49
+ })]
50
+ }),
51
+ error && /* @__PURE__ */ jsx("p", {
52
+ role: "alert",
53
+ className: "text-xs font-medium text-danger-text",
54
+ children: error
55
+ }),
56
+ /* @__PURE__ */ jsx(Button, {
57
+ variant: "primary",
58
+ type: "submit",
59
+ loading: busy,
60
+ className: "min-h-10",
61
+ children: "Sign in"
62
+ }),
63
+ onForgotPassword && /* @__PURE__ */ jsx("button", {
64
+ type: "button",
65
+ onClick: onForgotPassword,
66
+ className: "self-center text-[13px] font-medium text-primary-text hover:underline",
67
+ children: "Forgot password?"
68
+ })
69
+ ]
70
+ })]
71
+ });
72
+ };
73
+ //#endregion
74
+ export { SignInForm };
@@ -0,0 +1,38 @@
1
+ import { useAuth } from "./auth-provider.js";
2
+ import { SignInForm } from "./sign-in-form.js";
3
+ import { useState } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+ import { AuthError } from "@protobase/client";
6
+ //#region src/auth/sign-in-page.tsx
7
+ /** Better Auth's messages, in words for the person at the keyboard. */
8
+ const signInMessage = (error) => {
9
+ if (error.status === 429) return "Too many attempts. Wait a minute and try again.";
10
+ if (error.status === 401 || error.code === "INVALID_EMAIL_OR_PASSWORD") return "The email or password is not right.";
11
+ return error.message;
12
+ };
13
+ const SignInPage = ({ workspace, notice, onForgotPassword }) => {
14
+ const { signIn } = useAuth();
15
+ const [busy, setBusy] = useState(false);
16
+ const [error, setError] = useState();
17
+ const submit = async (email, password) => {
18
+ setBusy(true);
19
+ setError(void 0);
20
+ try {
21
+ await signIn(email, password);
22
+ } catch (failure) {
23
+ if (!(failure instanceof AuthError)) throw failure;
24
+ setError(signInMessage(failure));
25
+ setBusy(false);
26
+ }
27
+ };
28
+ return /* @__PURE__ */ jsx(SignInForm, {
29
+ busy,
30
+ error,
31
+ workspace,
32
+ notice,
33
+ onForgotPassword,
34
+ onSubmit: (email, password) => void submit(email, password)
35
+ });
36
+ };
37
+ //#endregion
38
+ export { SignInPage, signInMessage };
@@ -0,0 +1,18 @@
1
+ //#region src/auth/user-to-shell.ts
2
+ const hueOf = (text) => [...text].reduce((sum, char) => (sum * 31 + char.charCodeAt(0)) % 360, 7);
3
+ const initialsOf = (name) => name.split(/[\s@.]+/).filter(Boolean).slice(0, 2).map((part) => part[0].toUpperCase()).join("");
4
+ /** What the sidebar's profile menu shows for a signed-in user. */
5
+ const userToShell = (user) => {
6
+ const roles = (user.role ?? "user").split(",").map((role) => role.trim());
7
+ const admin = roles.includes("admin");
8
+ return {
9
+ name: user.name || user.email,
10
+ email: user.email,
11
+ initials: initialsOf(user.name || user.email) || "?",
12
+ hue: hueOf(user.id || user.email),
13
+ role: admin ? "Admin" : (roles[0] ?? "user").replace(/^./, (char) => char.toUpperCase()),
14
+ roleTone: admin ? "violet" : "blue"
15
+ };
16
+ };
17
+ //#endregion
18
+ export { userToShell };
@@ -0,0 +1,54 @@
1
+ import { ReactNode } from "react";
2
+ import { QueryClient } from "@tanstack/react-query";
3
+ import { Client, ClientOptions } from "@protobase/client";
4
+ //#region src/data/api-provider.d.ts
5
+ export declare const useClient: () => {
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>>>;
7
+ search: <R extends import("@protobase/client").ResourceRef>(resource: R, params?: import("@protobase/client").ListParams) => Promise<import("@protobase/client").ListPage<import("@protobase/client").RecordOf<R>>>;
8
+ get: <R extends import("@protobase/client").ResourceRef>(resource: R, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[]) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>>>;
9
+ create: <R extends import("@protobase/client").ResourceRef>(resource: R, body: Partial<import("@protobase/client").RecordOf<R>>) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>> & {
10
+ location: string;
11
+ }>;
12
+ update: <R extends import("@protobase/client").ResourceRef>(resource: R, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], patch: Partial<import("@protobase/client").RecordOf<R>>, etag: string) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>>>;
13
+ remove: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], { etag }?: {
14
+ etag?: string;
15
+ }) => Promise<void>;
16
+ undelete: <R extends import("@protobase/client").ResourceRef>(resource: R, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[]) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>>>;
17
+ reveal: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], field: string) => Promise<unknown>;
18
+ batchWrite: (ops: import("@protobase/client").BatchOp[]) => Promise<import("@protobase/client").BatchResult>;
19
+ facets: (resource: import("@protobase/client").ResourceRef, field: string, o?: {
20
+ filter?: import("@protobase/client").FilterInput;
21
+ limit?: number;
22
+ }) => Promise<import("@protobase/client").Facet[]>;
23
+ series: (resource: import("@protobase/client").ResourceRef, params: import("@protobase/client").SeriesParams & {
24
+ filter?: import("@protobase/client").FilterInput;
25
+ }) => Promise<import("@protobase/client").Series>;
26
+ histogram: (resource: import("@protobase/client").ResourceRef, field: string, o?: {
27
+ filter?: import("@protobase/client").FilterInput;
28
+ buckets?: number;
29
+ }) => Promise<import("@protobase/client").Histogram>;
30
+ seek: <R extends import("@protobase/client").ResourceRef>(resource: R, params: import("@protobase/client").ListParams & {
31
+ position: number;
32
+ }) => Promise<import("@protobase/client").ListPage<import("@protobase/client").RecordOf<R>>>;
33
+ meta: (etag?: string) => Promise<import("@protobase/client").MetaResult>;
34
+ };
35
+ export type ApiProviderProps = {
36
+ /** Defaults to `/api/v1` on the current origin. */
37
+ baseUrl?: string;
38
+ /** A ready-made client, for tests; skips the meta-version watcher. */
39
+ client?: Client;
40
+ /** The `fetch` the built client uses, for tests that keep the meta-version watcher. */
41
+ fetch?: ClientOptions['fetch'];
42
+ queryClient?: QueryClient;
43
+ /** The bearer token source, and what to do when the session is over; see `createAuthSession`. */
44
+ token?: ClientOptions['token'];
45
+ onUnauthenticated?: () => void;
46
+ children: ReactNode;
47
+ };
48
+ /**
49
+ * Owns the query cache and the API client; refetches `/meta` whenever `X-Meta-Version` changes. The version is
50
+ * compared only with the last one seen: it hashes the models and roles, while the `/meta` ETag hashes the whole
51
+ * per-caller body, so the two never match.
52
+ */
53
+ export declare const ApiProvider: ({ baseUrl, client: given, fetch, queryClient: givenQueryClient, token, onUnauthenticated, children }: ApiProviderProps) => import("react").JSX.Element;
54
+ //#endregion
@@ -0,0 +1,53 @@
1
+ import { keys } from "./query-keys.js";
2
+ import { createContext, useContext, useMemo, useRef, useState } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+ import { createClient } from "@protobase/client";
6
+ //#region src/data/api-provider.tsx
7
+ const ApiContext = createContext(null);
8
+ const useClient = () => {
9
+ const client = useContext(ApiContext);
10
+ if (!client) throw new Error("useClient must be used inside ApiProvider");
11
+ return client;
12
+ };
13
+ /**
14
+ * Owns the query cache and the API client; refetches `/meta` whenever `X-Meta-Version` changes. The version is
15
+ * compared only with the last one seen: it hashes the models and roles, while the `/meta` ETag hashes the whole
16
+ * per-caller body, so the two never match.
17
+ */
18
+ const ApiProvider = ({ baseUrl, client: given, fetch, queryClient: givenQueryClient, token, onUnauthenticated, children }) => {
19
+ const [queryClient] = useState(() => givenQueryClient ?? new QueryClient({ defaultOptions: { queries: {
20
+ staleTime: 3e4,
21
+ retry: false,
22
+ refetchOnWindowFocus: false
23
+ } } }));
24
+ const metaVersion = useRef(void 0);
25
+ const client = useMemo(() => given ?? createClient({
26
+ baseUrl,
27
+ fetch,
28
+ token,
29
+ onUnauthenticated,
30
+ onMetaVersion: (version) => {
31
+ const previous = metaVersion.current;
32
+ metaVersion.current = version;
33
+ if (previous === void 0 || previous === version) return;
34
+ queryClient.invalidateQueries({ queryKey: keys.meta }, { cancelRefetch: false });
35
+ }
36
+ }), [
37
+ given,
38
+ baseUrl,
39
+ fetch,
40
+ queryClient,
41
+ token,
42
+ onUnauthenticated
43
+ ]);
44
+ return /* @__PURE__ */ jsx(QueryClientProvider, {
45
+ client: queryClient,
46
+ children: /* @__PURE__ */ jsx(ApiContext.Provider, {
47
+ value: client,
48
+ children
49
+ })
50
+ });
51
+ };
52
+ //#endregion
53
+ export { ApiProvider, useClient };
@@ -0,0 +1,14 @@
1
+ import { ResourcePermissions } from "@protobase/client";
2
+ import { ResourceModel, UserMenuModel, ViewModel } from "@protobase/schema";
3
+ import { PageModel } from "@protobase/layout";
4
+ //#region src/data/meta-data.d.ts
5
+ export type MetaData = {
6
+ etag: string;
7
+ resources: Record<string, ResourceModel>;
8
+ views: Record<string, ViewModel>;
9
+ /** Composed pages by name. */
10
+ pages: Record<string, PageModel>;
11
+ permissions: Record<string, ResourcePermissions>;
12
+ userMenu?: UserMenuModel;
13
+ };
14
+ //#endregion
@@ -0,0 +1,9 @@
1
+ //#region src/data/meta-data.ts
2
+ const allowEverything = {
3
+ create: true,
4
+ update: true,
5
+ delete: true,
6
+ conditional: []
7
+ };
8
+ //#endregion
9
+ export { allowEverything };
@@ -0,0 +1,95 @@
1
+ //#region src/data/query-keys.ts
2
+ const keys = {
3
+ meta: ["meta"],
4
+ list: (resource, filter, orderBy, pageSize, startToken) => [
5
+ "list",
6
+ resource,
7
+ filter,
8
+ orderBy,
9
+ pageSize,
10
+ startToken
11
+ ],
12
+ page: (resource, filter, orderBy, pageSize, page) => [
13
+ "page",
14
+ resource,
15
+ filter,
16
+ orderBy,
17
+ pageSize,
18
+ page
19
+ ],
20
+ /** A sidebar group's records; starts with `list`, so invalidating a resource's lists refreshes it too. */
21
+ navRecent: (resource, filter, orderBy, pageSize) => [
22
+ "list",
23
+ resource,
24
+ filter,
25
+ orderBy,
26
+ pageSize,
27
+ "nav-recent"
28
+ ],
29
+ first: (resource, filter, orderBy) => [
30
+ "first-page",
31
+ resource,
32
+ filter,
33
+ orderBy
34
+ ],
35
+ record: (resource, key) => [
36
+ "record",
37
+ resource,
38
+ key
39
+ ],
40
+ facets: (resource, field, filter) => [
41
+ "facets",
42
+ resource,
43
+ field,
44
+ filter
45
+ ],
46
+ series: (resource, field, range, granularity, filter) => [
47
+ "series",
48
+ resource,
49
+ field,
50
+ range,
51
+ granularity,
52
+ filter
53
+ ],
54
+ histogram: (resource, field, buckets, filter) => [
55
+ "histogram",
56
+ resource,
57
+ field,
58
+ buckets,
59
+ filter
60
+ ],
61
+ relatedLabel: (resource, id) => [
62
+ "related-label",
63
+ resource,
64
+ id
65
+ ],
66
+ labels: (resource, ids) => [
67
+ "labels",
68
+ resource,
69
+ ids
70
+ ],
71
+ count: (resource, filter) => [
72
+ "count",
73
+ resource,
74
+ filter
75
+ ],
76
+ firstRecord: (resource, filter, orderBy) => [
77
+ "first-record",
78
+ resource,
79
+ filter,
80
+ orderBy
81
+ ],
82
+ pathValue: (resource, key, path) => [
83
+ "path-value",
84
+ resource,
85
+ key,
86
+ path
87
+ ],
88
+ labelMap: (resource, ids) => [
89
+ "label-map",
90
+ resource,
91
+ ids
92
+ ]
93
+ };
94
+ //#endregion
95
+ export { keys };
@@ -0,0 +1,8 @@
1
+ import { FilterInput, SeriesParams } from "@protobase/client";
2
+ //#region src/data/use-aggregates.d.ts
3
+ export declare const useFacets: (resource: string, field: string, filter?: FilterInput, enabled?: boolean) => import("@tanstack/react-query").UseQueryResult<import("@protobase/client").Facet[], Error>;
4
+ export declare const useSeries: (resource: string, params: SeriesParams & {
5
+ filter?: FilterInput;
6
+ }, enabled?: boolean) => import("@tanstack/react-query").UseQueryResult<import("@protobase/client").Series, Error>;
7
+ export declare const useHistogram: (resource: string, field: string, filter?: FilterInput, buckets?: number, enabled?: boolean) => import("@tanstack/react-query").UseQueryResult<import("@protobase/client").Histogram, Error>;
8
+ //#endregion
@@ -0,0 +1,43 @@
1
+ import { keys } from "./query-keys.js";
2
+ import { useClient } from "./api-provider.js";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { filterText } from "@protobase/client";
5
+ //#region src/data/use-aggregates.ts
6
+ const useFacets = (resource, field, filter, enabled = true) => {
7
+ const client = useClient();
8
+ const text = filterText(filter) ?? "";
9
+ return useQuery({
10
+ queryKey: keys.facets(resource, field, text),
11
+ enabled,
12
+ queryFn: () => client.facets(resource, field, {
13
+ filter: text,
14
+ limit: 50
15
+ })
16
+ });
17
+ };
18
+ const useSeries = (resource, params, enabled = true) => {
19
+ const client = useClient();
20
+ const text = filterText(params.filter) ?? "";
21
+ return useQuery({
22
+ queryKey: keys.series(resource, params.field, `${params.range ?? ""}${params.from ?? ""}${params.to ?? ""}`, params.granularity ?? "", text),
23
+ enabled,
24
+ queryFn: () => client.series(resource, {
25
+ ...params,
26
+ filter: text
27
+ })
28
+ });
29
+ };
30
+ const useHistogram = (resource, field, filter, buckets = 20, enabled = true) => {
31
+ const client = useClient();
32
+ const text = filterText(filter) ?? "";
33
+ return useQuery({
34
+ queryKey: keys.histogram(resource, field, buckets, text),
35
+ enabled,
36
+ queryFn: () => client.histogram(resource, field, {
37
+ filter: text,
38
+ buckets
39
+ })
40
+ });
41
+ };
42
+ //#endregion
43
+ export { useFacets, useHistogram, useSeries };