@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,94 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { navOpenStorageKey, readNavOpen, writeNavOpen } from "./nav-open-storage.js";
3
+ import { SidebarItem } from "./sidebar-item.js";
4
+ import { StatusDot } from "./status-dot.js";
5
+ import { useId, useState } from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { ChevronRight } from "lucide-react";
8
+ //#region src/app-shell/sidebar-group.tsx
9
+ const row = {
10
+ "text-large": "h-8 gap-2.5 px-2.5 text-[13px]",
11
+ "text-small": "h-7 gap-2 px-2 text-xs"
12
+ };
13
+ /** Lines the records up under the entry's icon. */
14
+ const indent = {
15
+ "text-large": "ml-[20px]",
16
+ "text-small": "ml-[17px]"
17
+ };
18
+ /**
19
+ * A sidebar entry with a few of its records under it. The label opens the list, the chevron shows or hides the
20
+ * records (remembered in this browser), and "View all" at the end opens the list too.
21
+ */
22
+ const SidebarGroup = ({ item, recent, mode, active, onNavigate }) => {
23
+ const storageKey = navOpenStorageKey(item.id);
24
+ const [open, setOpen] = useState(() => readNavOpen(localStorage, storageKey));
25
+ const listId = useId();
26
+ const href = item.href ?? `#${item.id}`;
27
+ const toggle = () => {
28
+ writeNavOpen(localStorage, storageKey, !open);
29
+ setOpen(!open);
30
+ };
31
+ const link = (to) => ({
32
+ href: to,
33
+ onClick: onNavigate && ((event) => {
34
+ event.preventDefault();
35
+ onNavigate(to);
36
+ })
37
+ });
38
+ return /* @__PURE__ */ jsxs("div", {
39
+ className: "flex flex-col gap-0.5",
40
+ children: [/* @__PURE__ */ jsxs("div", {
41
+ className: "relative",
42
+ children: [/* @__PURE__ */ jsx(SidebarItem, {
43
+ item,
44
+ mode,
45
+ active,
46
+ onNavigate,
47
+ className: "pr-8"
48
+ }), /* @__PURE__ */ jsx("button", {
49
+ type: "button",
50
+ "aria-expanded": open,
51
+ "aria-controls": listId,
52
+ "aria-label": `Recent ${item.label.toLowerCase()}`,
53
+ onClick: toggle,
54
+ className: "absolute right-1 top-1/2 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded text-faint-foreground hover:bg-muted hover:text-foreground",
55
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: cn("size-3.5 transition-transform", open && "rotate-90") })
56
+ })]
57
+ }), open && /* @__PURE__ */ jsxs("ul", {
58
+ id: listId,
59
+ "aria-label": `Recent ${item.label.toLowerCase()}`,
60
+ className: cn("flex flex-col gap-0.5 border-l pl-1.5", indent[mode]),
61
+ children: [
62
+ recent.state === "loading" && /* @__PURE__ */ jsx("li", {
63
+ className: cn("flex items-center text-faint-foreground", row[mode]),
64
+ children: "Loading..."
65
+ }),
66
+ recent.state === "error" && /* @__PURE__ */ jsx("li", {
67
+ className: cn("flex items-center text-faint-foreground", row[mode]),
68
+ children: "Could not load"
69
+ }),
70
+ recent.state === "ready" && recent.records.map((record) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", {
71
+ ...link(record.href),
72
+ "aria-label": `${record.label}, ${record.status}`,
73
+ title: `${record.label} (${record.status})`,
74
+ "aria-current": record.active ? "page" : void 0,
75
+ className: cn("flex min-w-0 items-center rounded-md font-medium transition-colors", row[mode], record.active ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground"),
76
+ children: [/* @__PURE__ */ jsx(StatusDot, {
77
+ tone: record.tone,
78
+ pulse: record.pulse
79
+ }), /* @__PURE__ */ jsx("span", {
80
+ className: "truncate",
81
+ children: record.label
82
+ })]
83
+ }) }, record.id)),
84
+ /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("a", {
85
+ ...link(href),
86
+ className: cn("flex items-center rounded-md text-faint-foreground transition-colors hover:bg-muted hover:text-foreground", row[mode]),
87
+ children: "View all"
88
+ }) })
89
+ ]
90
+ })]
91
+ });
92
+ };
93
+ //#endregion
94
+ export { SidebarGroup };
@@ -0,0 +1,46 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { Tooltip } from "../primitives/tooltip.js";
3
+ import { isTextMode, showsLabelInline } from "./sidebar-mode.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/app-shell/sidebar-item.tsx
6
+ const layout = {
7
+ "text-large": "h-9 gap-3 px-3 text-sm",
8
+ "text-small": "h-7 gap-2.5 px-2.5 text-xs",
9
+ icon: "size-9 justify-center",
10
+ "icon-label": "w-full flex-col gap-0.5 py-1.5 text-[10px]",
11
+ "icon-tooltip": "size-9 justify-center",
12
+ "icon-expand": "h-8 gap-3 px-3 text-[13px]"
13
+ };
14
+ const SidebarItem = ({ item, mode, active, onNavigate, className }) => {
15
+ const Icon = item.icon;
16
+ const button = /* @__PURE__ */ jsxs("a", {
17
+ onClick: onNavigate && ((event) => {
18
+ event.preventDefault();
19
+ onNavigate(item.href ?? `#${item.id}`);
20
+ }),
21
+ href: item.href ?? `#${item.id}`,
22
+ "aria-label": item.label,
23
+ "aria-current": active ? "page" : void 0,
24
+ className: cn("flex shrink-0 items-center rounded-md font-medium transition-colors", layout[mode], active ? "bg-primary-soft text-primary-text" : "text-muted-foreground hover:bg-muted hover:text-foreground", className),
25
+ children: [
26
+ /* @__PURE__ */ jsx(Icon, {
27
+ className: cn("shrink-0", mode === "text-small" ? "size-4" : "size-[18px]"),
28
+ strokeWidth: active ? 2.2 : 1.9
29
+ }),
30
+ (showsLabelInline(mode) || mode === "icon-label") && /* @__PURE__ */ jsx("span", {
31
+ className: "whitespace-nowrap",
32
+ children: item.label
33
+ }),
34
+ isTextMode(mode) && item.count && /* @__PURE__ */ jsx("span", {
35
+ className: "ml-auto rounded bg-muted px-1.5 text-[11px] tabular-nums text-muted-foreground",
36
+ children: item.count
37
+ })
38
+ ]
39
+ });
40
+ return mode === "icon-tooltip" ? /* @__PURE__ */ jsx(Tooltip, {
41
+ label: item.label,
42
+ children: button
43
+ }) : button;
44
+ };
45
+ //#endregion
46
+ export { SidebarItem };
@@ -0,0 +1,4 @@
1
+ //#region src/app-shell/sidebar-mode.d.ts
2
+ export declare const sidebarModes: readonly ["text-large", "text-small", "icon", "icon-label", "icon-tooltip", "icon-expand"];
3
+ export type SidebarMode = (typeof sidebarModes)[number];
4
+ //#endregion
@@ -0,0 +1,13 @@
1
+ //#region src/app-shell/sidebar-mode.ts
2
+ const showsLabelInline = (mode) => mode === "text-large" || mode === "text-small" || mode === "icon-expand";
3
+ const isTextMode = (mode) => mode === "text-large" || mode === "text-small";
4
+ const sidebarWidth = {
5
+ "text-large": "w-60",
6
+ "text-small": "w-52",
7
+ icon: "w-14",
8
+ "icon-label": "w-[76px]",
9
+ "icon-tooltip": "w-14",
10
+ "icon-expand": "w-14"
11
+ };
12
+ //#endregion
13
+ export { isTextMode, showsLabelInline, sidebarWidth };
@@ -0,0 +1,2 @@
1
+ import "./nav.js";
2
+ import "./profile-menu.js";
@@ -0,0 +1,64 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { Logo } from "./logo.js";
3
+ import { navGroups } from "./nav.js";
4
+ import { ProfileMenu } from "./profile-menu.js";
5
+ import { isTextMode, sidebarWidth } from "./sidebar-mode.js";
6
+ import { SidebarItem } from "./sidebar-item.js";
7
+ import { SidebarGroup } from "./sidebar-group.js";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ //#region src/app-shell/sidebar.tsx
10
+ const Sidebar = ({ mode, activeItem, user, workspace, forceExpanded, profileMenuOpen, groups = navGroups, userMenu, onNavigate, onSignOut }) => {
11
+ const expanding = mode === "icon-expand";
12
+ const expanded = expanding && forceExpanded;
13
+ const inline = isTextMode(mode) || expanding;
14
+ return /* @__PURE__ */ jsx("nav", {
15
+ "aria-label": "Resources",
16
+ className: cn("relative z-30 shrink-0", sidebarWidth[mode]),
17
+ children: /* @__PURE__ */ jsxs("div", {
18
+ className: cn("group/side absolute inset-y-0 left-0 flex flex-col overflow-hidden border-r bg-sidebar", expanding ? "w-14 transition-[width,box-shadow] duration-150 hover:w-60 hover:shadow-pop focus-within:w-60 focus-within:shadow-pop" : "w-full", expanded && "w-60 shadow-pop"),
19
+ children: [
20
+ /* @__PURE__ */ jsx("div", {
21
+ className: cn("flex h-12 shrink-0 items-center px-3.5", mode === "icon-label" && "justify-center px-0"),
22
+ children: /* @__PURE__ */ jsx(Logo, {
23
+ showName: isTextMode(mode) || expanding,
24
+ workspace
25
+ })
26
+ }),
27
+ /* @__PURE__ */ jsx("div", {
28
+ className: cn("flex-1 overflow-y-auto overflow-x-hidden pb-2", mode === "icon-label" ? "px-2" : "px-2"),
29
+ children: groups.map((group, index) => /* @__PURE__ */ jsxs("div", {
30
+ className: cn("flex flex-col gap-0.5", index > 0 && "mt-3"),
31
+ children: [inline ? /* @__PURE__ */ jsx("div", {
32
+ className: cn("px-3 pb-1 pt-1 text-[11px] font-medium uppercase tracking-wide text-faint-foreground", expanding && "h-4 whitespace-nowrap py-0 opacity-0 transition-opacity group-hover/side:opacity-100 group-focus-within/side:opacity-100", expanded && "opacity-100"),
33
+ children: group.label
34
+ }) : index > 0 && /* @__PURE__ */ jsx("div", { className: "mx-auto mb-1 h-px w-6 bg-border" }), group.items.map((item) => item.recent && isTextMode(mode) ? /* @__PURE__ */ jsx(SidebarGroup, {
35
+ item,
36
+ recent: item.recent,
37
+ mode,
38
+ active: item.id === activeItem,
39
+ onNavigate
40
+ }, item.id) : /* @__PURE__ */ jsx(SidebarItem, {
41
+ item,
42
+ mode,
43
+ active: item.id === activeItem,
44
+ onNavigate
45
+ }, item.id))]
46
+ }, group.label))
47
+ }),
48
+ /* @__PURE__ */ jsx("div", {
49
+ className: cn("shrink-0 border-t p-2", mode !== "text-large" && mode !== "text-small" && !expanding && "flex justify-center"),
50
+ children: /* @__PURE__ */ jsx(ProfileMenu, {
51
+ user,
52
+ compact: !inline,
53
+ open: profileMenuOpen,
54
+ items: userMenu,
55
+ onNavigate,
56
+ onSignOut
57
+ })
58
+ })
59
+ ]
60
+ })
61
+ });
62
+ };
63
+ //#endregion
64
+ export { Sidebar };
@@ -0,0 +1 @@
1
+ import "@protobase/schema";
@@ -0,0 +1,20 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region src/app-shell/status-dot.tsx
4
+ const colour = {
5
+ neutral: "bg-faint-foreground",
6
+ info: "bg-primary",
7
+ success: "bg-success",
8
+ warning: "bg-warning",
9
+ danger: "bg-danger"
10
+ };
11
+ /** A record's status as a coloured dot; `pulse` animates a ring around it for states in progress (still under reduced motion). */
12
+ const StatusDot = ({ tone, pulse = false, className }) => /* @__PURE__ */ jsxs("span", {
13
+ "aria-hidden": true,
14
+ className: cn("relative flex size-2 shrink-0", className),
15
+ "data-tone": tone,
16
+ "data-pulse": pulse || void 0,
17
+ children: [pulse && /* @__PURE__ */ jsx("span", { className: cn("absolute inset-0 animate-ping rounded-full opacity-60 motion-reduce:animate-none", colour[tone]) }), /* @__PURE__ */ jsx("span", { className: cn("relative size-2 rounded-full", colour[tone]) })]
18
+ });
19
+ //#endregion
20
+ export { StatusDot };
@@ -0,0 +1,82 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { Breadcrumb } from "./breadcrumb.js";
3
+ import { GlobalSearch } from "./global-search.js";
4
+ import { useEffect, useRef, useState } from "react";
5
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
+ import { Menu, Search, X } from "lucide-react";
7
+ //#region src/app-shell/top-bar.tsx
8
+ const iconButton = "inline-flex size-11 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted md:hidden";
9
+ const TopBar = ({ breadcrumb, onNavigate, actions, onMenu, menuOpen, menuControls, search }) => {
10
+ const [searchOpen, setSearchOpen] = useState(false);
11
+ const desktopSearch = useRef(null);
12
+ useEffect(() => {
13
+ if (!search) return;
14
+ const onKey = (event) => {
15
+ if (event.key.toLowerCase() !== "k" || !(event.metaKey || event.ctrlKey)) return;
16
+ event.preventDefault();
17
+ const input = desktopSearch.current;
18
+ if (input && input.offsetParent !== null) input.focus();
19
+ else setSearchOpen(true);
20
+ };
21
+ window.addEventListener("keydown", onKey);
22
+ return () => window.removeEventListener("keydown", onKey);
23
+ }, [search]);
24
+ return /* @__PURE__ */ jsxs("header", {
25
+ className: "relative flex h-12 shrink-0 items-center gap-1 border-b bg-background px-2 md:gap-4 md:px-5",
26
+ children: [
27
+ onMenu && /* @__PURE__ */ jsx("button", {
28
+ type: "button",
29
+ "aria-label": "Open navigation",
30
+ "aria-expanded": Boolean(menuOpen),
31
+ "aria-controls": menuControls,
32
+ onClick: onMenu,
33
+ className: cn(iconButton, "-ml-0.5"),
34
+ children: /* @__PURE__ */ jsx(Menu, { className: "size-5" })
35
+ }),
36
+ /* @__PURE__ */ jsx(Breadcrumb, {
37
+ parts: breadcrumb,
38
+ onNavigate
39
+ }),
40
+ /* @__PURE__ */ jsxs("div", {
41
+ className: "ml-auto flex items-center gap-1 md:gap-3",
42
+ children: [search && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
43
+ className: "hidden md:block",
44
+ children: /* @__PURE__ */ jsx(GlobalSearch, {
45
+ ref: desktopSearch,
46
+ search
47
+ })
48
+ }), /* @__PURE__ */ jsx("button", {
49
+ type: "button",
50
+ "aria-label": "Search",
51
+ "aria-expanded": searchOpen,
52
+ onClick: () => setSearchOpen(true),
53
+ className: iconButton,
54
+ children: /* @__PURE__ */ jsx(Search, { className: "size-5" })
55
+ })] }), actions]
56
+ }),
57
+ search && searchOpen && /* @__PURE__ */ jsxs("div", {
58
+ className: "absolute inset-0 z-20 flex items-center gap-1 bg-background px-3 md:hidden",
59
+ children: [/* @__PURE__ */ jsx(GlobalSearch, {
60
+ className: "min-w-0 flex-1",
61
+ autoFocus: true,
62
+ showShortcut: false,
63
+ search: {
64
+ ...search,
65
+ onSelect: (href) => {
66
+ search.onSelect(href);
67
+ setSearchOpen(false);
68
+ }
69
+ }
70
+ }), /* @__PURE__ */ jsx("button", {
71
+ type: "button",
72
+ "aria-label": "Close search",
73
+ onClick: () => setSearchOpen(false),
74
+ className: iconButton,
75
+ children: /* @__PURE__ */ jsx(X, { className: "size-5" })
76
+ })]
77
+ })
78
+ ]
79
+ });
80
+ };
81
+ //#endregion
82
+ export { TopBar };
@@ -0,0 +1,15 @@
1
+ import { DockItem } from "./model.js";
2
+ import { DockItemHandlers } from "./dock-item.js";
3
+ //#region src/assistant-dock/assistant-dock.d.ts
4
+ export type AssistantDockProps = DockItemHandlers & {
5
+ /** The chat in order: messages and the cards of its tasks and queries. */
6
+ items: DockItem[];
7
+ /** The organization's credits; Approve needs the plan's quote. */
8
+ balance: number;
9
+ replying?: boolean;
10
+ onSend?: (text: string) => void;
11
+ onClose?: () => void;
12
+ };
13
+ /** The assistant beside the app: chat, plans to approve, the queue, progress, results and query tables. */
14
+ export declare const AssistantDock: ({ items, balance, replying, onSend, onClose, onRequestChanges, ...handlers }: AssistantDockProps) => import("react").JSX.Element;
15
+ //#endregion
@@ -0,0 +1,81 @@
1
+ import { Button } from "../primitives/button.js";
2
+ import { Composer } from "./composer.js";
3
+ import { formatCredits } from "./model.js";
4
+ import { DockItemView } from "./dock-item.js";
5
+ import { useEffect, useRef, useState } from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { Sparkles, X } from "lucide-react";
8
+ //#region src/assistant-dock/assistant-dock.tsx
9
+ /** The assistant beside the app: chat, plans to approve, the queue, progress, results and query tables. */
10
+ const AssistantDock = ({ items, balance, replying, onSend, onClose, onRequestChanges, ...handlers }) => {
11
+ const list = useRef(null);
12
+ const input = useRef(null);
13
+ const [revising, setRevising] = useState(false);
14
+ useEffect(() => {
15
+ const element = list.current;
16
+ if (element) element.scrollTop = element.scrollHeight;
17
+ }, [items.length]);
18
+ const requestChanges = (taskId) => {
19
+ setRevising(true);
20
+ input.current?.focus();
21
+ onRequestChanges?.(taskId);
22
+ };
23
+ const send = (text) => {
24
+ setRevising(false);
25
+ onSend?.(text);
26
+ };
27
+ return /* @__PURE__ */ jsxs("aside", {
28
+ "aria-label": "Assistant",
29
+ className: "flex h-full w-[380px] max-w-full shrink-0 flex-col border-l border-border bg-surface",
30
+ children: [
31
+ /* @__PURE__ */ jsxs("header", {
32
+ className: "flex h-12 shrink-0 items-center gap-2 border-b border-border bg-background px-4",
33
+ children: [
34
+ /* @__PURE__ */ jsx(Sparkles, { className: "size-4 text-primary" }),
35
+ /* @__PURE__ */ jsx("h2", {
36
+ className: "flex-1 text-[13px] font-semibold",
37
+ children: "Assistant"
38
+ }),
39
+ /* @__PURE__ */ jsx("span", {
40
+ className: "text-xs font-medium text-muted-foreground",
41
+ children: formatCredits(balance)
42
+ }),
43
+ onClose && /* @__PURE__ */ jsx(Button, {
44
+ variant: "ghost",
45
+ size: "sm",
46
+ className: "w-7 px-0",
47
+ "aria-label": "Close",
48
+ onClick: onClose,
49
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
50
+ })
51
+ ]
52
+ }),
53
+ /* @__PURE__ */ jsxs("div", {
54
+ ref: list,
55
+ className: "flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4",
56
+ children: [items.length === 0 && /* @__PURE__ */ jsx("p", {
57
+ className: "m-auto max-w-60 text-center text-[13px] text-muted-foreground",
58
+ children: "Ask for a change to this app, or a question about its data."
59
+ }), items.map((item) => /* @__PURE__ */ jsx(DockItemView, {
60
+ item,
61
+ balance,
62
+ handlers: {
63
+ ...handlers,
64
+ onRequestChanges: requestChanges
65
+ }
66
+ }, item.id))]
67
+ }),
68
+ /* @__PURE__ */ jsx("footer", {
69
+ className: "shrink-0 border-t border-border bg-background p-3",
70
+ children: /* @__PURE__ */ jsx(Composer, {
71
+ inputRef: input,
72
+ onSend: send,
73
+ replying,
74
+ placeholder: revising ? "What should change in the plan?" : "Ask for a change or about your data..."
75
+ })
76
+ })
77
+ ]
78
+ });
79
+ };
80
+ //#endregion
81
+ export { AssistantDock };
@@ -0,0 +1,9 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/assistant-dock/chat-message.tsx
4
+ const ChatMessage = ({ from, text }) => /* @__PURE__ */ jsx("div", {
5
+ className: cn("whitespace-pre-wrap break-words text-[13px]", from === "user" ? "ml-8 self-end rounded-lg rounded-br-sm bg-primary px-3 py-2 text-primary-foreground shadow-sm" : "mr-4 text-foreground"),
6
+ children: text
7
+ });
8
+ //#endregion
9
+ export { ChatMessage };
@@ -0,0 +1,39 @@
1
+ import { Input } from "../primitives/input.js";
2
+ import { Button } from "../primitives/button.js";
3
+ import { useState } from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { SendHorizontal } from "lucide-react";
6
+ //#region src/assistant-dock/composer.tsx
7
+ const Composer = ({ onSend, replying, placeholder, inputRef }) => {
8
+ const [text, setText] = useState("");
9
+ const message = text.trim();
10
+ const send = () => {
11
+ if (!message || replying) return;
12
+ onSend?.(message);
13
+ setText("");
14
+ };
15
+ return /* @__PURE__ */ jsxs("form", {
16
+ className: "flex gap-2",
17
+ onSubmit: (event) => {
18
+ event.preventDefault();
19
+ send();
20
+ },
21
+ children: [/* @__PURE__ */ jsx(Input, {
22
+ ref: inputRef,
23
+ "aria-label": "Message",
24
+ placeholder,
25
+ value: text,
26
+ onChange: (event) => setText(event.target.value),
27
+ wrapperClassName: "flex-1"
28
+ }), /* @__PURE__ */ jsx(Button, {
29
+ type: "submit",
30
+ variant: "primary",
31
+ className: "w-8 px-0",
32
+ "aria-label": "Send",
33
+ disabled: !message || replying,
34
+ children: /* @__PURE__ */ jsx(SendHorizontal, { className: "size-4" })
35
+ })]
36
+ });
37
+ };
38
+ //#endregion
39
+ export { Composer };
@@ -0,0 +1,4 @@
1
+ import { PlanCardProps } from "./plan-card.js";
2
+ //#region src/assistant-dock/dock-item.d.ts
3
+ export type DockItemHandlers = Pick<PlanCardProps, 'onApprove' | 'onRequestChanges' | 'onCancel' | 'onUpdatePlan'>;
4
+ //#endregion
@@ -0,0 +1,37 @@
1
+ import { ChatMessage } from "./chat-message.js";
2
+ import { PlanCard } from "./plan-card.js";
3
+ import { ProgressCard } from "./progress-card.js";
4
+ import { QueryCard } from "./query-card.js";
5
+ import { QueueCard } from "./queue-card.js";
6
+ import { ResultCard } from "./result-card.js";
7
+ import { jsx } from "react/jsx-runtime";
8
+ //#region src/assistant-dock/dock-item.tsx
9
+ const DockItemView = ({ item, balance, handlers }) => {
10
+ switch (item.kind) {
11
+ case "user":
12
+ case "assistant": return /* @__PURE__ */ jsx(ChatMessage, {
13
+ from: item.kind,
14
+ text: item.text
15
+ });
16
+ case "queue": return /* @__PURE__ */ jsx(QueueCard, {
17
+ taskId: item.taskId,
18
+ request: item.request,
19
+ ahead: item.ahead,
20
+ waitingOnPlan: item.waitingOnPlan,
21
+ onCancel: handlers.onCancel
22
+ });
23
+ case "progress": return /* @__PURE__ */ jsx(ProgressCard, {
24
+ phase: item.phase,
25
+ steps: item.steps
26
+ });
27
+ case "plan": return /* @__PURE__ */ jsx(PlanCard, {
28
+ plan: item.plan,
29
+ balance,
30
+ ...handlers
31
+ });
32
+ case "result": return /* @__PURE__ */ jsx(ResultCard, { result: item.result });
33
+ case "query": return /* @__PURE__ */ jsx(QueryCard, { query: item.query });
34
+ }
35
+ };
36
+ //#endregion
37
+ export { DockItemView };
@@ -0,0 +1,84 @@
1
+ import { Step } from "../assistant-panel/content.js";
2
+ //#region src/assistant-dock/model.d.ts
3
+ export type PlanSize = 'S' | 'M' | 'L';
4
+ /** A plan as the planner returned it and central priced it. */
5
+ export type Plan = {
6
+ taskId: string;
7
+ /** 1 for the first plan; "Ask for changes" and "Update plan" make the next one. */
8
+ revision: number;
9
+ summary: string;
10
+ steps: Array<{
11
+ title: string;
12
+ files?: string[];
13
+ }>;
14
+ migrations: Array<{
15
+ description: string;
16
+ destructive: boolean;
17
+ }>;
18
+ risks: string[];
19
+ size: PlanSize;
20
+ /** The quote: what Approve holds. */
21
+ credits: number;
22
+ /** The branch moved on GitHub since the plan was made. */
23
+ outdated: boolean;
24
+ /** Tasks behind this one in the app's queue, waiting until it is answered. */
25
+ waiting: number;
26
+ /** `dropped`: the platform dropped it and refunded the deposit (repository replaced, app archived). */
27
+ status: 'awaiting' | 'approved' | 'canceled' | 'superseded' | 'dropped';
28
+ };
29
+ export type JobResult = {
30
+ taskId: string;
31
+ outcome: 'live' | 'failed' | 'capped';
32
+ summary: string;
33
+ commit?: {
34
+ sha: string;
35
+ url?: string;
36
+ };
37
+ /** The migrations the job applied; they stay live when it fails. */
38
+ dbChanges: string[];
39
+ /** A finished change GitHub refused, kept on `protobase/task-<n>`. */
40
+ keptOnBranch?: string;
41
+ /** What was charged: the quote when live, 0 otherwise. */
42
+ credits: number;
43
+ };
44
+ export type QueryResult = {
45
+ sql: string;
46
+ columns: string[];
47
+ rows: unknown[][];
48
+ truncated: boolean;
49
+ };
50
+ export type DockItem = {
51
+ kind: 'user';
52
+ id: string;
53
+ text: string;
54
+ } | {
55
+ kind: 'assistant';
56
+ id: string;
57
+ text: string;
58
+ } | {
59
+ kind: 'queue';
60
+ id: string;
61
+ taskId: string;
62
+ request: string;
63
+ ahead: number;
64
+ waitingOnPlan: boolean;
65
+ } | {
66
+ kind: 'progress';
67
+ id: string;
68
+ taskId: string;
69
+ phase: 'planning' | 'running';
70
+ steps: Step[];
71
+ } | {
72
+ kind: 'plan';
73
+ id: string;
74
+ plan: Plan;
75
+ } | {
76
+ kind: 'result';
77
+ id: string;
78
+ result: JobResult;
79
+ } | {
80
+ kind: 'query';
81
+ id: string;
82
+ query: QueryResult;
83
+ };
84
+ //#endregion
@@ -0,0 +1,26 @@
1
+ //#region src/assistant-dock/model.ts
2
+ const formatCredits = (credits) => `${credits} ${credits === 1 ? "credit" : "credits"}`;
3
+ /** Approve is offered only on a plan awaiting an answer, and only when the balance covers the quote. */
4
+ const approval = (plan, balance) => {
5
+ if (plan.status !== "awaiting") return { enabled: false };
6
+ if (balance < plan.credits) return {
7
+ enabled: false,
8
+ reason: `Needs ${formatCredits(plan.credits)}; the balance is ${formatCredits(balance)}.`
9
+ };
10
+ return { enabled: true };
11
+ };
12
+ const approveLabel = (plan) => `${plan.outdated ? "Approve anyway" : "Approve"} · ${formatCredits(plan.credits)}`;
13
+ const queueText = (ahead, waitingOnPlan) => {
14
+ if (waitingOnPlan) return "Waiting for the plan above to be approved or canceled.";
15
+ if (ahead === 0) return "Next in line.";
16
+ return `Waiting, ${ahead} ${ahead === 1 ? "task" : "tasks"} ahead.`;
17
+ };
18
+ const waitingText = (waiting) => waiting === 0 ? void 0 : `${waiting} ${waiting === 1 ? "task waits" : "tasks wait"} on this plan.`;
19
+ /** A query cell as text: `null` stays visible, objects and arrays as JSON. */
20
+ const formatCell = (value) => {
21
+ if (value === null || value === void 0) return "null";
22
+ if (typeof value === "object") return JSON.stringify(value);
23
+ return String(value);
24
+ };
25
+ //#endregion
26
+ export { approval, approveLabel, formatCell, formatCredits, queueText, waitingText };
@@ -0,0 +1,13 @@
1
+ import { Plan } from "./model.js";
2
+ //#region src/assistant-dock/plan-card.d.ts
3
+ export type PlanCardProps = {
4
+ plan: Plan;
5
+ balance: number;
6
+ onApprove?: (taskId: string) => void;
7
+ /** Asks for the next revision; the dock moves to the chat box for what should change. */
8
+ onRequestChanges?: (taskId: string) => void;
9
+ onCancel?: (taskId: string) => void;
10
+ /** A free re-plan from the moved branch. */
11
+ onUpdatePlan?: (taskId: string) => void;
12
+ };
13
+ //#endregion