@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,167 @@
1
+ //#region src/mocks/products.ts
2
+ const catalog = [
3
+ [
4
+ "M8 hex bolt A2, 100 pcs",
5
+ "FST",
6
+ 1890
7
+ ],
8
+ [
9
+ "M10 hex nut A2, 100 pcs",
10
+ "FST",
11
+ 1240
12
+ ],
13
+ [
14
+ "Stainless washer 8 mm, 250 pcs",
15
+ "FST",
16
+ 990
17
+ ],
18
+ [
19
+ "Cable tie 300 mm, 100 pcs",
20
+ "ELC",
21
+ 650
22
+ ],
23
+ [
24
+ "Cable gland M20, 25 pcs",
25
+ "ELC",
26
+ 2340
27
+ ],
28
+ [
29
+ "Junction box IP65",
30
+ "ELC",
31
+ 1480
32
+ ],
33
+ [
34
+ "Hydraulic hose 1/2 in, 5 m",
35
+ "HYD",
36
+ 8650
37
+ ],
38
+ [
39
+ "Hydraulic coupling 3/8 in",
40
+ "HYD",
41
+ 1895
42
+ ],
43
+ [
44
+ "Pallet wrap 500 mm",
45
+ "PKG",
46
+ 740
47
+ ],
48
+ [
49
+ "Corrugated box 400 x 300",
50
+ "PKG",
51
+ 215
52
+ ],
53
+ [
54
+ "Bubble wrap roll 50 m",
55
+ "PKG",
56
+ 1990
57
+ ],
58
+ [
59
+ "Safety gloves, size L",
60
+ "PPE",
61
+ 395
62
+ ],
63
+ [
64
+ "Safety glasses, clear",
65
+ "PPE",
66
+ 450
67
+ ],
68
+ [
69
+ "Ear defenders SNR 31",
70
+ "PPE",
71
+ 1295
72
+ ],
73
+ [
74
+ "Ball bearing 6204-2RS",
75
+ "BRG",
76
+ 685
77
+ ],
78
+ [
79
+ "Taper roller bearing 30205",
80
+ "BRG",
81
+ 1420
82
+ ],
83
+ [
84
+ "Timing belt HTD 5M, 600 mm",
85
+ "BLT",
86
+ 2780
87
+ ],
88
+ [
89
+ "V-belt SPA 1250",
90
+ "BLT",
91
+ 1360
92
+ ],
93
+ [
94
+ "Grease cartridge, 400 g",
95
+ "LUB",
96
+ 920
97
+ ],
98
+ [
99
+ "Machine oil ISO 68, 5 l",
100
+ "LUB",
101
+ 3950
102
+ ],
103
+ [
104
+ "Angle grinder disc 125 mm, 10 pcs",
105
+ "ABR",
106
+ 1880
107
+ ],
108
+ [
109
+ "Drill bit set HSS, 19 pcs",
110
+ "TLS",
111
+ 3490
112
+ ],
113
+ [
114
+ "Torque wrench 20-100 Nm",
115
+ "TLS",
116
+ 14900
117
+ ],
118
+ [
119
+ "Pneumatic cylinder 40 x 100",
120
+ "PNE",
121
+ 6120
122
+ ],
123
+ [
124
+ "Air hose PU 8 mm, 20 m",
125
+ "PNE",
126
+ 2475
127
+ ],
128
+ [
129
+ "LED panel 600 x 600, 40 W",
130
+ "LGT",
131
+ 3290
132
+ ],
133
+ [
134
+ "Pallet label roll 100 x 150",
135
+ "LBL",
136
+ 1175
137
+ ],
138
+ [
139
+ "Thermal printer ribbon",
140
+ "LBL",
141
+ 890
142
+ ],
143
+ [
144
+ "Euro pallet EPAL, used",
145
+ "PLT",
146
+ 1150
147
+ ],
148
+ [
149
+ "Roll container, galvanised",
150
+ "PLT",
151
+ 9800
152
+ ]
153
+ ];
154
+ catalog.length;
155
+ const productAt = (index) => {
156
+ const [name, category, priceCents] = catalog[index % catalog.length];
157
+ const slot = index % catalog.length;
158
+ return {
159
+ id: `P-${200 + slot}`,
160
+ sku: `${category}-${String(1e3 + slot * 7).padStart(4, "0")}`,
161
+ name,
162
+ category,
163
+ priceCents
164
+ };
165
+ };
166
+ //#endregion
167
+ export { productAt };
@@ -0,0 +1,20 @@
1
+ //#region src/mocks/rng.ts
2
+ const mockNow = Date.UTC(2026, 9, 6, 9, 41);
3
+ const hash = (seed, index) => {
4
+ let h = (seed ^ Math.imul(index + 2654435769, 2246822507)) >>> 0;
5
+ h = Math.imul(h ^ h >>> 16, 2246822507);
6
+ h = Math.imul(h ^ h >>> 13, 3266489909);
7
+ return (h ^ h >>> 16) >>> 0;
8
+ };
9
+ const rngAt = (seed, index) => {
10
+ let state = hash(seed, index);
11
+ return () => {
12
+ state = state + 1831565813 >>> 0;
13
+ let t = state;
14
+ t = Math.imul(t ^ t >>> 15, t | 1);
15
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
16
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
17
+ };
18
+ };
19
+ //#endregion
20
+ export { hash, mockNow, rngAt };
@@ -0,0 +1,63 @@
1
+ //#region src/mocks/users.ts
2
+ const users = [
3
+ {
4
+ id: "u1",
5
+ name: "Sanne M.",
6
+ initials: "SM",
7
+ hue: 250,
8
+ role: "Admin"
9
+ },
10
+ {
11
+ id: "u2",
12
+ name: "Jonas K.",
13
+ initials: "JK",
14
+ hue: 160,
15
+ role: "Sales"
16
+ },
17
+ {
18
+ id: "u3",
19
+ name: "Lieke D.",
20
+ initials: "LD",
21
+ hue: 20,
22
+ role: "Finance"
23
+ },
24
+ {
25
+ id: "u4",
26
+ name: "Tobias R.",
27
+ initials: "TR",
28
+ hue: 310,
29
+ role: "Warehouse"
30
+ },
31
+ {
32
+ id: "u5",
33
+ name: "Imke V.",
34
+ initials: "IV",
35
+ hue: 200,
36
+ role: "Sales"
37
+ },
38
+ {
39
+ id: "u6",
40
+ name: "Marek W.",
41
+ initials: "MW",
42
+ hue: 80,
43
+ role: "Warehouse"
44
+ },
45
+ {
46
+ id: "u7",
47
+ name: "Hanna B.",
48
+ initials: "HB",
49
+ hue: 350,
50
+ role: "Finance"
51
+ },
52
+ {
53
+ id: "u8",
54
+ name: "Daan P.",
55
+ initials: "DP",
56
+ hue: 120,
57
+ role: "Sales"
58
+ }
59
+ ];
60
+ const userById = (id) => users.find((user) => user.id === id) ?? users[0];
61
+ users.filter((user) => user.role === "Sales");
62
+ //#endregion
63
+ export { userById, users };
@@ -0,0 +1,22 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/primitives/avatar.tsx
4
+ const sizes = {
5
+ xs: "size-5 text-[9px]",
6
+ sm: "size-6 text-[10px]",
7
+ md: "size-8 text-xs"
8
+ };
9
+ const Avatar = ({ initials, hue, size = "sm", className }) => /* @__PURE__ */ jsx("span", {
10
+ className: cn("inline-flex shrink-0 select-none items-center justify-center rounded-full font-semibold text-white ring-2 ring-background", sizes[size], className),
11
+ style: { background: `oklch(0.58 0.13 ${hue})` },
12
+ children: initials
13
+ });
14
+ const AvatarStack = ({ people, size = "sm" }) => /* @__PURE__ */ jsx("span", {
15
+ className: "inline-flex -space-x-1",
16
+ children: people.map((person) => /* @__PURE__ */ jsx(Avatar, {
17
+ ...person,
18
+ size
19
+ }, person.initials))
20
+ });
21
+ //#endregion
22
+ export { Avatar, AvatarStack };
@@ -0,0 +1 @@
1
+ import { ReactNode } from "react";
@@ -0,0 +1,25 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region src/primitives/badge.tsx
4
+ const tones = {
5
+ neutral: "bg-muted text-muted-foreground",
6
+ blue: "bg-primary-soft text-primary-text",
7
+ green: "bg-success-soft text-success-text",
8
+ amber: "bg-warning-soft text-warning-text",
9
+ red: "bg-danger-soft text-danger-text",
10
+ violet: "bg-violet-soft text-violet-text"
11
+ };
12
+ const dots = {
13
+ neutral: "bg-faint-foreground",
14
+ blue: "bg-primary",
15
+ green: "bg-success",
16
+ amber: "bg-warning",
17
+ red: "bg-danger",
18
+ violet: "bg-violet"
19
+ };
20
+ const Badge = ({ tone = "neutral", dot = true, children, className }) => /* @__PURE__ */ jsxs("span", {
21
+ className: cn("inline-flex h-5 items-center gap-1.5 rounded-full px-2 text-xs font-medium whitespace-nowrap", tones[tone], className),
22
+ children: [dot && /* @__PURE__ */ jsx("span", { className: cn("size-1.5 rounded-full", dots[tone]) }), children]
23
+ });
24
+ //#endregion
25
+ export { Badge };
@@ -0,0 +1,23 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { Spinner } from "./spinner.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/primitives/button.tsx
5
+ const variants = {
6
+ primary: "bg-primary text-primary-foreground hover:bg-primary-hover shadow-sm",
7
+ secondary: "bg-background text-foreground border border-border-strong hover:bg-muted shadow-sm",
8
+ ghost: "text-muted-foreground hover:bg-muted hover:text-foreground",
9
+ danger: "bg-background text-danger-text border border-border-strong hover:bg-danger-soft shadow-sm"
10
+ };
11
+ const sizes = {
12
+ sm: "h-7 px-2.5 text-xs gap-1.5",
13
+ md: "h-8 px-3 text-[13px] gap-2"
14
+ };
15
+ const Button = ({ variant = "secondary", size = "md", loading, className, children, disabled, ...rest }) => /* @__PURE__ */ jsxs("button", {
16
+ type: "button",
17
+ disabled: disabled || loading,
18
+ className: cn("inline-flex shrink-0 items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:opacity-60 disabled:cursor-default", variants[variant], sizes[size], className),
19
+ ...rest,
20
+ children: [loading && /* @__PURE__ */ jsx(Spinner, { className: "size-3.5" }), children]
21
+ });
22
+ //#endregion
23
+ export { Button };
@@ -0,0 +1,21 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { Check, Minus } from "lucide-react";
4
+ //#region src/primitives/checkbox.tsx
5
+ const Checkbox = ({ checked, indeterminate, onChange, label, className }) => /* @__PURE__ */ jsx("button", {
6
+ type: "button",
7
+ role: "checkbox",
8
+ "aria-checked": indeterminate ? "mixed" : checked,
9
+ "aria-label": label,
10
+ onClick: () => onChange?.(!checked),
11
+ className: cn("relative inline-flex size-4 shrink-0 items-center justify-center rounded border transition-colors after:absolute after:-inset-1 after:content-[\"\"] pointer-coarse:after:-inset-3.5", checked || indeterminate ? "border-primary bg-primary text-primary-foreground" : "border-border-strong bg-background hover:border-faint-foreground", className),
12
+ children: indeterminate ? /* @__PURE__ */ jsx(Minus, {
13
+ className: "size-3",
14
+ strokeWidth: 3
15
+ }) : checked && /* @__PURE__ */ jsx(Check, {
16
+ className: "size-3",
17
+ strokeWidth: 3
18
+ })
19
+ });
20
+ //#endregion
21
+ export { Checkbox };
@@ -0,0 +1,41 @@
1
+ import { useFocusTrap } from "../lib/use-focus-trap.js";
2
+ import { useRef } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/primitives/dialog.tsx
5
+ /** A modal with a title, optional body and action buttons. Closes on Escape and on a click outside. */
6
+ const Dialog = ({ title, description, onClose, actions, children }) => {
7
+ const panel = useRef(null);
8
+ useFocusTrap(panel, true, onClose);
9
+ return /* @__PURE__ */ jsx("div", {
10
+ className: "fixed inset-0 z-[90] flex items-center justify-center bg-foreground/30 p-4",
11
+ onMouseDown: (event) => event.target === event.currentTarget && onClose(),
12
+ children: /* @__PURE__ */ jsxs("div", {
13
+ ref: panel,
14
+ tabIndex: -1,
15
+ role: "dialog",
16
+ "aria-modal": "true",
17
+ "aria-label": title,
18
+ className: "outline-none max-h-[90vh] w-full max-w-md overflow-y-auto rounded-lg border bg-background p-4 shadow-pop sm:p-5",
19
+ children: [
20
+ /* @__PURE__ */ jsx("h2", {
21
+ className: "text-[15px] font-semibold",
22
+ children: title
23
+ }),
24
+ description && /* @__PURE__ */ jsx("p", {
25
+ className: "mt-1.5 text-[13px] text-muted-foreground",
26
+ children: description
27
+ }),
28
+ children && /* @__PURE__ */ jsx("div", {
29
+ className: "mt-4",
30
+ children
31
+ }),
32
+ /* @__PURE__ */ jsx("div", {
33
+ className: "mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end [&>button]:min-h-11 sm:[&>button]:min-h-0",
34
+ children: actions
35
+ })
36
+ ]
37
+ })
38
+ });
39
+ };
40
+ //#endregion
41
+ export { Dialog };
@@ -0,0 +1,16 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region src/primitives/input.tsx
4
+ const Input = ({ leading, trailing, invalid, className, wrapperClassName, ...rest }) => /* @__PURE__ */ jsxs("span", {
5
+ className: cn("flex h-8 items-center gap-2 rounded-md border bg-background px-2.5 text-muted-foreground shadow-sm focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20", invalid ? "border-danger" : "border-border-strong", wrapperClassName),
6
+ children: [
7
+ leading,
8
+ /* @__PURE__ */ jsx("input", {
9
+ className: cn("min-w-0 flex-1 bg-transparent text-[13px] text-foreground outline-none placeholder:text-faint-foreground", className),
10
+ ...rest
11
+ }),
12
+ trailing
13
+ ]
14
+ });
15
+ //#endregion
16
+ export { Input };
@@ -0,0 +1,8 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ //#region src/primitives/kbd.tsx
3
+ const Kbd = ({ children }) => /* @__PURE__ */ jsx("kbd", {
4
+ className: "inline-flex h-5 items-center rounded border border-border-strong bg-background px-1.5 font-sans text-[11px] font-medium text-muted-foreground",
5
+ children
6
+ });
7
+ //#endregion
8
+ export { Kbd };
@@ -0,0 +1,14 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/primitives/segmented.tsx
4
+ const Segmented = ({ value, options, onChange }) => /* @__PURE__ */ jsx("span", {
5
+ className: "inline-flex h-7 rounded-md bg-muted p-0.5",
6
+ children: options.map((option) => /* @__PURE__ */ jsx("button", {
7
+ type: "button",
8
+ onClick: () => onChange(option.value),
9
+ className: cn("rounded px-2.5 text-xs font-medium transition-colors", option.value === value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"),
10
+ children: option.label
11
+ }, option.value))
12
+ });
13
+ //#endregion
14
+ export { Segmented };
@@ -0,0 +1,44 @@
1
+ import { useFocusTrap } from "../lib/use-focus-trap.js";
2
+ import { useRef } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { X } from "lucide-react";
5
+ //#region src/primitives/sheet.tsx
6
+ /** A panel that rises from the bottom of the screen, for the filter panel on phones. */
7
+ const Sheet = ({ title, open, onClose, children }) => {
8
+ const panel = useRef(null);
9
+ useFocusTrap(panel, open, onClose);
10
+ if (!open) return null;
11
+ return /* @__PURE__ */ jsxs("div", {
12
+ className: "fixed inset-0 z-[80]",
13
+ children: [/* @__PURE__ */ jsx("div", {
14
+ className: "absolute inset-0 bg-foreground/40",
15
+ onClick: onClose,
16
+ "aria-hidden": true
17
+ }), /* @__PURE__ */ jsxs("div", {
18
+ ref: panel,
19
+ role: "dialog",
20
+ "aria-modal": "true",
21
+ "aria-label": title,
22
+ tabIndex: -1,
23
+ className: "absolute inset-x-0 bottom-0 flex max-h-[85vh] flex-col rounded-t-xl border-t bg-background shadow-pop outline-none",
24
+ children: [/* @__PURE__ */ jsxs("header", {
25
+ className: "flex shrink-0 items-center justify-between border-b px-4",
26
+ children: [/* @__PURE__ */ jsx("h2", {
27
+ className: "text-[15px] font-semibold",
28
+ children: title
29
+ }), /* @__PURE__ */ jsx("button", {
30
+ type: "button",
31
+ "aria-label": "Close",
32
+ onClick: onClose,
33
+ className: "-mr-2 inline-flex size-11 items-center justify-center rounded-md text-muted-foreground hover:bg-muted",
34
+ children: /* @__PURE__ */ jsx(X, { className: "size-5" })
35
+ })]
36
+ }), /* @__PURE__ */ jsx("div", {
37
+ className: "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pb-6",
38
+ children
39
+ })]
40
+ })]
41
+ });
42
+ };
43
+ //#endregion
44
+ export { Sheet };
@@ -0,0 +1,6 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/primitives/skeleton.tsx
4
+ const Skeleton = ({ className }) => /* @__PURE__ */ jsx("span", { className: cn("block h-3 rounded bg-muted [animation:pb-shimmer_1.4s_ease-in-out_infinite]", className) });
5
+ //#endregion
6
+ export { Skeleton };
@@ -0,0 +1,10 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { Loader2 } from "lucide-react";
4
+ //#region src/primitives/spinner.tsx
5
+ const Spinner = ({ className }) => /* @__PURE__ */ jsx(Loader2, {
6
+ "aria-label": "Loading",
7
+ className: cn("size-4 animate-spin", className)
8
+ });
9
+ //#endregion
10
+ export { Spinner };
@@ -0,0 +1,14 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/primitives/switch.tsx
4
+ const Switch = ({ checked, onChange, label }) => /* @__PURE__ */ jsx("button", {
5
+ type: "button",
6
+ role: "switch",
7
+ "aria-checked": checked,
8
+ "aria-label": label,
9
+ onClick: () => onChange?.(!checked),
10
+ className: cn("relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors", checked ? "bg-primary" : "bg-border-strong"),
11
+ children: /* @__PURE__ */ jsx("span", { className: cn("size-4 rounded-full bg-white shadow transition-transform", checked ? "translate-x-[18px]" : "translate-x-0.5") })
12
+ });
13
+ //#endregion
14
+ export { Switch };
@@ -0,0 +1,13 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region src/primitives/tooltip.tsx
4
+ const Tooltip = ({ label, side = "right", visible, children }) => /* @__PURE__ */ jsxs("span", {
5
+ className: "group/tooltip relative inline-flex",
6
+ children: [children, /* @__PURE__ */ jsx("span", {
7
+ role: "tooltip",
8
+ className: cn("pointer-events-none absolute z-50 whitespace-nowrap rounded-md bg-foreground px-2 py-1 text-xs font-medium text-background opacity-0 shadow-md transition-opacity group-hover/tooltip:opacity-100", visible && "opacity-100", side === "right" && "left-full top-1/2 ml-2 -translate-y-1/2", side === "top" && "bottom-full left-1/2 mb-2 -translate-x-1/2", side === "bottom" && "left-1/2 top-full mt-2 -translate-x-1/2"),
9
+ children: label
10
+ })]
11
+ });
12
+ //#endregion
13
+ export { Tooltip };
@@ -0,0 +1,28 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ //#region src/record-view/action-rail.tsx
3
+ /** Named actions with one-line descriptions; on phones a row of buttons, from `md` a list. */
4
+ const ActionRail = ({ actions }) => /* @__PURE__ */ jsxs("div", {
5
+ className: "overflow-hidden rounded-lg border border-border bg-background",
6
+ children: [/* @__PURE__ */ jsx("h3", {
7
+ className: "hidden border-b border-border px-4 py-2.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground md:block",
8
+ children: "Actions"
9
+ }), /* @__PURE__ */ jsx("ul", {
10
+ className: "flex flex-wrap gap-2 p-2 md:block md:divide-y md:divide-border md:p-0",
11
+ children: actions.map(({ name, description, icon: Icon, onRun, blocked, running }) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
12
+ type: "button",
13
+ onClick: onRun,
14
+ disabled: Boolean(blocked) || running,
15
+ title: blocked,
16
+ className: "flex min-h-11 items-center gap-2 rounded-md border border-border-strong px-3 text-left hover:bg-surface disabled:cursor-default disabled:opacity-60 md:min-h-0 md:w-full md:items-start md:gap-3 md:rounded-none md:border-0 md:px-4 md:py-3",
17
+ children: [/* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0 text-muted-foreground md:mt-0.5" }), /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("span", {
18
+ className: "block text-[13px] font-medium text-foreground",
19
+ children: name
20
+ }), description && /* @__PURE__ */ jsx("span", {
21
+ className: "mt-0.5 hidden text-xs leading-snug text-muted-foreground md:block",
22
+ children: description
23
+ })] })]
24
+ }) }, name))
25
+ })]
26
+ });
27
+ //#endregion
28
+ export { ActionRail };
@@ -0,0 +1,62 @@
1
+ import { Avatar } from "../primitives/avatar.js";
2
+ import { Button } from "../primitives/button.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { TriangleAlert } from "lucide-react";
5
+ //#region src/record-view/field.tsx
6
+ const Field = ({ label, required, help, example, error, conflict, children }) => /* @__PURE__ */ jsxs("div", { children: [
7
+ /* @__PURE__ */ jsxs("div", {
8
+ className: "mb-1.5 flex items-center gap-2",
9
+ children: [/* @__PURE__ */ jsxs("label", {
10
+ className: "text-[13px] font-medium text-foreground",
11
+ children: [label, required && /* @__PURE__ */ jsx("span", {
12
+ className: "ml-0.5 text-danger-text",
13
+ "aria-label": "required",
14
+ children: "*"
15
+ })]
16
+ }), conflict && /* @__PURE__ */ jsxs("span", {
17
+ className: "inline-flex items-center gap-1.5 text-warning-text",
18
+ title: `${conflict.user.name} saved a different value`,
19
+ children: [/* @__PURE__ */ jsx(TriangleAlert, { className: "size-3.5" }), /* @__PURE__ */ jsx(Avatar, {
20
+ initials: conflict.user.initials,
21
+ hue: conflict.user.hue,
22
+ size: "xs"
23
+ })]
24
+ })]
25
+ }),
26
+ children,
27
+ help && /* @__PURE__ */ jsx("p", {
28
+ className: "mt-1.5 text-xs text-muted-foreground",
29
+ children: help
30
+ }),
31
+ example && /* @__PURE__ */ jsxs("p", {
32
+ className: "mt-0.5 text-xs text-faint-foreground",
33
+ children: ["Example: ", example]
34
+ }),
35
+ error && /* @__PURE__ */ jsx("p", {
36
+ className: "mt-1.5 text-xs font-medium text-danger-text",
37
+ children: error
38
+ }),
39
+ conflict && /* @__PURE__ */ jsxs("div", {
40
+ className: "mt-2 flex items-center justify-between gap-3 rounded-md border border-warning/30 bg-warning-soft px-3 py-2 text-xs text-warning-text",
41
+ children: [/* @__PURE__ */ jsxs("span", { children: [
42
+ /* @__PURE__ */ jsx("strong", { children: conflict.user.name }),
43
+ " saved this ",
44
+ conflict.ago,
45
+ ": ",
46
+ /* @__PURE__ */ jsx("strong", { children: conflict.theirValue })
47
+ ] }), /* @__PURE__ */ jsxs("span", {
48
+ className: "flex gap-1.5",
49
+ children: [/* @__PURE__ */ jsx(Button, {
50
+ size: "sm",
51
+ onClick: conflict.onKeepMine,
52
+ children: "Keep mine"
53
+ }), /* @__PURE__ */ jsx(Button, {
54
+ size: "sm",
55
+ onClick: conflict.onUseTheirs,
56
+ children: "Use theirs"
57
+ })]
58
+ })]
59
+ })
60
+ ] });
61
+ //#endregion
62
+ export { Field };
@@ -0,0 +1,50 @@
1
+ import { AvatarStack } from "../primitives/avatar.js";
2
+ import { Badge } from "../primitives/badge.js";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/record-view/record-header.tsx
5
+ const RecordHeader = ({ collection, title, status, lastEditedBy, presence = [], actions }) => /* @__PURE__ */ jsxs("header", {
6
+ className: "flex flex-col gap-4 border-b border-border pb-6 md:flex-row md:items-end md:justify-between md:gap-6",
7
+ children: [/* @__PURE__ */ jsxs("div", { children: [
8
+ /* @__PURE__ */ jsx("p", {
9
+ className: "text-xs text-muted-foreground",
10
+ children: collection
11
+ }),
12
+ /* @__PURE__ */ jsxs("div", {
13
+ className: "mt-1 flex flex-wrap items-center gap-x-3 gap-y-1",
14
+ children: [/* @__PURE__ */ jsx("h1", {
15
+ className: "text-xl font-semibold tracking-tight text-foreground md:text-2xl",
16
+ children: title
17
+ }), /* @__PURE__ */ jsx(Badge, {
18
+ tone: status.tone,
19
+ children: status.label
20
+ })]
21
+ }),
22
+ /* @__PURE__ */ jsx("p", {
23
+ className: "mt-1.5 text-xs text-muted-foreground",
24
+ children: lastEditedBy.name ? /* @__PURE__ */ jsxs(Fragment, { children: [
25
+ "Last edited by ",
26
+ /* @__PURE__ */ jsx("span", {
27
+ className: "font-medium text-foreground",
28
+ children: lastEditedBy.name
29
+ }),
30
+ " ",
31
+ lastEditedBy.ago
32
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: ["Last updated ", lastEditedBy.ago] })
33
+ })
34
+ ] }), /* @__PURE__ */ jsxs("div", {
35
+ className: "flex items-center gap-4",
36
+ children: [presence.length > 0 && /* @__PURE__ */ jsxs("span", {
37
+ className: "flex items-center gap-2 text-xs text-muted-foreground",
38
+ children: [
39
+ /* @__PURE__ */ jsx(AvatarStack, {
40
+ people: presence,
41
+ size: "md"
42
+ }),
43
+ presence.length,
44
+ " viewing"
45
+ ]
46
+ }), actions]
47
+ })]
48
+ });
49
+ //#endregion
50
+ export { RecordHeader };