@protobase/ui 0.0.0-stage → 0.1.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 (232) 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 +123 -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 +30 -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/assistant-dock/assistant-dock.d.ts +15 -0
  104. package/dist/assistant-dock/assistant-dock.js +81 -0
  105. package/dist/assistant-dock/chat-message.js +9 -0
  106. package/dist/assistant-dock/composer.js +39 -0
  107. package/dist/assistant-dock/dock-item.d.ts +4 -0
  108. package/dist/assistant-dock/dock-item.js +37 -0
  109. package/dist/assistant-dock/model.d.ts +84 -0
  110. package/dist/assistant-dock/model.js +26 -0
  111. package/dist/assistant-dock/plan-card.d.ts +13 -0
  112. package/dist/assistant-dock/plan-card.js +133 -0
  113. package/dist/assistant-dock/progress-card.js +18 -0
  114. package/dist/assistant-dock/query-card.js +52 -0
  115. package/dist/assistant-dock/queue-card.js +31 -0
  116. package/dist/assistant-dock/result-card.js +77 -0
  117. package/dist/assistant-panel/content.d.ts +7 -0
  118. package/dist/assistant-panel/step-list.js +30 -0
  119. package/dist/auth/auth-gate.js +70 -0
  120. package/dist/auth/auth-layout.js +29 -0
  121. package/dist/auth/auth-provider.js +71 -0
  122. package/dist/auth/first-run-page.js +51 -0
  123. package/dist/auth/forgot-password-form.js +75 -0
  124. package/dist/auth/forgot-password-page.js +35 -0
  125. package/dist/auth/reset-link.js +29 -0
  126. package/dist/auth/reset-password-form.js +88 -0
  127. package/dist/auth/reset-password-page.js +43 -0
  128. package/dist/auth/sign-in-form.js +90 -0
  129. package/dist/auth/sign-in-page.js +39 -0
  130. package/dist/auth/user-to-shell.js +18 -0
  131. package/dist/data/api-provider.d.ts +54 -0
  132. package/dist/data/api-provider.js +53 -0
  133. package/dist/data/meta-data.d.ts +14 -0
  134. package/dist/data/meta-data.js +9 -0
  135. package/dist/data/query-keys.js +95 -0
  136. package/dist/data/use-aggregates.d.ts +8 -0
  137. package/dist/data/use-aggregates.js +43 -0
  138. package/dist/data/use-list.d.ts +231 -0
  139. package/dist/data/use-list.js +31 -0
  140. package/dist/data/use-meta.d.ts +8 -0
  141. package/dist/data/use-meta.js +35 -0
  142. package/dist/data/use-record.d.ts +6 -0
  143. package/dist/data/use-record.js +16 -0
  144. package/dist/data/use-update-record.d.ts +15 -0
  145. package/dist/data/use-update-record.js +26 -0
  146. package/dist/data-grid/cells.js +85 -0
  147. package/dist/data-grid/column-spec.js +4 -0
  148. package/dist/data-grid/columns.js +16 -0
  149. package/dist/data-grid/data-grid.js +238 -0
  150. package/dist/data-grid/geometry.js +32 -0
  151. package/dist/data-grid/grid-footer.js +33 -0
  152. package/dist/data-grid/grid-header.js +69 -0
  153. package/dist/data-grid/grid-row.js +83 -0
  154. package/dist/data-grid/grid-template.js +12 -0
  155. package/dist/data-grid/header-menu.js +52 -0
  156. package/dist/data-grid/page-cache.js +27 -0
  157. package/dist/data-grid/pinned-storage.js +13 -0
  158. package/dist/data-grid/row-menu.js +88 -0
  159. package/dist/data-grid/use-grid-scroll.js +80 -0
  160. package/dist/data-grid/use-horizontal-scroll.js +38 -0
  161. package/dist/data-grid/use-paged-rows.js +50 -0
  162. package/dist/data-grid/use-pinned-columns.js +16 -0
  163. package/dist/filter-panel/active-filter-chips.js +34 -0
  164. package/dist/filter-panel/date-presets.js +20 -0
  165. package/dist/filter-panel/facet-group.js +48 -0
  166. package/dist/filter-panel/filter-config.js +18 -0
  167. package/dist/filter-panel/filter-menu.js +41 -0
  168. package/dist/filter-panel/filter-panel.js +112 -0
  169. package/dist/filter-panel/filter-section.js +11 -0
  170. package/dist/filter-panel/filter-state.js +63 -0
  171. package/dist/filter-panel/range-filter.js +54 -0
  172. package/dist/filter-panel/range-math.js +11 -0
  173. package/dist/filter-panel/range-slider.css +32 -0
  174. package/dist/filter-panel/toggle-row.js +13 -0
  175. package/dist/filter-panel/use-filter-state.js +15 -0
  176. package/dist/format/date.js +37 -0
  177. package/dist/format/money.js +15 -0
  178. package/dist/format/number.js +12 -0
  179. package/dist/index.d.ts +14 -0
  180. package/dist/index.js +11 -0
  181. package/dist/inline-child-table/columns.js +4 -0
  182. package/dist/inline-child-table/inline-child-table.js +120 -0
  183. package/dist/inline-child-table/line-menu.js +54 -0
  184. package/dist/inline-child-table/line-row.js +91 -0
  185. package/dist/inline-child-table/number-cell.js +25 -0
  186. package/dist/inline-child-table/reorder.js +10 -0
  187. package/dist/inline-child-table/totals-footer.js +42 -0
  188. package/dist/lib/cn.js +4 -0
  189. package/dist/lib/guarded.js +12 -0
  190. package/dist/lib/use-debounced.js +13 -0
  191. package/dist/lib/use-focus-trap.js +46 -0
  192. package/dist/lib/use-media-query.js +12 -0
  193. package/dist/live/columns-from-model.js +93 -0
  194. package/dist/live/display-value.js +24 -0
  195. package/dist/live/field-values.js +32 -0
  196. package/dist/live/filter-string.js +135 -0
  197. package/dist/live/grid-loader.js +63 -0
  198. package/dist/live/model-helpers.js +30 -0
  199. package/dist/live/naming.js +9 -0
  200. package/dist/live/relation-labels.js +53 -0
  201. package/dist/live/use-filter-config.js +88 -0
  202. package/dist/live/use-live-rows.js +45 -0
  203. package/dist/live/view-fields.js +35 -0
  204. package/dist/mocks/activity.js +24 -0
  205. package/dist/mocks/invoices.js +13 -0
  206. package/dist/mocks/products.js +167 -0
  207. package/dist/mocks/rng.js +20 -0
  208. package/dist/mocks/users.js +63 -0
  209. package/dist/primitives/avatar.js +22 -0
  210. package/dist/primitives/badge.d.ts +1 -0
  211. package/dist/primitives/badge.js +25 -0
  212. package/dist/primitives/button.js +23 -0
  213. package/dist/primitives/checkbox.js +21 -0
  214. package/dist/primitives/dialog.js +41 -0
  215. package/dist/primitives/input.js +16 -0
  216. package/dist/primitives/kbd.js +8 -0
  217. package/dist/primitives/segmented.js +14 -0
  218. package/dist/primitives/sheet.js +44 -0
  219. package/dist/primitives/skeleton.js +6 -0
  220. package/dist/primitives/spinner.js +10 -0
  221. package/dist/primitives/switch.js +14 -0
  222. package/dist/primitives/tooltip.js +13 -0
  223. package/dist/record-view/action-rail.js +28 -0
  224. package/dist/record-view/field.js +62 -0
  225. package/dist/record-view/record-header.js +50 -0
  226. package/dist/record-view/record-sidebar.js +58 -0
  227. package/dist/record-view/section.js +21 -0
  228. package/dist/styles.css +132 -0
  229. package/dist/toasts/toast-provider.js +69 -0
  230. package/dist/toasts/toast.js +45 -0
  231. package/package.json +45 -3
  232. package/README.md +0 -3
@@ -0,0 +1,34 @@
1
+ import { cn } from "../../../lib/cn.js";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region src/app/pages/blocks/card-frame.tsx
4
+ /** The card every block that shows a box uses: title, description and header actions over the body. */
5
+ const CardFrame = ({ title, description, actions, className, children }) => /* @__PURE__ */ jsxs("section", {
6
+ className: cn("flex min-w-0 flex-col rounded-lg border border-border bg-background", className),
7
+ "aria-label": title,
8
+ children: [(title || description || actions) && /* @__PURE__ */ jsxs("header", {
9
+ className: "flex flex-wrap items-start justify-between gap-x-4 gap-y-2 px-4 pt-4",
10
+ children: [/* @__PURE__ */ jsxs("div", {
11
+ className: "min-w-0",
12
+ children: [title && /* @__PURE__ */ jsx("h2", {
13
+ className: "text-[15px] font-semibold tracking-tight",
14
+ children: title
15
+ }), description && /* @__PURE__ */ jsx("p", {
16
+ className: "mt-0.5 text-[13px] text-muted-foreground",
17
+ children: description
18
+ })]
19
+ }), actions && /* @__PURE__ */ jsx("div", {
20
+ className: "flex flex-wrap items-center gap-2",
21
+ children: actions
22
+ })]
23
+ }), /* @__PURE__ */ jsx("div", {
24
+ className: "min-w-0 flex-1 p-4 text-[13px]",
25
+ children
26
+ })]
27
+ });
28
+ /** A short line in place of content: nothing to show, or not loaded yet. */
29
+ const Quiet = ({ children }) => /* @__PURE__ */ jsx("p", {
30
+ className: "text-[13px] text-muted-foreground",
31
+ children
32
+ });
33
+ //#endregion
34
+ export { CardFrame, Quiet };
@@ -0,0 +1,55 @@
1
+ import { ErrorBanner } from "../../error-banner.js";
2
+ import { useAdminMeta } from "../../meta-gate.js";
3
+ import { Skeleton } from "../../../primitives/skeleton.js";
4
+ import { RecordScope } from "../record-scope.js";
5
+ import { scopeOf, useListPage } from "../use-layout-data.js";
6
+ import { propsOf } from "./block-props.js";
7
+ import { CardFrame, Quiet } from "./card-frame.js";
8
+ import { jsx } from "react/jsx-runtime";
9
+ //#region src/app/pages/blocks/card-row.tsx
10
+ const LoadedCardRow = ({ model, filter, sort, limit = 12, title, description, empty, actions, children }) => {
11
+ const page = useListPage(model, {
12
+ filter,
13
+ sort,
14
+ pageSize: limit
15
+ });
16
+ const rows = page.data?.items ?? [];
17
+ return /* @__PURE__ */ jsx(CardFrame, {
18
+ title,
19
+ description,
20
+ actions,
21
+ children: page.isPending ? /* @__PURE__ */ jsx("div", {
22
+ className: "flex gap-3",
23
+ children: Array.from({ length: 3 }, (_, index) => /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-56" }, index))
24
+ }) : page.error ? /* @__PURE__ */ jsx(ErrorBanner, {
25
+ title: "Could not load",
26
+ error: page.error
27
+ }) : rows.length === 0 ? /* @__PURE__ */ jsx(Quiet, { children: empty ?? "Nothing here yet." }) : /* @__PURE__ */ jsx("ul", {
28
+ className: "-mx-4 flex snap-x gap-3 overflow-x-auto px-4 pb-1",
29
+ children: rows.map((row) => {
30
+ const scope = scopeOf(model, row);
31
+ return /* @__PURE__ */ jsx("li", {
32
+ className: "flex w-60 shrink-0 snap-start flex-col gap-2 rounded-lg border border-border bg-surface p-3",
33
+ children: /* @__PURE__ */ jsx(RecordScope, {
34
+ value: scope,
35
+ children
36
+ })
37
+ }, scope.key);
38
+ })
39
+ })
40
+ });
41
+ };
42
+ /** Up to `limit` records side by side, each a small card made from the children, with their own actions. */
43
+ const CardRowBlock = ({ node, children, slots }) => {
44
+ const { resource, ...props } = propsOf(node);
45
+ const model = useAdminMeta().resources[resource];
46
+ if (!model) return null;
47
+ return /* @__PURE__ */ jsx(LoadedCardRow, {
48
+ ...props,
49
+ model,
50
+ actions: slots.actions,
51
+ children
52
+ });
53
+ };
54
+ //#endregion
55
+ export { CardRowBlock };
@@ -0,0 +1,19 @@
1
+ import { propsOf } from "./block-props.js";
2
+ import { CardFrame } from "./card-frame.js";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/app/pages/blocks/card.tsx
5
+ const CardBlock = ({ node, children, slots }) => {
6
+ const { title, description } = propsOf(node);
7
+ return /* @__PURE__ */ jsx(CardFrame, {
8
+ title,
9
+ description,
10
+ actions: slots.actions,
11
+ className: "h-full",
12
+ children: /* @__PURE__ */ jsx("div", {
13
+ className: "flex flex-col gap-3",
14
+ children
15
+ })
16
+ });
17
+ };
18
+ //#endregion
19
+ export { CardBlock };
@@ -0,0 +1,45 @@
1
+ import { humanize } from "../../../live/naming.js";
2
+ import { useAdminMeta } from "../../meta-gate.js";
3
+ import { Skeleton } from "../../../primitives/skeleton.js";
4
+ import { useLayoutRecord } from "../record-scope.js";
5
+ import { propsOf } from "./block-props.js";
6
+ import { FieldValue } from "../field-value.js";
7
+ import { usePathValue } from "../use-path-value.js";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ //#region src/app/pages/blocks/field.tsx
10
+ /** The view's label for the field at the end of the path, else the path in words: `companyId.city` is "Company city". */
11
+ const fieldLabel = (path, viewLabel) => {
12
+ if (viewLabel) return viewLabel;
13
+ const words = path.split(".").map((part) => humanize(part).toLowerCase()).join(" ");
14
+ return words[0].toUpperCase() + words.slice(1);
15
+ };
16
+ /** A label over a value of the record around it; `label={false}` shows the value alone. */
17
+ const FieldBlock = ({ node }) => {
18
+ const { name, label, format } = propsOf(node);
19
+ const { views } = useAdminMeta();
20
+ const value = usePathValue(useLayoutRecord(), name);
21
+ if (value.state === "missing") return null;
22
+ const shown = value.state === "loading" ? /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" }) : /* @__PURE__ */ jsx(FieldValue, {
23
+ model: value.model,
24
+ field: value.field,
25
+ value: value.value,
26
+ format
27
+ });
28
+ if (label === false) return /* @__PURE__ */ jsx("div", {
29
+ className: "text-[13px]",
30
+ children: shown
31
+ });
32
+ const text = label ?? fieldLabel(name, value.state === "ready" ? views[value.model.name]?.fields[value.field.name]?.label : void 0);
33
+ return /* @__PURE__ */ jsxs("div", {
34
+ className: "min-w-0",
35
+ children: [/* @__PURE__ */ jsx("div", {
36
+ className: "text-xs text-muted-foreground",
37
+ children: text
38
+ }), /* @__PURE__ */ jsx("div", {
39
+ className: "mt-0.5 truncate text-[13px]",
40
+ children: shown
41
+ })]
42
+ });
43
+ };
44
+ //#endregion
45
+ export { FieldBlock, fieldLabel };
@@ -0,0 +1,34 @@
1
+ import { cn } from "../../../lib/cn.js";
2
+ import { propsOf } from "./block-props.js";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/app/pages/blocks/grid.tsx
5
+ const columns = {
6
+ 1: "",
7
+ 2: "md:grid-cols-2",
8
+ 3: "md:grid-cols-2 xl:grid-cols-3",
9
+ 4: "sm:grid-cols-2 xl:grid-cols-4"
10
+ };
11
+ const spans = {
12
+ 1: "",
13
+ 2: "md:col-span-2",
14
+ 3: "md:col-span-2 xl:col-span-3",
15
+ 4: "sm:col-span-2 md:col-span-2 xl:col-span-4"
16
+ };
17
+ /** Columns that collapse to one on a phone; a child's `span` makes it wider. */
18
+ const GridBlock = ({ node, children }) => {
19
+ const { columns: count = 2 } = propsOf(node);
20
+ const childNodes = node.children;
21
+ return /* @__PURE__ */ jsx("div", {
22
+ className: cn("grid grid-cols-1 gap-4", columns[count]),
23
+ children: children.map((child, index) => {
24
+ const element = childNodes[index];
25
+ const span = typeof element === "object" ? element.props.span : void 0;
26
+ return /* @__PURE__ */ jsx("div", {
27
+ className: cn("min-w-0", span && spans[span]),
28
+ children: child
29
+ }, index);
30
+ })
31
+ });
32
+ };
33
+ //#endregion
34
+ export { GridBlock };
@@ -0,0 +1,27 @@
1
+ import { Link } from "../../router.js";
2
+ import { isAppPath } from "../action-href.js";
3
+ import { propsOf } from "./block-props.js";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region src/app/pages/blocks/link.tsx
6
+ const style = "font-medium text-primary-text hover:underline";
7
+ /** A path in the app goes through the router; other links (`mailto:`, `https:`) are ordinary anchors. Its text is its children, else the href. */
8
+ const LinkBlock = ({ node }) => {
9
+ const { href } = propsOf(node);
10
+ const label = node.children.filter((child) => typeof child === "string").join("") || href;
11
+ if (isAppPath(href)) return /* @__PURE__ */ jsx(Link, {
12
+ to: href,
13
+ className: style,
14
+ children: label
15
+ });
16
+ return /* @__PURE__ */ jsx("a", {
17
+ href,
18
+ className: style,
19
+ ...href.startsWith("http") && {
20
+ target: "_blank",
21
+ rel: "noreferrer"
22
+ },
23
+ children: label
24
+ });
25
+ };
26
+ //#endregion
27
+ export { LinkBlock };
@@ -0,0 +1,149 @@
1
+ import { useClient } from "../../../data/api-provider.js";
2
+ import { Button } from "../../../primitives/button.js";
3
+ import { humanize } from "../../../live/naming.js";
4
+ import { useToast } from "../../../toasts/toast-provider.js";
5
+ import { Dialog } from "../../../primitives/dialog.js";
6
+ import { differs, toDraft, toPatchValue } from "../../../live/field-values.js";
7
+ import { ErrorBanner } from "../../error-banner.js";
8
+ import { FieldEditor } from "../../field-editor.js";
9
+ import { useAdminMeta, usePermissions } from "../../meta-gate.js";
10
+ import { can } from "../../permissions.js";
11
+ import { Field } from "../../../record-view/field.js";
12
+ import { createBody, isChosen } from "../../create/create-values.js";
13
+ import { useLayoutRecord } from "../record-scope.js";
14
+ import { useFirstRecord, useRefreshResource } from "../use-layout-data.js";
15
+ import { propsOf } from "./block-props.js";
16
+ import { useState } from "react";
17
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
18
+ import { isApiError } from "@protobase/client";
19
+ //#region src/app/pages/blocks/modal-form.tsx
20
+ /** The literal defaults of new records, or the record's values when editing; relations as `{ key, label }` once chosen. */
21
+ const startingDraft = (model, fields, record) => Object.fromEntries(fields.map((name) => {
22
+ const field = model.fields[name];
23
+ if (record) return [name, field.type === "boolean" ? Boolean(record.record[name]) : toDraft(record.record[name])];
24
+ const fallback = field.default && "value" in field.default ? field.default.value : void 0;
25
+ return [name, field.type === "boolean" ? Boolean(fallback) : toDraft(fallback)];
26
+ }));
27
+ /** Changed fields only; a chosen relation sends its key. */
28
+ const editBody = (model, draft, record) => Object.fromEntries(Object.entries(draft).flatMap(([name, value]) => {
29
+ const field = model.fields[name];
30
+ if (isChosen(value)) return value.key === toDraft(record.record[name]) ? [] : [[name, field.type === "integer" ? Number(value.key) : value.key]];
31
+ return differs(field, value, record.record[name]) ? [[name, toPatchValue(field, value)]] : [];
32
+ }));
33
+ const ModalFormDialog = ({ model, record, mode, label, title, fields, values, onClose }) => {
34
+ const { resources, views } = useAdminMeta();
35
+ const client = useClient();
36
+ const toast = useToast();
37
+ const refresh = useRefreshResource();
38
+ const view = views[model.name];
39
+ const [draft, setDraft] = useState(() => startingDraft(model, fields, record));
40
+ const [errors, setErrors] = useState({});
41
+ const [failure, setFailure] = useState();
42
+ const [saving, setSaving] = useState(false);
43
+ const submit = async () => {
44
+ setSaving(true);
45
+ setFailure(void 0);
46
+ await (mode === "create" ? client.create(model.name, {
47
+ ...createBody(model, resources, draft),
48
+ ...values
49
+ }) : client.update(model.name, record.key, editBody(model, draft, record), record.etag ?? "*")).then(async () => {
50
+ await refresh(model.name);
51
+ toast.show({
52
+ state: "success",
53
+ title: `${title ?? label}: saved`
54
+ });
55
+ onClose();
56
+ }, (error) => {
57
+ if (isApiError(error)) setErrors(Object.fromEntries(error.errors.flatMap((entry) => entry.field ? [[entry.field, entry.message]] : [])));
58
+ setFailure(error);
59
+ });
60
+ setSaving(false);
61
+ };
62
+ return /* @__PURE__ */ jsx(Dialog, {
63
+ title: title ?? label,
64
+ onClose,
65
+ actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
66
+ onClick: onClose,
67
+ children: "Cancel"
68
+ }), /* @__PURE__ */ jsx(Button, {
69
+ variant: "primary",
70
+ loading: saving,
71
+ onClick: () => void submit(),
72
+ children: mode === "create" ? "Create" : "Save"
73
+ })] }),
74
+ children: /* @__PURE__ */ jsxs("form", {
75
+ className: "flex flex-col gap-4",
76
+ onSubmit: (event) => {
77
+ event.preventDefault();
78
+ submit();
79
+ },
80
+ children: [failure !== void 0 && Object.keys(errors).length === 0 && /* @__PURE__ */ jsx(ErrorBanner, {
81
+ title: "Not saved",
82
+ error: failure
83
+ }), fields.map((name) => {
84
+ const field = model.fields[name];
85
+ const value = draft[name];
86
+ const target = field.relation ? resources[field.relation.resource] : void 0;
87
+ return /* @__PURE__ */ jsx(Field, {
88
+ label: view?.fields[name]?.label ?? humanize(name),
89
+ help: view?.fields[name]?.help,
90
+ error: errors[name],
91
+ children: /* @__PURE__ */ jsx(FieldEditor, {
92
+ field,
93
+ hints: view?.fields[name],
94
+ value: isChosen(value) ? value.key : value,
95
+ stored: record?.record[name],
96
+ relationLabel: isChosen(value) ? value.label : void 0,
97
+ relationPicker: target ? {
98
+ target,
99
+ view: views[target.name]
100
+ } : void 0,
101
+ invalid: Boolean(errors[name]),
102
+ onChange: (next) => {
103
+ setErrors(({ [name]: _, ...rest }) => rest);
104
+ setDraft((current) => ({
105
+ ...current,
106
+ [name]: next
107
+ }));
108
+ }
109
+ })
110
+ }, name);
111
+ })]
112
+ })
113
+ });
114
+ };
115
+ const KeyedRecord = ({ model, recordKey, children }) => {
116
+ const record = useFirstRecord(model, { key: recordKey });
117
+ return /* @__PURE__ */ jsx(Fragment$1, { children: children(record.data ?? void 0) });
118
+ };
119
+ /** A button that opens a form in a dialog: a new record of `resource`, or the record around it (or `recordKey`) to edit. */
120
+ const ModalFormBlock = ({ node }) => {
121
+ const props = propsOf(node);
122
+ const scope = useLayoutRecord();
123
+ const { resources } = useAdminMeta();
124
+ const model = resources[props.resource ?? scope?.model.name ?? ""];
125
+ const permissions = usePermissions(model?.name ?? "");
126
+ const [open, setOpen] = useState(false);
127
+ if (!model) return null;
128
+ if (!(props.mode === "create" ? can(permissions, "create") : can(permissions, "update") && scope?.permissions?.update !== false)) return null;
129
+ const button = (record) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
130
+ size: "sm",
131
+ variant: props.variant ?? "secondary",
132
+ disabled: props.mode === "edit" && !record,
133
+ onClick: () => setOpen(true),
134
+ children: props.label
135
+ }), open && /* @__PURE__ */ jsx(ModalFormDialog, {
136
+ ...props,
137
+ model,
138
+ record: props.mode === "edit" ? record : void 0,
139
+ onClose: () => setOpen(false)
140
+ })] });
141
+ if (props.mode === "edit" && props.recordKey !== void 0) return /* @__PURE__ */ jsx(KeyedRecord, {
142
+ model,
143
+ recordKey: props.recordKey,
144
+ children: button
145
+ });
146
+ return button(props.mode === "edit" ? scope : void 0);
147
+ };
148
+ //#endregion
149
+ export { ModalFormBlock };
@@ -0,0 +1,20 @@
1
+ import { PageHeader } from "../../../app-shell/page-header.js";
2
+ import { propsOf } from "./block-props.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/app/pages/blocks/page-block.tsx
5
+ const PageBlock = ({ node, children, slots }) => {
6
+ const { title, description } = propsOf(node);
7
+ return /* @__PURE__ */ jsx("div", {
8
+ className: "min-h-0 flex-1 overflow-y-auto",
9
+ children: /* @__PURE__ */ jsxs("div", {
10
+ className: "mx-auto flex max-w-[1200px] flex-col gap-6 px-4 pb-16 pt-6 md:px-10 md:py-8",
11
+ children: [/* @__PURE__ */ jsx(PageHeader, {
12
+ title,
13
+ subtitle: description,
14
+ actions: slots.actions
15
+ }), children]
16
+ })
17
+ });
18
+ };
19
+ //#endregion
20
+ export { PageBlock };
@@ -0,0 +1,54 @@
1
+ import { Skeleton } from "../../../primitives/skeleton.js";
2
+ import { useLayoutRecord } from "../record-scope.js";
3
+ import { propsOf } from "./block-props.js";
4
+ import { usePathValue } from "../use-path-value.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/app/pages/blocks/progress.tsx
7
+ /** A number as given, or the number in a field of the record around it. */
8
+ const useAmount = (amount, fallback) => {
9
+ const scope = useLayoutRecord();
10
+ const value = usePathValue(typeof amount === "string" ? scope : void 0, typeof amount === "string" ? amount : "");
11
+ if (typeof amount === "number") return amount;
12
+ if (amount === void 0) return fallback;
13
+ if (value.state === "ready") return Number(value.value);
14
+ return value.state === "loading" ? void 0 : NaN;
15
+ };
16
+ /** A bar for `value` out of `max` (100 by default), with the numbers beside the label. */
17
+ const ProgressBlock = ({ node }) => {
18
+ const { value, max, label } = propsOf(node);
19
+ const current = useAmount(value, 0);
20
+ const total = useAmount(max, 100);
21
+ if (current === void 0 || total === void 0) return /* @__PURE__ */ jsx(Skeleton, { className: "h-6 w-full" });
22
+ if (!Number.isFinite(current) || !Number.isFinite(total)) return null;
23
+ const share = total > 0 ? Math.min(1, Math.max(0, current / total)) : 0;
24
+ return /* @__PURE__ */ jsxs("div", {
25
+ className: "min-w-0",
26
+ children: [/* @__PURE__ */ jsxs("div", {
27
+ className: "flex items-baseline justify-between gap-2 text-xs",
28
+ children: [/* @__PURE__ */ jsx("span", {
29
+ className: "text-muted-foreground",
30
+ children: label
31
+ }), /* @__PURE__ */ jsxs("span", {
32
+ className: "tabular-nums",
33
+ children: [
34
+ current.toLocaleString("en-IE"),
35
+ " / ",
36
+ total.toLocaleString("en-IE")
37
+ ]
38
+ })]
39
+ }), /* @__PURE__ */ jsx("div", {
40
+ role: "progressbar",
41
+ "aria-label": label,
42
+ "aria-valuenow": current,
43
+ "aria-valuemin": 0,
44
+ "aria-valuemax": total,
45
+ className: "mt-1.5 h-2 overflow-hidden rounded-full bg-muted",
46
+ children: /* @__PURE__ */ jsx("div", {
47
+ className: share >= .9 ? "h-full rounded-full bg-warning" : "h-full rounded-full bg-primary",
48
+ style: { width: `${share * 100}%` }
49
+ })
50
+ })]
51
+ });
52
+ };
53
+ //#endregion
54
+ export { ProgressBlock };
@@ -0,0 +1,56 @@
1
+ import { ErrorBanner } from "../../error-banner.js";
2
+ import { useAdminMeta } from "../../meta-gate.js";
3
+ import { Skeleton } from "../../../primitives/skeleton.js";
4
+ import { RecordScope } from "../record-scope.js";
5
+ import { useFirstRecord } from "../use-layout-data.js";
6
+ import { propsOf } from "./block-props.js";
7
+ import { CardFrame, Quiet } from "./card-frame.js";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ //#region src/app/pages/blocks/record-card.tsx
10
+ const LoadedRecordCard = ({ model, filter, sort, recordKey, title, description, empty, actions, children }) => {
11
+ const record = useFirstRecord(model, {
12
+ filter,
13
+ sort,
14
+ key: recordKey
15
+ });
16
+ const scoped = (content) => record.data ? /* @__PURE__ */ jsx(RecordScope, {
17
+ value: record.data,
18
+ children: content
19
+ }) : void 0;
20
+ const body = record.isPending ? /* @__PURE__ */ jsxs("div", {
21
+ className: "flex flex-col gap-2",
22
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-1/2" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-1/3" })]
23
+ }) : record.error ? /* @__PURE__ */ jsx(ErrorBanner, {
24
+ title: "Could not load",
25
+ error: record.error
26
+ }) : record.data ? scoped(/* @__PURE__ */ jsx("div", {
27
+ className: "flex flex-col gap-3",
28
+ children
29
+ })) : /* @__PURE__ */ jsx(Quiet, { children: empty ?? "Nothing here yet." });
30
+ return /* @__PURE__ */ jsx(CardFrame, {
31
+ title,
32
+ description,
33
+ actions: actions ? scoped(actions) : void 0,
34
+ className: "h-full",
35
+ children: body
36
+ });
37
+ };
38
+ /** One record, the one `recordKey` names or the first match: its Fields, Actions and forms inside, actions in the header. */
39
+ const RecordCardBlock = ({ node, children, slots }) => {
40
+ const { resource, filter, sort, recordKey, title, description, empty } = propsOf(node);
41
+ const model = useAdminMeta().resources[resource];
42
+ if (!model) return null;
43
+ return /* @__PURE__ */ jsx(LoadedRecordCard, {
44
+ model,
45
+ filter,
46
+ sort,
47
+ recordKey,
48
+ title,
49
+ description,
50
+ empty,
51
+ actions: slots.actions,
52
+ children
53
+ });
54
+ };
55
+ //#endregion
56
+ export { RecordCardBlock };
@@ -0,0 +1,43 @@
1
+ import { useClient } from "../../../data/api-provider.js";
2
+ import { useAdminMeta } from "../../meta-gate.js";
3
+ import { useLayoutRecord } from "../record-scope.js";
4
+ import { countQuery } from "../use-layout-data.js";
5
+ import { propsOf } from "./block-props.js";
6
+ import { usePathValues } from "../use-path-value.js";
7
+ import { useMemo } from "react";
8
+ import { Fragment as Fragment$1, jsx } from "react/jsx-runtime";
9
+ import { useQueries } from "@tanstack/react-query";
10
+ import { countedResource, evaluateCondition, parseCondition } from "@protobase/layout";
11
+ //#region src/app/pages/blocks/show.tsx
12
+ /** Its children while `when` holds: fields of the record around it, and `<resource>.count` for any resource. */
13
+ const ShowBlock = ({ node, children }) => {
14
+ const { when } = propsOf(node);
15
+ const { resources } = useAdminMeta();
16
+ const client = useClient();
17
+ const scope = useLayoutRecord();
18
+ const condition = useMemo(() => parseCondition(when), [when]);
19
+ const paths = condition.ok ? condition.paths : [];
20
+ const counted = paths.map((path) => countedResource(path, (name) => Boolean(scope && Object.hasOwn(scope.model.fields, name))));
21
+ const fieldPaths = paths.filter((_, index) => counted[index] === void 0);
22
+ const fields = usePathValues(scope, fieldPaths);
23
+ const counts = useQueries({ queries: counted.flatMap((resource) => {
24
+ const model = resource === void 0 ? void 0 : resources[resource];
25
+ if (!model) return [];
26
+ return [countQuery(client, model)];
27
+ }) });
28
+ if (!condition.ok) return null;
29
+ if (fields.some((value) => value.state === "loading") || counts.some((count) => count.isPending)) return null;
30
+ const values = {};
31
+ fieldPaths.forEach((path, index) => {
32
+ const value = fields[index];
33
+ if (value.state === "ready") values[path] = value.value;
34
+ });
35
+ let next = 0;
36
+ paths.forEach((path, index) => {
37
+ if (counted[index] === void 0) return;
38
+ values[path] = resources[counted[index]] ? counts[next++]?.data ?? 0 : 0;
39
+ });
40
+ return evaluateCondition(condition, values) ? /* @__PURE__ */ jsx(Fragment$1, { children }) : null;
41
+ };
42
+ //#endregion
43
+ export { ShowBlock };
@@ -0,0 +1,78 @@
1
+ import { Link } from "../../router.js";
2
+ import { useAdminMeta } from "../../meta-gate.js";
3
+ import { Skeleton } from "../../../primitives/skeleton.js";
4
+ import { useLayoutRecord } from "../record-scope.js";
5
+ import { useCount } from "../use-layout-data.js";
6
+ import { propsOf } from "./block-props.js";
7
+ import { CardFrame } from "./card-frame.js";
8
+ import { FieldValue } from "../field-value.js";
9
+ import { usePathValue } from "../use-path-value.js";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ //#region src/app/pages/blocks/stat.tsx
12
+ const big = "text-2xl font-semibold tracking-tight tabular-nums";
13
+ const Count = ({ model, filter }) => {
14
+ const count = useCount(model, filter);
15
+ if (count.isPending) return /* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-16" });
16
+ if (count.error) return /* @__PURE__ */ jsx("span", {
17
+ className: "text-[13px] text-danger-text",
18
+ children: "Could not count"
19
+ });
20
+ const href = `/${model.name}${filter ? `?filter=${encodeURIComponent(filter)}` : ""}`;
21
+ return /* @__PURE__ */ jsx(Link, {
22
+ to: href,
23
+ className: `${big} hover:text-primary-text`,
24
+ children: count.data.toLocaleString("en-IE")
25
+ });
26
+ };
27
+ const RecordValue = ({ field, format }) => {
28
+ const value = usePathValue(useLayoutRecord(), field);
29
+ if (value.state === "loading") return /* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-16" });
30
+ if (value.state === "missing") return null;
31
+ return /* @__PURE__ */ jsx("span", {
32
+ className: big,
33
+ children: /* @__PURE__ */ jsx(FieldValue, {
34
+ model: value.model,
35
+ field: value.field,
36
+ value: value.value,
37
+ format
38
+ })
39
+ });
40
+ };
41
+ /** One number or value with a label: fixed, a count of matching records (linking to them), or a field of the record. */
42
+ const StatBlock = ({ node }) => {
43
+ const { label, value, resource, filter, field, format, description } = propsOf(node);
44
+ const { resources } = useAdminMeta();
45
+ const model = resource === void 0 ? void 0 : resources[resource];
46
+ return /* @__PURE__ */ jsxs(CardFrame, {
47
+ className: "h-full",
48
+ children: [
49
+ /* @__PURE__ */ jsx("p", {
50
+ className: "text-[13px] text-muted-foreground",
51
+ children: label
52
+ }),
53
+ /* @__PURE__ */ jsxs("div", {
54
+ className: "mt-1",
55
+ children: [
56
+ value !== void 0 && /* @__PURE__ */ jsx("span", {
57
+ className: big,
58
+ children: value
59
+ }),
60
+ model && /* @__PURE__ */ jsx(Count, {
61
+ model,
62
+ filter
63
+ }),
64
+ field !== void 0 && /* @__PURE__ */ jsx(RecordValue, {
65
+ field,
66
+ format
67
+ })
68
+ ]
69
+ }),
70
+ description && /* @__PURE__ */ jsx("p", {
71
+ className: "mt-1 text-xs text-muted-foreground",
72
+ children: description
73
+ })
74
+ ]
75
+ });
76
+ };
77
+ //#endregion
78
+ export { StatBlock };