@spacesync/client 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 (100) hide show
  1. package/README.md +39 -0
  2. package/dist/analog-clock-picker.d.ts +10 -0
  3. package/dist/analog-clock-picker.js +408 -0
  4. package/dist/analog-clock-picker.js.map +1 -0
  5. package/dist/api.d.ts +222 -0
  6. package/dist/api.js +167 -0
  7. package/dist/api.js.map +1 -0
  8. package/dist/app-modal.d.ts +25 -0
  9. package/dist/app-modal.js +145 -0
  10. package/dist/app-modal.js.map +1 -0
  11. package/dist/app-shell-mobile-nav.d.ts +29 -0
  12. package/dist/app-shell-mobile-nav.js +122 -0
  13. package/dist/app-shell-mobile-nav.js.map +1 -0
  14. package/dist/app-web-shell.d.ts +20 -0
  15. package/dist/app-web-shell.js +45 -0
  16. package/dist/app-web-shell.js.map +1 -0
  17. package/dist/booking-schedule-calendars.d.ts +95 -0
  18. package/dist/booking-schedule-calendars.js +1343 -0
  19. package/dist/booking-schedule-calendars.js.map +1 -0
  20. package/dist/chat-stick-to-bottom.d.ts +22 -0
  21. package/dist/chat-stick-to-bottom.js +190 -0
  22. package/dist/chat-stick-to-bottom.js.map +1 -0
  23. package/dist/detail-page-header.d.ts +16 -0
  24. package/dist/detail-page-header.js +220 -0
  25. package/dist/detail-page-header.js.map +1 -0
  26. package/dist/dialog-CIdvylPK.d.ts +16 -0
  27. package/dist/errors-D1niseaa.d.ts +34 -0
  28. package/dist/file-picker-field.d.ts +23 -0
  29. package/dist/file-picker-field.js +165 -0
  30. package/dist/file-picker-field.js.map +1 -0
  31. package/dist/file-viewer-dialog.d.ts +15 -0
  32. package/dist/file-viewer-dialog.js +387 -0
  33. package/dist/file-viewer-dialog.js.map +1 -0
  34. package/dist/floor-switcher.d.ts +14 -0
  35. package/dist/floor-switcher.js +416 -0
  36. package/dist/floor-switcher.js.map +1 -0
  37. package/dist/forms.d.ts +52 -0
  38. package/dist/forms.js +66 -0
  39. package/dist/forms.js.map +1 -0
  40. package/dist/image-picker-field.d.ts +11 -0
  41. package/dist/image-picker-field.js +135 -0
  42. package/dist/image-picker-field.js.map +1 -0
  43. package/dist/layout-canvas-view.d.ts +57 -0
  44. package/dist/layout-canvas-view.js +974 -0
  45. package/dist/layout-canvas-view.js.map +1 -0
  46. package/dist/lib/dateDisplay.d.ts +24 -0
  47. package/dist/lib/dateDisplay.js +84 -0
  48. package/dist/lib/dateDisplay.js.map +1 -0
  49. package/dist/live-resource-meta.d.ts +11 -0
  50. package/dist/live-resource-meta.js +27 -0
  51. package/dist/live-resource-meta.js.map +1 -0
  52. package/dist/live-space-pulse-dot.d.ts +8 -0
  53. package/dist/live-space-pulse-dot.js +18 -0
  54. package/dist/live-space-pulse-dot.js.map +1 -0
  55. package/dist/notification-sidebar-preview.d.ts +37 -0
  56. package/dist/notification-sidebar-preview.js +121 -0
  57. package/dist/notification-sidebar-preview.js.map +1 -0
  58. package/dist/page-list-chrome.d.ts +124 -0
  59. package/dist/page-list-chrome.js +970 -0
  60. package/dist/page-list-chrome.js.map +1 -0
  61. package/dist/platform-support-message-composer.d.ts +38 -0
  62. package/dist/platform-support-message-composer.js +446 -0
  63. package/dist/platform-support-message-composer.js.map +1 -0
  64. package/dist/shell-sidebar-nav.d.ts +16 -0
  65. package/dist/shell-sidebar-nav.js +69 -0
  66. package/dist/shell-sidebar-nav.js.map +1 -0
  67. package/dist/shell-theme.d.ts +6 -0
  68. package/dist/shell-theme.js +9 -0
  69. package/dist/shell-theme.js.map +1 -0
  70. package/dist/shell-top-bar-chrome.d.ts +39 -0
  71. package/dist/shell-top-bar-chrome.js +105 -0
  72. package/dist/shell-top-bar-chrome.js.map +1 -0
  73. package/dist/shell-top-bar.d.ts +16 -0
  74. package/dist/shell-top-bar.js +71 -0
  75. package/dist/shell-top-bar.js.map +1 -0
  76. package/dist/styles/theme.css +223 -0
  77. package/dist/styles/theme.css.map +1 -0
  78. package/dist/styles/theme.d.ts +2 -0
  79. package/dist/support-presence-status.d.ts +11 -0
  80. package/dist/support-presence-status.js +92 -0
  81. package/dist/support-presence-status.js.map +1 -0
  82. package/dist/theme-appearance-control.d.ts +5 -0
  83. package/dist/theme-appearance-control.js +114 -0
  84. package/dist/theme-appearance-control.js.map +1 -0
  85. package/dist/theme-boot-script.d.ts +4 -0
  86. package/dist/theme-boot-script.js +9 -0
  87. package/dist/theme-boot-script.js.map +1 -0
  88. package/dist/theme-provider.d.ts +9 -0
  89. package/dist/theme-provider.js +25 -0
  90. package/dist/theme-provider.js.map +1 -0
  91. package/dist/ui.d.ts +765 -0
  92. package/dist/ui.js +5637 -0
  93. package/dist/ui.js.map +1 -0
  94. package/dist/use-client-pagination.d.ts +22 -0
  95. package/dist/use-client-pagination.js +51 -0
  96. package/dist/use-client-pagination.js.map +1 -0
  97. package/dist/use-client-sort.d.ts +20 -0
  98. package/dist/use-client-sort.js +41 -0
  99. package/dist/use-client-sort.js.map +1 -0
  100. package/package.json +208 -0
@@ -0,0 +1,416 @@
1
+ import { Layers, ChevronDown, Check, Plus, XIcon } from 'lucide-react';
2
+ import { clsx } from 'clsx';
3
+ import { twMerge } from 'tailwind-merge';
4
+ import * as React from 'react';
5
+ import { useState, useEffect } from 'react';
6
+ import { Slot } from '@radix-ui/react-slot';
7
+ import { cva } from 'class-variance-authority';
8
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
+ import * as LabelPrimitive from '@radix-ui/react-label';
10
+ import * as DialogPrimitive from '@radix-ui/react-dialog';
11
+ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
12
+
13
+ // src/live-space/floor-switcher.tsx
14
+ function cn(...inputs) {
15
+ return twMerge(clsx(inputs));
16
+ }
17
+
18
+ // src/shell/shell-top-bar-chrome.ts
19
+ var shellTopBarMutedIconClass = "text-sidebar-foreground/60";
20
+ var shellTopBarActionSize = cn(
21
+ "inline-flex !h-7 min-h-0 items-center justify-center gap-1 px-2.5 text-xs font-medium",
22
+ "[&_svg:not([class*='size-'])]:size-3.5"
23
+ );
24
+ var shellTopBarRadius = "rounded-md";
25
+ var shellTopBarNeutralRest = "border border-sidebar-border bg-sidebar-accent text-sidebar-foreground shadow-none transition-colors";
26
+ var shellTopBarNeutralHover = "hover:!border-sidebar-foreground/14 hover:!bg-sidebar-foreground/6 hover:!text-sidebar-foreground";
27
+ var shellTopBarNeutralActive = "active:!border-sidebar-foreground/10 active:!bg-sidebar-foreground/4 active:!text-sidebar-foreground";
28
+ cn(
29
+ "inline-flex !h-6 !w-6 shrink-0 items-center justify-center text-sidebar-foreground/70",
30
+ shellTopBarRadius,
31
+ shellTopBarNeutralRest,
32
+ shellTopBarNeutralHover,
33
+ shellTopBarNeutralActive
34
+ );
35
+ cn(
36
+ "inline-flex !h-7 !w-7 shrink-0 items-center justify-center text-sidebar-foreground/70",
37
+ shellTopBarRadius,
38
+ shellTopBarNeutralRest,
39
+ shellTopBarNeutralHover,
40
+ shellTopBarNeutralActive,
41
+ "[&_svg:not([class*='size-'])]:size-3.5"
42
+ );
43
+ var shellTopBarSecondaryButtonClass = cn(
44
+ shellTopBarActionSize,
45
+ shellTopBarRadius,
46
+ "!border-sidebar-border !bg-sidebar-accent !text-sidebar-foreground shadow-none transition-colors",
47
+ "!py-0",
48
+ shellTopBarNeutralHover,
49
+ shellTopBarNeutralActive,
50
+ "disabled:pointer-events-none disabled:!border-sidebar-border/60 disabled:!bg-sidebar-accent/60 disabled:!text-sidebar-foreground/40"
51
+ );
52
+ cn(
53
+ shellTopBarActionSize,
54
+ shellTopBarRadius,
55
+ "bg-blue-500 text-white transition-colors hover:bg-blue-600",
56
+ "disabled:pointer-events-none disabled:opacity-50"
57
+ );
58
+ cn(
59
+ shellTopBarActionSize,
60
+ shellTopBarRadius,
61
+ "bg-emerald-600 text-white transition-colors hover:bg-emerald-700"
62
+ );
63
+ cn(
64
+ shellTopBarActionSize,
65
+ shellTopBarRadius,
66
+ "border border-emerald-200 bg-emerald-50 text-emerald-700 transition-colors hover:border-emerald-300 hover:bg-emerald-100"
67
+ );
68
+ cn(
69
+ shellTopBarActionSize,
70
+ shellTopBarRadius,
71
+ "border border-blue-200 bg-blue-50 text-blue-700 transition-colors hover:border-blue-300 hover:bg-blue-100"
72
+ );
73
+ cn(
74
+ "flex items-center gap-0.5 border border-sidebar-border bg-sidebar-accent p-0.5",
75
+ shellTopBarRadius
76
+ );
77
+ cn(
78
+ "flex h-7 items-center gap-1.5 border border-sidebar-border bg-sidebar-accent pl-2.5 pr-1 text-sidebar-foreground",
79
+ shellTopBarRadius
80
+ );
81
+ cn(
82
+ "inline-flex !h-6 !w-6 items-center justify-center text-sidebar-foreground/70",
83
+ shellTopBarRadius,
84
+ shellTopBarNeutralRest,
85
+ shellTopBarNeutralHover,
86
+ shellTopBarNeutralActive,
87
+ "[&_svg:not([class*='size-'])]:size-3.5"
88
+ );
89
+ cn(
90
+ shellTopBarActionSize,
91
+ "min-w-[72px]",
92
+ shellTopBarNeutralRest,
93
+ shellTopBarNeutralHover,
94
+ shellTopBarNeutralActive
95
+ );
96
+ var buttonVariants = cva(
97
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium cursor-pointer transition-all disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
98
+ {
99
+ variants: {
100
+ variant: {
101
+ default: "bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80 active:scale-[0.98]",
102
+ destructive: "bg-destructive text-white hover:bg-destructive/90 active:bg-destructive/80 active:scale-[0.98] focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 dark:active:bg-destructive/70",
103
+ outline: "border bg-background text-foreground hover:bg-accent hover:text-accent-foreground active:bg-accent/80 active:scale-[0.98] dark:bg-input/30 dark:border-input dark:hover:bg-input/50 dark:active:bg-input/60",
104
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 active:bg-secondary/70 active:scale-[0.98]",
105
+ ghost: "hover:bg-accent hover:text-accent-foreground active:bg-accent/90 active:scale-[0.98] dark:hover:bg-accent/50 dark:active:bg-accent/40",
106
+ link: "text-primary underline-offset-4 hover:underline active:text-primary/80"
107
+ },
108
+ size: {
109
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
110
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
111
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
112
+ icon: "size-9 rounded-md"
113
+ }
114
+ },
115
+ defaultVariants: {
116
+ variant: "default",
117
+ size: "default"
118
+ }
119
+ }
120
+ );
121
+ var Button = React.forwardRef(function Button2({ className, variant, size, asChild = false, ...props }, ref) {
122
+ const Comp = asChild ? Slot : "button";
123
+ return /* @__PURE__ */ jsx(
124
+ Comp,
125
+ {
126
+ ref,
127
+ "data-slot": "button",
128
+ className: cn(buttonVariants({ variant, size }), className),
129
+ ...props
130
+ }
131
+ );
132
+ });
133
+ Button.displayName = "Button";
134
+ function Label({ className, ...props }) {
135
+ return /* @__PURE__ */ jsx(
136
+ LabelPrimitive.Root,
137
+ {
138
+ "data-slot": "label",
139
+ className: cn(
140
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
141
+ className
142
+ ),
143
+ ...props
144
+ }
145
+ );
146
+ }
147
+ var inputVariants = cva(
148
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-border bg-card text-foreground flex w-full min-w-0 rounded-md border px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:border-blue-400 focus-visible:ring-[3px] focus-visible:ring-blue-500/25 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-red-400 aria-invalid:ring-red-500/20 md:text-sm",
149
+ {
150
+ variants: {
151
+ size: {
152
+ sm: "h-8 text-sm",
153
+ default: "h-9",
154
+ lg: "h-10",
155
+ xl: "h-11"
156
+ }
157
+ },
158
+ defaultVariants: {
159
+ size: "default"
160
+ }
161
+ }
162
+ );
163
+ var Input = React.forwardRef(function Input2({ className, type, size, ...props }, ref) {
164
+ return /* @__PURE__ */ jsx(
165
+ "input",
166
+ {
167
+ ref,
168
+ type,
169
+ "data-slot": "input",
170
+ className: cn(inputVariants({ size }), className),
171
+ ...props
172
+ }
173
+ );
174
+ });
175
+ function Dialog({ ...props }) {
176
+ return /* @__PURE__ */ jsx(DialogPrimitive.Root, { "data-slot": "dialog", ...props });
177
+ }
178
+ function DialogPortal({ ...props }) {
179
+ return /* @__PURE__ */ jsx(DialogPrimitive.Portal, { "data-slot": "dialog-portal", ...props });
180
+ }
181
+ function DialogOverlay({
182
+ className,
183
+ ...props
184
+ }) {
185
+ return /* @__PURE__ */ jsx(
186
+ DialogPrimitive.Overlay,
187
+ {
188
+ "data-slot": "dialog-overlay",
189
+ className: cn(
190
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
191
+ className
192
+ ),
193
+ ...props
194
+ }
195
+ );
196
+ }
197
+ function DialogContent({
198
+ className,
199
+ children,
200
+ ...props
201
+ }) {
202
+ return /* @__PURE__ */ jsxs(DialogPortal, { "data-slot": "dialog-portal", children: [
203
+ /* @__PURE__ */ jsx(DialogOverlay, {}),
204
+ /* @__PURE__ */ jsxs(
205
+ DialogPrimitive.Content,
206
+ {
207
+ "data-slot": "dialog-content",
208
+ className: cn(
209
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
210
+ className
211
+ ),
212
+ ...props,
213
+ children: [
214
+ children,
215
+ /* @__PURE__ */ jsxs(DialogPrimitive.Close, { className: "ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", children: [
216
+ /* @__PURE__ */ jsx(XIcon, {}),
217
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Close" })
218
+ ] })
219
+ ]
220
+ }
221
+ )
222
+ ] });
223
+ }
224
+ function DialogHeader({ className, ...props }) {
225
+ return /* @__PURE__ */ jsx(
226
+ "div",
227
+ {
228
+ "data-slot": "dialog-header",
229
+ className: cn("flex flex-col gap-2 text-center sm:text-left", className),
230
+ ...props
231
+ }
232
+ );
233
+ }
234
+ function DialogFooter({ className, ...props }) {
235
+ return /* @__PURE__ */ jsx(
236
+ "div",
237
+ {
238
+ "data-slot": "dialog-footer",
239
+ className: cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className),
240
+ ...props
241
+ }
242
+ );
243
+ }
244
+ function DialogTitle({ className, ...props }) {
245
+ return /* @__PURE__ */ jsx(
246
+ DialogPrimitive.Title,
247
+ {
248
+ "data-slot": "dialog-title",
249
+ className: cn("text-lg leading-none font-semibold", className),
250
+ ...props
251
+ }
252
+ );
253
+ }
254
+ function DialogDescription({
255
+ className,
256
+ ...props
257
+ }) {
258
+ return /* @__PURE__ */ jsx(
259
+ DialogPrimitive.Description,
260
+ {
261
+ "data-slot": "dialog-description",
262
+ className: cn("text-muted-foreground text-sm", className),
263
+ ...props
264
+ }
265
+ );
266
+ }
267
+ function DropdownMenu({ ...props }) {
268
+ return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
269
+ }
270
+ function DropdownMenuTrigger({
271
+ ...props
272
+ }) {
273
+ return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Trigger, { "data-slot": "dropdown-menu-trigger", ...props });
274
+ }
275
+ function DropdownMenuContent({
276
+ className,
277
+ sideOffset = 4,
278
+ ...props
279
+ }) {
280
+ return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx(
281
+ DropdownMenuPrimitive.Content,
282
+ {
283
+ "data-slot": "dropdown-menu-content",
284
+ sideOffset,
285
+ className: cn(
286
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg border border-white/10 p-1 shadow-md",
287
+ className
288
+ ),
289
+ ...props
290
+ }
291
+ ) });
292
+ }
293
+ function DropdownMenuItem({
294
+ className,
295
+ inset,
296
+ variant = "default",
297
+ ...props
298
+ }) {
299
+ return /* @__PURE__ */ jsx(
300
+ DropdownMenuPrimitive.Item,
301
+ {
302
+ "data-slot": "dropdown-menu-item",
303
+ "data-inset": inset,
304
+ "data-variant": variant,
305
+ className: cn(
306
+ "focus:bg-card/10 focus:text-popover-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
307
+ className
308
+ ),
309
+ ...props
310
+ }
311
+ );
312
+ }
313
+ function DropdownMenuSeparator({
314
+ className,
315
+ ...props
316
+ }) {
317
+ return /* @__PURE__ */ jsx(
318
+ DropdownMenuPrimitive.Separator,
319
+ {
320
+ "data-slot": "dropdown-menu-separator",
321
+ className: cn("bg-card/10 -mx-1 my-1 h-px", className),
322
+ ...props
323
+ }
324
+ );
325
+ }
326
+ function FloorSwitcher({
327
+ floors,
328
+ activeFloorId,
329
+ onFloorChange,
330
+ onAddFloor
331
+ }) {
332
+ const activeFloor = floors.find((f) => f.id === activeFloorId) ?? floors[0];
333
+ const [addOpen, setAddOpen] = useState(false);
334
+ const [name, setName] = useState("");
335
+ useEffect(() => {
336
+ if (addOpen) {
337
+ setName(`Floor ${floors.length + 1}`);
338
+ }
339
+ }, [addOpen, floors.length]);
340
+ const handleAdd = () => {
341
+ onAddFloor?.(name.trim() || `Floor ${floors.length + 1}`);
342
+ setAddOpen(false);
343
+ };
344
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
345
+ /* @__PURE__ */ jsxs(DropdownMenu, { children: [
346
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
347
+ "button",
348
+ {
349
+ type: "button",
350
+ className: cn(
351
+ shellTopBarSecondaryButtonClass,
352
+ "min-w-0 max-w-[10rem] outline-none focus-visible:ring-2 focus-visible:ring-blue-500/30"
353
+ ),
354
+ "aria-label": "Switch floor",
355
+ children: [
356
+ /* @__PURE__ */ jsx(Layers, { size: 12, className: cn("shrink-0", shellTopBarMutedIconClass), "aria-hidden": true }),
357
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: activeFloor?.name ?? "Floor" }),
358
+ /* @__PURE__ */ jsx(ChevronDown, { size: 12, className: cn("shrink-0", shellTopBarMutedIconClass), "aria-hidden": true })
359
+ ]
360
+ }
361
+ ) }),
362
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "start", className: "w-48", children: [
363
+ floors.map((floor) => /* @__PURE__ */ jsxs(
364
+ DropdownMenuItem,
365
+ {
366
+ onClick: () => onFloorChange(floor.id),
367
+ className: "flex items-center justify-between gap-2",
368
+ children: [
369
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: floor.name }),
370
+ floor.id === activeFloorId ? /* @__PURE__ */ jsx(Check, { size: 14, className: "shrink-0 text-blue-600", "aria-hidden": true }) : /* @__PURE__ */ jsx("span", { className: "size-3.5 shrink-0", "aria-hidden": true })
371
+ ]
372
+ },
373
+ floor.id
374
+ )),
375
+ onAddFloor ? /* @__PURE__ */ jsxs(Fragment, { children: [
376
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
377
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => setAddOpen(true), children: [
378
+ /* @__PURE__ */ jsx(Plus, { size: 14, className: "mr-1.5" }),
379
+ "Add floor\u2026"
380
+ ] })
381
+ ] }) : null
382
+ ] })
383
+ ] }),
384
+ onAddFloor ? /* @__PURE__ */ jsx(Dialog, { open: addOpen, onOpenChange: setAddOpen, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-md border-border", children: [
385
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
386
+ /* @__PURE__ */ jsx(DialogTitle, { children: "Add floor" }),
387
+ /* @__PURE__ */ jsx(DialogDescription, { children: "Create a new floor with its own resources and 2D layout." })
388
+ ] }),
389
+ /* @__PURE__ */ jsxs("div", { className: "py-1", children: [
390
+ /* @__PURE__ */ jsx(Label, { htmlFor: "new-floor-name", children: "Name" }),
391
+ /* @__PURE__ */ jsx(
392
+ Input,
393
+ {
394
+ id: "new-floor-name",
395
+ value: name,
396
+ onChange: (e) => setName(e.target.value),
397
+ placeholder: "e.g. Floor 2",
398
+ className: "mt-2 h-9",
399
+ autoFocus: true,
400
+ onKeyDown: (e) => {
401
+ if (e.key === "Enter") handleAdd();
402
+ }
403
+ }
404
+ )
405
+ ] }),
406
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
407
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: () => setAddOpen(false), children: "Cancel" }),
408
+ /* @__PURE__ */ jsx(Button, { type: "button", onClick: handleAdd, children: "Add floor" })
409
+ ] })
410
+ ] }) }) : null
411
+ ] });
412
+ }
413
+
414
+ export { FloorSwitcher };
415
+ //# sourceMappingURL=floor-switcher.js.map
416
+ //# sourceMappingURL=floor-switcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ui/utils.ts","../src/shell/shell-top-bar-chrome.ts","../src/ui/button.tsx","../src/ui/label.tsx","../src/ui/input.tsx","../src/ui/dialog.tsx","../src/ui/dropdown-menu.tsx","../src/live-space/floor-switcher.tsx"],"names":["Button","jsx","cva","React2","Input","jsxs"],"mappings":";;;;;;;;;;;;;AAGO,SAAS,MAAM,MAAA,EAAsB;AAC1C,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC7B;;;ACFO,IAAM,yBAAA,GAA4B,4BAAA;AAIzC,IAAM,qBAAA,GAAwB,EAAA;AAAA,EAC5B,uFAAA;AAAA,EACA;AACF,CAAA;AACA,IAAM,iBAAA,GAAoB,YAAA;AAO1B,IAAM,sBAAA,GACJ,sGAAA;AAGF,IAAM,uBAAA,GAEF,mGAAA;AAEJ,IAAM,wBAAA,GAEF,sGAAA;AAQsC,EAAA;AAAA,EACxC,uFAAA;AAAA,EACA,iBAAA;AAAA,EACA,sBAAA;AAAA,EACA,uBAAA;AAAA,EACA;AACF;AAG0C,EAAA;AAAA,EACxC,uFAAA;AAAA,EACA,iBAAA;AAAA,EACA,sBAAA;AAAA,EACA,uBAAA;AAAA,EACA,wBAAA;AAAA,EACA;AACF;AAMO,IAAM,+BAAA,GAAkC,EAAA;AAAA,EAC7C,qBAAA;AAAA,EACA,iBAAA;AAAA,EACA,kGAAA;AAAA,EACA,OAAA;AAAA,EACA,uBAAA;AAAA,EACA,wBAAA;AAAA,EACA;AACF,CAAA;AAG6C,EAAA;AAAA,EAC3C,qBAAA;AAAA,EACA,iBAAA;AAAA,EACA,4DAAA;AAAA,EACA;AACF;AAG4C,EAAA;AAAA,EAC1C,qBAAA;AAAA,EACA,iBAAA;AAAA,EACA;AACF;AAG6C,EAAA;AAAA,EAC3C,qBAAA;AAAA,EACA,iBAAA;AAAA,EAGI;AACN;AAG0C,EAAA;AAAA,EACxC,qBAAA;AAAA,EACA,iBAAA;AAAA,EAGI;AACN;AAM8C,EAAA;AAAA,EAC5C,gFAAA;AAAA,EACA;AACF;AAmB4C,EAAA;AAAA,EAC1C,kHAAA;AAAA,EACA;AACF;AAUiD,EAAA;AAAA,EAC/C,8EAAA;AAAA,EACA,iBAAA;AAAA,EACA,sBAAA;AAAA,EACA,uBAAA;AAAA,EACA,wBAAA;AAAA,EACA;AACF;AAG+C,EAAA;AAAA,EAC7C,qBAAA;AAAA,EACA,cAAA;AAAA,EACA,sBAAA;AAAA,EACA,uBAAA;AAAA,EACA;AACF;ACtJA,IAAM,cAAA,GAAiB,GAAA;AAAA,EACrB,weAAA;AAAA,EACA;AAAA,IACE,QAAA,EAAU;AAAA,MACR,OAAA,EAAS;AAAA,QACP,OAAA,EACE,iGAAA;AAAA,QACF,WAAA,EACE,8NAAA;AAAA,QACF,OAAA,EACE,6MAAA;AAAA,QACF,SAAA,EACE,yGAAA;AAAA,QACF,KAAA,EACE,uIAAA;AAAA,QACF,IAAA,EAAM;AAAA,OACR;AAAA,MACA,IAAA,EAAM;AAAA,QACJ,OAAA,EAAS,+BAAA;AAAA,QACT,EAAA,EAAI,+CAAA;AAAA,QACJ,EAAA,EAAI,sCAAA;AAAA,QACJ,IAAA,EAAM;AAAA;AACR,KACF;AAAA,IACA,eAAA,EAAiB;AAAA,MACf,OAAA,EAAS,SAAA;AAAA,MACT,IAAA,EAAM;AAAA;AACR;AAEJ,CAAA;AAEA,IAAM,MAAA,GAAe,KAAA,CAAA,UAAA,CAMnB,SAASA,OAAAA,CAAO,EAAE,SAAA,EAAW,OAAA,EAAS,IAAA,EAAM,OAAA,GAAU,KAAA,EAAO,GAAG,KAAA,IAAS,GAAA,EAAK;AAC9E,EAAA,MAAM,IAAA,GAAO,UAAU,IAAA,GAAO,QAAA;AAE9B,EAAA,uBACE,GAAA;AAAA,IAAC,IAAA;AAAA,IAAA;AAAA,MACC,GAAA;AAAA,MACA,WAAA,EAAU,QAAA;AAAA,MACV,SAAA,EAAW,GAAG,cAAA,CAAe,EAAE,SAAS,IAAA,EAAM,GAAG,SAAS,CAAA;AAAA,MACzD,GAAG;AAAA;AAAA,GACN;AAEJ,CAAC,CAAA;AACD,MAAA,CAAO,WAAA,GAAc,QAAA;AChDrB,SAAS,KAAA,CAAM,EAAE,SAAA,EAAW,GAAG,OAAM,EAAqD;AACxF,EAAA,uBACEC,GAAAA;AAAA,IAAgB,cAAA,CAAA,IAAA;AAAA,IAAf;AAAA,MACC,WAAA,EAAU,OAAA;AAAA,MACV,SAAA,EAAW,EAAA;AAAA,QACT,qNAAA;AAAA,QACA;AAAA,OACF;AAAA,MACC,GAAG;AAAA;AAAA,GACN;AAEJ;ACbA,IAAM,aAAA,GAAgBC,GAAAA;AAAA,EACpB,2jBAAA;AAAA,EACA;AAAA,IACE,QAAA,EAAU;AAAA,MACR,IAAA,EAAM;AAAA,QACJ,EAAA,EAAI,aAAA;AAAA,QACJ,OAAA,EAAS,KAAA;AAAA,QACT,EAAA,EAAI,MAAA;AAAA,QACJ,EAAA,EAAI;AAAA;AACN,KACF;AAAA,IACA,eAAA,EAAiB;AAAA,MACf,IAAA,EAAM;AAAA;AACR;AAEJ,CAAA;AAEA,IAAM,KAAA,GAAcC,KAAA,CAAA,UAAA,CAGlB,SAASC,MAAAA,CAAM,EAAE,SAAA,EAAW,IAAA,EAAM,IAAA,EAAM,GAAG,KAAA,EAAM,EAAG,GAAA,EAAK;AACzD,EAAA,uBACEH,GAAAA;AAAA,IAAC,OAAA;AAAA,IAAA;AAAA,MACC,GAAA;AAAA,MACA,IAAA;AAAA,MACA,WAAA,EAAU,OAAA;AAAA,MACV,WAAW,EAAA,CAAG,aAAA,CAAc,EAAE,IAAA,EAAM,GAAG,SAAS,CAAA;AAAA,MAC/C,GAAG;AAAA;AAAA,GACN;AAEJ,CAAC,CAAA;AC3BD,SAAS,MAAA,CAAO,EAAE,GAAG,KAAA,EAAM,EAAsD;AAC/E,EAAA,uBAAOA,GAAAA,CAAiB,eAAA,CAAA,IAAA,EAAhB,EAAqB,WAAA,EAAU,QAAA,EAAU,GAAG,KAAA,EAAO,CAAA;AAC7D;AAMA,SAAS,YAAA,CAAa,EAAE,GAAG,KAAA,EAAM,EAAwD;AACvF,EAAA,uBAAOA,GAAAA,CAAiB,eAAA,CAAA,MAAA,EAAhB,EAAuB,WAAA,EAAU,eAAA,EAAiB,GAAG,KAAA,EAAO,CAAA;AACtE;AAMA,SAAS,aAAA,CAAc;AAAA,EACrB,SAAA;AAAA,EACA,GAAG;AACL,CAAA,EAAyD;AACvD,EAAA,uBACEA,GAAAA;AAAA,IAAiB,eAAA,CAAA,OAAA;AAAA,IAAhB;AAAA,MACC,WAAA,EAAU,gBAAA;AAAA,MACV,SAAA,EAAW,EAAA;AAAA,QACT,wJAAA;AAAA,QACA;AAAA,OACF;AAAA,MACC,GAAG;AAAA;AAAA,GACN;AAEJ;AAEA,SAAS,aAAA,CAAc;AAAA,EACrB,SAAA;AAAA,EACA,QAAA;AAAA,EACA,GAAG;AACL,CAAA,EAAyD;AACvD,EAAA,uBACE,IAAA,CAAC,YAAA,EAAA,EAAa,WAAA,EAAU,eAAA,EACtB,QAAA,EAAA;AAAA,oBAAAA,IAAC,aAAA,EAAA,EAAc,CAAA;AAAA,oBACf,IAAA;AAAA,MAAiB,eAAA,CAAA,OAAA;AAAA,MAAhB;AAAA,QACC,WAAA,EAAU,gBAAA;AAAA,QACV,SAAA,EAAW,EAAA;AAAA,UACT,6WAAA;AAAA,UACA;AAAA,SACF;AAAA,QACC,GAAG,KAAA;AAAA,QAEH,QAAA,EAAA;AAAA,UAAA,QAAA;AAAA,0BACD,IAAA,CAAiB,eAAA,CAAA,KAAA,EAAhB,EAAsB,SAAA,EAAU,mWAAA,EAC/B,QAAA,EAAA;AAAA,4BAAAA,IAAC,KAAA,EAAA,EAAM,CAAA;AAAA,4BACPA,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,WAAU,QAAA,EAAA,OAAA,EAAK;AAAA,WAAA,EACjC;AAAA;AAAA;AAAA;AACF,GAAA,EACF,CAAA;AAEJ;AAEA,SAAS,YAAA,CAAa,EAAE,SAAA,EAAW,GAAG,OAAM,EAAgC;AAC1E,EAAA,uBACEA,GAAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,WAAA,EAAU,eAAA;AAAA,MACV,SAAA,EAAW,EAAA,CAAG,8CAAA,EAAgD,SAAS,CAAA;AAAA,MACtE,GAAG;AAAA;AAAA,GACN;AAEJ;AAEA,SAAS,YAAA,CAAa,EAAE,SAAA,EAAW,GAAG,OAAM,EAAgC;AAC1E,EAAA,uBACEA,GAAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,WAAA,EAAU,eAAA;AAAA,MACV,SAAA,EAAW,EAAA,CAAG,wDAAA,EAA0D,SAAS,CAAA;AAAA,MAChF,GAAG;AAAA;AAAA,GACN;AAEJ;AAEA,SAAS,WAAA,CAAY,EAAE,SAAA,EAAW,GAAG,OAAM,EAAuD;AAChG,EAAA,uBACEA,GAAAA;AAAA,IAAiB,eAAA,CAAA,KAAA;AAAA,IAAhB;AAAA,MACC,WAAA,EAAU,cAAA;AAAA,MACV,SAAA,EAAW,EAAA,CAAG,oCAAA,EAAsC,SAAS,CAAA;AAAA,MAC5D,GAAG;AAAA;AAAA,GACN;AAEJ;AAEA,SAAS,iBAAA,CAAkB;AAAA,EACzB,SAAA;AAAA,EACA,GAAG;AACL,CAAA,EAA6D;AAC3D,EAAA,uBACEA,GAAAA;AAAA,IAAiB,eAAA,CAAA,WAAA;AAAA,IAAhB;AAAA,MACC,WAAA,EAAU,oBAAA;AAAA,MACV,SAAA,EAAW,EAAA,CAAG,+BAAA,EAAiC,SAAS,CAAA;AAAA,MACvD,GAAG;AAAA;AAAA,GACN;AAEJ;ACnGA,SAAS,YAAA,CAAa,EAAE,GAAG,KAAA,EAAM,EAA4D;AAC3F,EAAA,uBAAOA,GAAAA,CAAuB,qBAAA,CAAA,IAAA,EAAtB,EAA2B,WAAA,EAAU,eAAA,EAAiB,GAAG,KAAA,EAAO,CAAA;AAC1E;AAQA,SAAS,mBAAA,CAAoB;AAAA,EAC3B,GAAG;AACL,CAAA,EAA+D;AAC7D,EAAA,uBAAOA,GAAAA,CAAuB,qBAAA,CAAA,OAAA,EAAtB,EAA8B,WAAA,EAAU,uBAAA,EAAyB,GAAG,KAAA,EAAO,CAAA;AACrF;AAEA,SAAS,mBAAA,CAAoB;AAAA,EAC3B,SAAA;AAAA,EACA,UAAA,GAAa,CAAA;AAAA,EACb,GAAG;AACL,CAAA,EAA+D;AAC7D,EAAA,uBACEA,GAAAA,CAAuB,qBAAA,CAAA,MAAA,EAAtB,EACC,QAAA,kBAAAA,GAAAA;AAAA,IAAuB,qBAAA,CAAA,OAAA;AAAA,IAAtB;AAAA,MACC,WAAA,EAAU,uBAAA;AAAA,MACV,UAAA;AAAA,MACA,SAAA,EAAW,EAAA;AAAA,QACT,wkBAAA;AAAA,QACA;AAAA,OACF;AAAA,MACC,GAAG;AAAA;AAAA,GACN,EACF,CAAA;AAEJ;AAMA,SAAS,gBAAA,CAAiB;AAAA,EACxB,SAAA;AAAA,EACA,KAAA;AAAA,EACA,OAAA,GAAU,SAAA;AAAA,EACV,GAAG;AACL,CAAA,EAGG;AACD,EAAA,uBACEA,GAAAA;AAAA,IAAuB,qBAAA,CAAA,IAAA;AAAA,IAAtB;AAAA,MACC,WAAA,EAAU,oBAAA;AAAA,MACV,YAAA,EAAY,KAAA;AAAA,MACZ,cAAA,EAAc,OAAA;AAAA,MACd,SAAA,EAAW,EAAA;AAAA,QACT,+mBAAA;AAAA,QACA;AAAA,OACF;AAAA,MACC,GAAG;AAAA;AAAA,GACN;AAEJ;AA2EA,SAAS,qBAAA,CAAsB;AAAA,EAC7B,SAAA;AAAA,EACA,GAAG;AACL,CAAA,EAAiE;AAC/D,EAAA,uBACEA,GAAAA;AAAA,IAAuB,qBAAA,CAAA,SAAA;AAAA,IAAtB;AAAA,MACC,WAAA,EAAU,yBAAA;AAAA,MACV,SAAA,EAAW,EAAA,CAAG,4BAAA,EAA8B,SAAS,CAAA;AAAA,MACpD,GAAG;AAAA;AAAA,GACN;AAEJ;ACjIO,SAAS,aAAA,CAAc;AAAA,EAC5B,MAAA;AAAA,EACA,aAAA;AAAA,EACA,aAAA;AAAA,EACA;AACF,CAAA,EAKG;AACD,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA,KAAM,EAAE,EAAA,KAAO,aAAa,CAAA,IAAK,MAAA,CAAO,CAAC,CAAA;AAC1E,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,KAAK,CAAA;AAC5C,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAI,SAAS,EAAE,CAAA;AAEnC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,OAAA,CAAQ,CAAA,MAAA,EAAS,MAAA,CAAO,MAAA,GAAS,CAAC,CAAA,CAAE,CAAA;AAAA,IACtC;AAAA,EACF,CAAA,EAAG,CAAC,OAAA,EAAS,MAAA,CAAO,MAAM,CAAC,CAAA;AAE3B,EAAA,MAAM,YAAY,MAAM;AACtB,IAAA,UAAA,GAAa,KAAK,IAAA,EAAK,IAAK,SAAS,MAAA,CAAO,MAAA,GAAS,CAAC,CAAA,CAAE,CAAA;AACxD,IAAA,UAAA,CAAW,KAAK,CAAA;AAAA,EAClB,CAAA;AAEA,EAAA,uBACEI,KAAA,QAAA,EAAA,EACE,QAAA,EAAA;AAAA,oBAAAA,KAAC,YAAA,EAAA,EACC,QAAA,EAAA;AAAA,sBAAAJ,GAAAA,CAAC,mBAAA,EAAA,EAAoB,OAAA,EAAO,IAAA,EAC1B,QAAA,kBAAAI,IAAAA;AAAA,QAAC,QAAA;AAAA,QAAA;AAAA,UACC,IAAA,EAAK,QAAA;AAAA,UACL,SAAA,EAAW,EAAA;AAAA,YACT,+BAAA;AAAA,YACA;AAAA,WACF;AAAA,UACA,YAAA,EAAW,cAAA;AAAA,UAEX,QAAA,EAAA;AAAA,4BAAAJ,GAAAA,CAAC,MAAA,EAAA,EAAO,IAAA,EAAM,EAAA,EAAI,SAAA,EAAW,GAAG,UAAA,EAAY,yBAAyB,CAAA,EAAG,aAAA,EAAW,IAAA,EAAC,CAAA;AAAA,4BACpFA,GAAAA,CAAC,MAAA,EAAA,EAAK,WAAU,UAAA,EAAY,QAAA,EAAA,WAAA,EAAa,QAAQ,OAAA,EAAQ,CAAA;AAAA,4BACzDA,GAAAA,CAAC,WAAA,EAAA,EAAY,IAAA,EAAM,EAAA,EAAI,SAAA,EAAW,EAAA,CAAG,UAAA,EAAY,yBAAyB,CAAA,EAAG,aAAA,EAAW,IAAA,EAAC;AAAA;AAAA;AAAA,OAC3F,EACF,CAAA;AAAA,sBACAI,IAAAA,CAAC,mBAAA,EAAA,EAAoB,KAAA,EAAM,OAAA,EAAQ,WAAU,MAAA,EAC1C,QAAA,EAAA;AAAA,QAAA,MAAA,CAAO,GAAA,CAAI,CAAC,KAAA,qBACXA,IAAAA;AAAA,UAAC,gBAAA;AAAA,UAAA;AAAA,YAEC,OAAA,EAAS,MAAM,aAAA,CAAc,KAAA,CAAM,EAAE,CAAA;AAAA,YACrC,SAAA,EAAU,yCAAA;AAAA,YAEV,QAAA,EAAA;AAAA,8BAAAJ,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,UAAA,EAAY,gBAAM,IAAA,EAAK,CAAA;AAAA,cACtC,MAAM,EAAA,KAAO,aAAA,mBACZA,GAAAA,CAAC,KAAA,EAAA,EAAM,MAAM,EAAA,EAAI,SAAA,EAAU,0BAAyB,aAAA,EAAW,IAAA,EAAC,oBAEhEA,GAAAA,CAAC,UAAK,SAAA,EAAU,mBAAA,EAAoB,eAAW,IAAA,EAAC;AAAA;AAAA,WAAA;AAAA,UAR7C,KAAA,CAAM;AAAA,SAWd,CAAA;AAAA,QACA,UAAA,mBACCI,IAAAA,CAAA,QAAA,EAAA,EACE,QAAA,EAAA;AAAA,0BAAAJ,IAAC,qBAAA,EAAA,EAAsB,CAAA;AAAA,0BACvBI,IAAAA,CAAC,gBAAA,EAAA,EAAiB,SAAS,MAAM,UAAA,CAAW,IAAI,CAAA,EAC9C,QAAA,EAAA;AAAA,4BAAAJ,GAAAA,CAAC,IAAA,EAAA,EAAK,IAAA,EAAM,EAAA,EAAI,WAAU,QAAA,EAAS,CAAA;AAAA,YAAE;AAAA,WAAA,EAEvC;AAAA,SAAA,EACF,CAAA,GACE;AAAA,OAAA,EACN;AAAA,KAAA,EACF,CAAA;AAAA,IAEC,UAAA,mBACCA,GAAAA,CAAC,MAAA,EAAA,EAAO,IAAA,EAAM,OAAA,EAAS,YAAA,EAAc,UAAA,EACnC,QAAA,kBAAAI,IAAAA,CAAC,aAAA,EAAA,EAAc,SAAA,EAAU,2BAAA,EACvB,QAAA,EAAA;AAAA,sBAAAA,KAAC,YAAA,EAAA,EACC,QAAA,EAAA;AAAA,wBAAAJ,GAAAA,CAAC,eAAY,QAAA,EAAA,WAAA,EAAS,CAAA;AAAA,wBACtBA,GAAAA,CAAC,iBAAA,EAAA,EAAkB,QAAA,EAAA,0DAAA,EAEnB;AAAA,OAAA,EACF,CAAA;AAAA,sBACAI,IAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,MAAA,EACb,QAAA,EAAA;AAAA,wBAAAJ,GAAAA,CAAC,KAAA,EAAA,EAAM,OAAA,EAAQ,gBAAA,EAAiB,QAAA,EAAA,MAAA,EAAI,CAAA;AAAA,wBACpCA,GAAAA;AAAA,UAAC,KAAA;AAAA,UAAA;AAAA,YACC,EAAA,EAAG,gBAAA;AAAA,YACH,KAAA,EAAO,IAAA;AAAA,YACP,UAAU,CAAC,CAAA,KAAM,OAAA,CAAQ,CAAA,CAAE,OAAO,KAAK,CAAA;AAAA,YACvC,WAAA,EAAY,cAAA;AAAA,YACZ,SAAA,EAAU,UAAA;AAAA,YACV,SAAA,EAAS,IAAA;AAAA,YACT,SAAA,EAAW,CAAC,CAAA,KAAM;AAChB,cAAA,IAAI,CAAA,CAAE,GAAA,KAAQ,OAAA,EAAS,SAAA,EAAU;AAAA,YACnC;AAAA;AAAA;AACF,OAAA,EACF,CAAA;AAAA,sBACAI,KAAC,YAAA,EAAA,EACC,QAAA,EAAA;AAAA,wBAAAJ,GAAAA,CAAC,MAAA,EAAA,EAAO,IAAA,EAAK,QAAA,EAAS,OAAA,EAAQ,SAAA,EAAU,OAAA,EAAS,MAAM,UAAA,CAAW,KAAK,CAAA,EAAG,QAAA,EAAA,QAAA,EAE1E,CAAA;AAAA,wBACAA,GAAAA,CAAC,MAAA,EAAA,EAAO,MAAK,QAAA,EAAS,OAAA,EAAS,WAAW,QAAA,EAAA,WAAA,EAE1C;AAAA,OAAA,EACF;AAAA,KAAA,EACF,GACF,CAAA,GACE;AAAA,GAAA,EACN,CAAA;AAEJ","file":"floor-switcher.js","sourcesContent":["import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","import { getShellTheme } from \"../theme/shell-theme\";\nimport { cn } from \"../ui/utils\";\n\nexport const shellTopBarMutedIconClass = \"text-sidebar-foreground/60\";\nexport const shellTopBarTitleTextClass = \"text-sidebar-foreground\";\n\n/** Shared sizing and corner radius for shell top bar actions. */\nconst shellTopBarActionSize = cn(\n \"inline-flex !h-7 min-h-0 items-center justify-center gap-1 px-2.5 text-xs font-medium\",\n \"[&_svg:not([class*='size-'])]:size-3.5\",\n);\nconst shellTopBarRadius = \"rounded-md\";\n\nfunction isDarkShell() {\n return getShellTheme() === \"dark\";\n}\n\n/** Resting surface for neutral shell controls (secondary, outline, icon). */\nconst shellTopBarNeutralRest =\n \"border border-sidebar-border bg-sidebar-accent text-sidebar-foreground shadow-none transition-colors\";\n\n/** Visible hover — lighten fill on dark shell, darken on light shell. */\nconst shellTopBarNeutralHover = isDarkShell()\n ? \"hover:!border-sidebar-foreground/25 hover:!bg-sidebar-foreground/12 hover:!text-sidebar-foreground\"\n : \"hover:!border-sidebar-foreground/14 hover:!bg-sidebar-foreground/6 hover:!text-sidebar-foreground\";\n\nconst shellTopBarNeutralActive = isDarkShell()\n ? \"active:!border-sidebar-foreground/20 active:!bg-sidebar-foreground/8 active:!text-sidebar-foreground\"\n : \"active:!border-sidebar-foreground/10 active:!bg-sidebar-foreground/4 active:!text-sidebar-foreground\";\n\n/** Apply to native shell actions (links, ticket buttons) so they share the same hover treatment as {@link ShellTopBarSecondaryActionButton}. */\nexport const shellTopBarNeutralActionDataAttr = {\n \"data-shell-top-bar-action\": \"\",\n} as const;\n\n/** Compact back control in the shell top bar (detail pages). */\nexport const shellTopBarBackButtonClass = cn(\n \"inline-flex !h-6 !w-6 shrink-0 items-center justify-center text-sidebar-foreground/70\",\n shellTopBarRadius,\n shellTopBarNeutralRest,\n shellTopBarNeutralHover,\n shellTopBarNeutralActive,\n);\n\n/** Icon-only control (e.g. overflow menu) in the shell top bar. */\nexport const shellTopBarIconButtonClass = cn(\n \"inline-flex !h-7 !w-7 shrink-0 items-center justify-center text-sidebar-foreground/70\",\n shellTopBarRadius,\n shellTopBarNeutralRest,\n shellTopBarNeutralHover,\n shellTopBarNeutralActive,\n \"[&_svg:not([class*='size-'])]:size-3.5\",\n);\n\n/**\n * Neutral secondary / outline action in the shell top bar.\n * Uses `!` on hover so shadcn `Button variant=\"outline\"` cannot win.\n */\nexport const shellTopBarSecondaryButtonClass = cn(\n shellTopBarActionSize,\n shellTopBarRadius,\n \"!border-sidebar-border !bg-sidebar-accent !text-sidebar-foreground shadow-none transition-colors\",\n \"!py-0\",\n shellTopBarNeutralHover,\n shellTopBarNeutralActive,\n \"disabled:pointer-events-none disabled:!border-sidebar-border/60 disabled:!bg-sidebar-accent/60 disabled:!text-sidebar-foreground/40\",\n);\n\n/** Primary CTA in the shell top bar (New invoice, Add staff, etc.). */\nexport const shellTopBarPrimaryButtonClass = cn(\n shellTopBarActionSize,\n shellTopBarRadius,\n \"bg-blue-500 text-white transition-colors hover:bg-blue-600\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n);\n\n/** Positive / payment emphasis in the shell top bar (e.g. Mark as paid). */\nexport const shellTopBarAccentButtonClass = cn(\n shellTopBarActionSize,\n shellTopBarRadius,\n \"bg-emerald-600 text-white transition-colors hover:bg-emerald-700\",\n);\n\n/** Positive status action in the shell top bar (e.g. Mark resolved). */\nexport const shellTopBarSuccessButtonClass = cn(\n shellTopBarActionSize,\n shellTopBarRadius,\n isDarkShell()\n ? \"border border-emerald-500/35 bg-emerald-500/15 text-emerald-300 transition-colors hover:border-emerald-400/45 hover:bg-emerald-500/25\"\n : \"border border-emerald-200 bg-emerald-50 text-emerald-700 transition-colors hover:border-emerald-300 hover:bg-emerald-100\",\n);\n\n/** Informative / reopen action in the shell top bar. */\nexport const shellTopBarInfoButtonClass = cn(\n shellTopBarActionSize,\n shellTopBarRadius,\n isDarkShell()\n ? \"border border-blue-500/35 bg-blue-500/15 text-blue-300 transition-colors hover:border-blue-400/45 hover:bg-blue-500/25\"\n : \"border border-blue-200 bg-blue-50 text-blue-700 transition-colors hover:border-blue-300 hover:bg-blue-100\",\n);\n\n/** Outline-style action — same neutral chrome as {@link shellTopBarSecondaryButtonClass}. */\nexport const shellTopBarOutlineButtonClass = shellTopBarSecondaryButtonClass;\n\n/** Wrapper for segmented toggles (2D / list, etc.) in the shell top bar. */\nexport const shellTopBarSegmentedShellClass = cn(\n \"flex items-center gap-0.5 border border-sidebar-border bg-sidebar-accent p-0.5\",\n shellTopBarRadius,\n);\n\nexport function shellTopBarSegmentedItemClass(active: boolean) {\n const dark = isDarkShell();\n return cn(\n shellTopBarActionSize,\n \"!h-6 px-2\",\n \"rounded-sm\",\n active\n ? dark\n ? \"bg-sidebar text-sidebar-foreground shadow-sm\"\n : \"bg-white text-sidebar-foreground shadow-sm\"\n : dark\n ? \"text-sidebar-foreground/60 hover:bg-sidebar-foreground/10 hover:text-sidebar-foreground\"\n : \"text-sidebar-foreground/60 hover:bg-sidebar-foreground/6 hover:text-sidebar-foreground\",\n );\n}\n\n/** Grouped field shell (month picker, etc.) in the top bar. */\nexport const shellTopBarControlGroupClass = cn(\n \"flex h-7 items-center gap-1.5 border border-sidebar-border bg-sidebar-accent pl-2.5 pr-1 text-sidebar-foreground\",\n shellTopBarRadius,\n);\n\nexport const shellTopBarControlLabelClass =\n \"m-0 shrink-0 text-[11px] font-normal text-sidebar-foreground/60\";\n\n/** Select trigger embedded in {@link shellTopBarControlGroupClass}. */\nexport const shellTopBarSelectTriggerClass =\n \"h-6 min-w-[140px] flex-1 border-0 bg-transparent px-1 text-xs text-sidebar-foreground shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 [&_svg]:text-sidebar-foreground/60\";\n\n/** Square control for date steppers and similar compact actions. */\nexport const shellTopBarCompactIconButtonClass = cn(\n \"inline-flex !h-6 !w-6 items-center justify-center text-sidebar-foreground/70\",\n shellTopBarRadius,\n shellTopBarNeutralRest,\n shellTopBarNeutralHover,\n shellTopBarNeutralActive,\n \"[&_svg:not([class*='size-'])]:size-3.5\",\n);\n\n/** Center label in a date stepper (shows “Today” or a formatted date). */\nexport const shellTopBarDateLabelButtonClass = cn(\n shellTopBarActionSize,\n \"min-w-[72px]\",\n shellTopBarNeutralRest,\n shellTopBarNeutralHover,\n shellTopBarNeutralActive,\n);\n","import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"./utils\";\n\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium cursor-pointer transition-all disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n {\n variants: {\n variant: {\n default:\n \"bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80 active:scale-[0.98]\",\n destructive:\n \"bg-destructive text-white hover:bg-destructive/90 active:bg-destructive/80 active:scale-[0.98] focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 dark:active:bg-destructive/70\",\n outline:\n \"border bg-background text-foreground hover:bg-accent hover:text-accent-foreground active:bg-accent/80 active:scale-[0.98] dark:bg-input/30 dark:border-input dark:hover:bg-input/50 dark:active:bg-input/60\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-secondary/80 active:bg-secondary/70 active:scale-[0.98]\",\n ghost:\n \"hover:bg-accent hover:text-accent-foreground active:bg-accent/90 active:scale-[0.98] dark:hover:bg-accent/50 dark:active:bg-accent/40\",\n link: \"text-primary underline-offset-4 hover:underline active:text-primary/80\",\n },\n size: {\n default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n icon: \"size-9 rounded-md\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n },\n);\n\nconst Button = React.forwardRef<\n HTMLButtonElement,\n React.ComponentProps<\"button\"> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean;\n }\n>(function Button({ className, variant, size, asChild = false, ...props }, ref) {\n const Comp = asChild ? Slot : \"button\";\n\n return (\n <Comp\n ref={ref}\n data-slot=\"button\"\n className={cn(buttonVariants({ variant, size }), className)}\n {...props}\n />\n );\n});\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n","\"use client\";\n\nimport * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\n\nimport { cn } from \"./utils\";\n\nfunction Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {\n return (\n <LabelPrimitive.Root\n data-slot=\"label\"\n className={cn(\n \"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { Label };\n","import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"./utils\";\n\nconst inputVariants = cva(\n \"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-border bg-card text-foreground flex w-full min-w-0 rounded-md border px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:border-blue-400 focus-visible:ring-[3px] focus-visible:ring-blue-500/25 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-red-400 aria-invalid:ring-red-500/20 md:text-sm\",\n {\n variants: {\n size: {\n sm: \"h-8 text-sm\",\n default: \"h-9\",\n lg: \"h-10\",\n xl: \"h-11\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n },\n);\n\nconst Input = React.forwardRef<\n HTMLInputElement,\n Omit<React.ComponentProps<\"input\">, \"size\"> & VariantProps<typeof inputVariants>\n>(function Input({ className, type, size, ...props }, ref) {\n return (\n <input\n ref={ref}\n type={type}\n data-slot=\"input\"\n className={cn(inputVariants({ size }), className)}\n {...props}\n />\n );\n});\n\nexport { Input, inputVariants };\n","\"use client\";\n\nimport * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { XIcon } from \"lucide-react\";\n\nimport { cn } from \"./utils\";\n\nfunction Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n return (\n <DialogPrimitive.Overlay\n data-slot=\"dialog-overlay\"\n className={cn(\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DialogContent({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content>) {\n return (\n <DialogPortal data-slot=\"dialog-portal\">\n <DialogOverlay />\n <DialogPrimitive.Content\n data-slot=\"dialog-content\"\n className={cn(\n \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg\",\n className,\n )}\n {...props}\n >\n {children}\n <DialogPrimitive.Close className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\">\n <XIcon />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n </DialogPrimitive.Content>\n </DialogPortal>\n );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-header\"\n className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n {...props}\n />\n );\n}\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn(\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\", className)}\n {...props}\n />\n );\n}\n\nfunction DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {\n return (\n <DialogPrimitive.Title\n data-slot=\"dialog-title\"\n className={cn(\"text-lg leading-none font-semibold\", className)}\n {...props}\n />\n );\n}\n\nfunction DialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return (\n <DialogPrimitive.Description\n data-slot=\"dialog-description\"\n className={cn(\"text-muted-foreground text-sm\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n};\n","\"use client\";\n\nimport * as React from \"react\";\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\";\n\nimport { cn } from \"./utils\";\n\nfunction DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />;\n}\n\nfunction DropdownMenuPortal({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n return <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />;\n}\n\nfunction DropdownMenuTrigger({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n return <DropdownMenuPrimitive.Trigger data-slot=\"dropdown-menu-trigger\" {...props} />;\n}\n\nfunction DropdownMenuContent({\n className,\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n data-slot=\"dropdown-menu-content\"\n sideOffset={sideOffset}\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg border border-white/10 p-1 shadow-md\",\n className,\n )}\n {...props}\n />\n </DropdownMenuPrimitive.Portal>\n );\n}\n\nfunction DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n return <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />;\n}\n\nfunction DropdownMenuItem({\n className,\n inset,\n variant = \"default\",\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n inset?: boolean;\n variant?: \"default\" | \"destructive\";\n}) {\n return (\n <DropdownMenuPrimitive.Item\n data-slot=\"dropdown-menu-item\"\n data-inset={inset}\n data-variant={variant}\n className={cn(\n \"focus:bg-card/10 focus:text-popover-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DropdownMenuCheckboxItem({\n className,\n children,\n checked,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n return (\n <DropdownMenuPrimitive.CheckboxItem\n data-slot=\"dropdown-menu-checkbox-item\"\n className={cn(\n \"focus:bg-card/10 focus:text-popover-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className,\n )}\n checked={checked}\n {...props}\n >\n <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <CheckIcon className=\"size-4\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.CheckboxItem>\n );\n}\n\nfunction DropdownMenuRadioGroup({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n return <DropdownMenuPrimitive.RadioGroup data-slot=\"dropdown-menu-radio-group\" {...props} />;\n}\n\nfunction DropdownMenuRadioItem({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n return (\n <DropdownMenuPrimitive.RadioItem\n data-slot=\"dropdown-menu-radio-item\"\n className={cn(\n \"focus:bg-card/10 focus:text-popover-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className,\n )}\n {...props}\n >\n <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <CircleIcon className=\"size-2 fill-current\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.RadioItem>\n );\n}\n\nfunction DropdownMenuLabel({\n className,\n inset,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n inset?: boolean;\n}) {\n return (\n <DropdownMenuPrimitive.Label\n data-slot=\"dropdown-menu-label\"\n data-inset={inset}\n className={cn(\"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8\", className)}\n {...props}\n />\n );\n}\n\nfunction DropdownMenuSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n return (\n <DropdownMenuPrimitive.Separator\n data-slot=\"dropdown-menu-separator\"\n className={cn(\"bg-card/10 -mx-1 my-1 h-px\", className)}\n {...props}\n />\n );\n}\n\nfunction DropdownMenuShortcut({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"dropdown-menu-shortcut\"\n className={cn(\"text-muted-foreground ml-auto text-xs tracking-widest\", className)}\n {...props}\n />\n );\n}\n\nfunction DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {\n return <DropdownMenuPrimitive.Sub data-slot=\"dropdown-menu-sub\" {...props} />;\n}\n\nfunction DropdownMenuSubTrigger({\n className,\n inset,\n children,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n inset?: boolean;\n}) {\n return (\n <DropdownMenuPrimitive.SubTrigger\n data-slot=\"dropdown-menu-sub-trigger\"\n data-inset={inset}\n className={cn(\n \"focus:bg-card/10 focus:text-popover-foreground data-[state=open]:bg-card/10 data-[state=open]:text-popover-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8\",\n className,\n )}\n {...props}\n >\n {children}\n <ChevronRightIcon className=\"ml-auto size-4\" />\n </DropdownMenuPrimitive.SubTrigger>\n );\n}\n\nfunction DropdownMenuSubContent({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n return (\n <DropdownMenuPrimitive.SubContent\n data-slot=\"dropdown-menu-sub-content\"\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg border border-white/10 p-1 shadow-md\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport {\n DropdownMenu,\n DropdownMenuPortal,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuLabel,\n DropdownMenuItem,\n DropdownMenuCheckboxItem,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuSub,\n DropdownMenuSubTrigger,\n DropdownMenuSubContent,\n};\n","import { Check, ChevronDown, Layers, Plus } from \"lucide-react\";\nimport { shellTopBarMutedIconClass, shellTopBarSecondaryButtonClass } from \"../shell/shell-top-bar-chrome\";\nimport {\n Button,\n cn,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Input,\n Label,\n} from \"../ui\";\nimport { useEffect, useState } from \"react\";\n\nexport interface FloorSwitcherFloor {\n id: string;\n name: string;\n}\n\nexport function FloorSwitcher({\n floors,\n activeFloorId,\n onFloorChange,\n onAddFloor,\n}: {\n floors: FloorSwitcherFloor[];\n activeFloorId: string;\n onFloorChange: (floorId: string) => void;\n onAddFloor?: (name: string) => void;\n}) {\n const activeFloor = floors.find((f) => f.id === activeFloorId) ?? floors[0];\n const [addOpen, setAddOpen] = useState(false);\n const [name, setName] = useState(\"\");\n\n useEffect(() => {\n if (addOpen) {\n setName(`Floor ${floors.length + 1}`);\n }\n }, [addOpen, floors.length]);\n\n const handleAdd = () => {\n onAddFloor?.(name.trim() || `Floor ${floors.length + 1}`);\n setAddOpen(false);\n };\n\n return (\n <>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n className={cn(\n shellTopBarSecondaryButtonClass,\n \"min-w-0 max-w-[10rem] outline-none focus-visible:ring-2 focus-visible:ring-blue-500/30\",\n )}\n aria-label=\"Switch floor\"\n >\n <Layers size={12} className={cn(\"shrink-0\", shellTopBarMutedIconClass)} aria-hidden />\n <span className=\"truncate\">{activeFloor?.name ?? \"Floor\"}</span>\n <ChevronDown size={12} className={cn(\"shrink-0\", shellTopBarMutedIconClass)} aria-hidden />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-48\">\n {floors.map((floor) => (\n <DropdownMenuItem\n key={floor.id}\n onClick={() => onFloorChange(floor.id)}\n className=\"flex items-center justify-between gap-2\"\n >\n <span className=\"truncate\">{floor.name}</span>\n {floor.id === activeFloorId ? (\n <Check size={14} className=\"shrink-0 text-blue-600\" aria-hidden />\n ) : (\n <span className=\"size-3.5 shrink-0\" aria-hidden />\n )}\n </DropdownMenuItem>\n ))}\n {onAddFloor ? (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={() => setAddOpen(true)}>\n <Plus size={14} className=\"mr-1.5\" />\n Add floor…\n </DropdownMenuItem>\n </>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n\n {onAddFloor ? (\n <Dialog open={addOpen} onOpenChange={setAddOpen}>\n <DialogContent className=\"sm:max-w-md border-border\">\n <DialogHeader>\n <DialogTitle>Add floor</DialogTitle>\n <DialogDescription>\n Create a new floor with its own resources and 2D layout.\n </DialogDescription>\n </DialogHeader>\n <div className=\"py-1\">\n <Label htmlFor=\"new-floor-name\">Name</Label>\n <Input\n id=\"new-floor-name\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder=\"e.g. Floor 2\"\n className=\"mt-2 h-9\"\n autoFocus\n onKeyDown={(e) => {\n if (e.key === \"Enter\") handleAdd();\n }}\n />\n </div>\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => setAddOpen(false)}>\n Cancel\n </Button>\n <Button type=\"button\" onClick={handleAdd}>\n Add floor\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n ) : null}\n </>\n );\n}\n"]}
@@ -0,0 +1,52 @@
1
+ import * as react_hook_form from 'react-hook-form';
2
+ import { FieldValues, UseFormReturn, UseFormProps } from 'react-hook-form';
3
+ import { z } from 'zod';
4
+ import { A as ApiClientError } from './errors-D1niseaa.js';
5
+
6
+ interface SubmitWithApiOptions<TFieldValues extends FieldValues> {
7
+ /**
8
+ * Called before the default per-`kind` handling below. Return `true` if you fully handled the
9
+ * error yourself (e.g. mapped a 409 "email already registered" onto the `email` field) - this
10
+ * skips the default handling. Return `false`/`undefined` to fall back to it.
11
+ */
12
+ onApiError?: (error: ApiClientError, form: UseFormReturn<TFieldValues>) => boolean | void;
13
+ }
14
+ /**
15
+ * Wraps `react-hook-form` + the zod resolver, and gives `submitWithApi` a default error-handling
16
+ * policy per `ApiClientError.kind`:
17
+ *
18
+ * - client-side validation (bad email, missing field) never reaches here - `zodResolver` blocks the
19
+ * submit and populates per-field errors before `onValid` runs.
20
+ * - "network" (server unreachable) -> root error, generic "can't reach the server" wording.
21
+ * - "validation" (server-side 400 with field-mappable issues) -> mapped onto matching fields by name;
22
+ * anything that can't be matched falls back to a root error.
23
+ * - "http" (401/403/404/409/500, etc.) -> root error by default; not reliably field-mappable from the
24
+ * backend's response shape, so a specific form can opt in via `onApiError` (e.g. map a register 409
25
+ * onto the `email` field).
26
+ *
27
+ * A 401 caused by an expired session on an *authenticated* call has already been resolved one layer
28
+ * down, inside `createApiClient`'s response interceptor (silent refresh-and-retry) - by the time an
29
+ * error reaches here, refresh already failed and the app-level `onAuthExpired` callback already fired,
30
+ * so this hook doesn't need auth-specific branching of its own.
31
+ */
32
+ declare function useApiForm<TSchema extends z.ZodType>(schema: TSchema, options?: UseFormProps<z.infer<TSchema>>): {
33
+ submitWithApi: (onValid: (values: z.infer<TSchema>) => Promise<void>, submitOptions?: SubmitWithApiOptions<z.infer<TSchema>>) => (e?: React.BaseSyntheticEvent) => Promise<void>;
34
+ watch: react_hook_form.UseFormWatch<z.TypeOf<TSchema>>;
35
+ getValues: react_hook_form.UseFormGetValues<z.TypeOf<TSchema>>;
36
+ getFieldState: react_hook_form.UseFormGetFieldState<z.TypeOf<TSchema>>;
37
+ setError: react_hook_form.UseFormSetError<z.TypeOf<TSchema>>;
38
+ clearErrors: react_hook_form.UseFormClearErrors<z.TypeOf<TSchema>>;
39
+ setValue: react_hook_form.UseFormSetValue<z.TypeOf<TSchema>>;
40
+ trigger: react_hook_form.UseFormTrigger<z.TypeOf<TSchema>>;
41
+ formState: react_hook_form.FormState<z.TypeOf<TSchema>>;
42
+ resetField: react_hook_form.UseFormResetField<z.TypeOf<TSchema>>;
43
+ reset: react_hook_form.UseFormReset<z.TypeOf<TSchema>>;
44
+ handleSubmit: react_hook_form.UseFormHandleSubmit<z.TypeOf<TSchema>, z.TypeOf<TSchema>>;
45
+ unregister: react_hook_form.UseFormUnregister<z.TypeOf<TSchema>>;
46
+ control: react_hook_form.Control<z.TypeOf<TSchema>, any, z.TypeOf<TSchema>>;
47
+ register: react_hook_form.UseFormRegister<z.TypeOf<TSchema>>;
48
+ setFocus: react_hook_form.UseFormSetFocus<z.TypeOf<TSchema>>;
49
+ subscribe: react_hook_form.UseFromSubscribe<z.TypeOf<TSchema>>;
50
+ };
51
+
52
+ export { type SubmitWithApiOptions, useApiForm };
package/dist/forms.js ADDED
@@ -0,0 +1,66 @@
1
+ import { zodResolver } from '@hookform/resolvers/zod';
2
+ import { useForm } from 'react-hook-form';
3
+ import 'axios';
4
+ import 'zod';
5
+
6
+ // src/forms/useApiForm.ts
7
+ var ApiClientError = class _ApiClientError extends Error {
8
+ kind;
9
+ statusCode;
10
+ details;
11
+ fieldErrors;
12
+ constructor(kind, message, options = {}) {
13
+ super(message);
14
+ this.name = "ApiClientError";
15
+ this.kind = kind;
16
+ this.statusCode = options.statusCode;
17
+ this.details = options.details;
18
+ this.fieldErrors = options.fieldErrors;
19
+ Object.setPrototypeOf(this, _ApiClientError.prototype);
20
+ }
21
+ };
22
+ function isApiClientError(err) {
23
+ return err instanceof ApiClientError;
24
+ }
25
+
26
+ // src/forms/useApiForm.ts
27
+ function useApiForm(schema, options) {
28
+ const form = useForm({ resolver: zodResolver(schema), ...options });
29
+ const submitWithApi = (onValid, submitOptions) => form.handleSubmit(async (values) => {
30
+ try {
31
+ await onValid(values);
32
+ } catch (err) {
33
+ if (!isApiClientError(err)) throw err;
34
+ if (submitOptions?.onApiError?.(err, form)) return;
35
+ switch (err.kind) {
36
+ case "network":
37
+ form.setError("root", { type: "network", message: err.message });
38
+ break;
39
+ case "validation": {
40
+ const knownFields = new Set(Object.keys(form.getValues()));
41
+ let mappedAny = false;
42
+ for (const fieldError of err.fieldErrors ?? []) {
43
+ if (knownFields.has(fieldError.path)) {
44
+ form.setError(fieldError.path, {
45
+ type: "server",
46
+ message: fieldError.message
47
+ });
48
+ mappedAny = true;
49
+ }
50
+ }
51
+ if (!mappedAny) form.setError("root", { type: "server", message: err.message });
52
+ break;
53
+ }
54
+ case "http":
55
+ default:
56
+ form.setError("root", { type: "server", message: err.message });
57
+ break;
58
+ }
59
+ }
60
+ });
61
+ return { ...form, submitWithApi };
62
+ }
63
+
64
+ export { useApiForm };
65
+ //# sourceMappingURL=forms.js.map
66
+ //# sourceMappingURL=forms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/api/errors.ts","../src/forms/useApiForm.ts"],"names":[],"mappings":";;;;;;AAwBO,IAAM,cAAA,GAAN,MAAM,eAAA,SAAuB,KAAA,CAAM;AAAA,EAC/B,IAAA;AAAA,EACA,UAAA;AAAA,EACA,OAAA;AAAA,EACA,WAAA;AAAA,EAET,WAAA,CAAY,IAAA,EAA0B,OAAA,EAAiB,OAAA,GAAiC,EAAC,EAAG;AAC1F,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,gBAAA;AACZ,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AACZ,IAAA,IAAA,CAAK,aAAa,OAAA,CAAQ,UAAA;AAC1B,IAAA,IAAA,CAAK,UAAU,OAAA,CAAQ,OAAA;AACvB,IAAA,IAAA,CAAK,cAAc,OAAA,CAAQ,WAAA;AAC3B,IAAA,MAAA,CAAO,cAAA,CAAe,IAAA,EAAM,eAAA,CAAe,SAAS,CAAA;AAAA,EACtD;AACF,CAAA;AAEO,SAAS,iBAAiB,GAAA,EAAqC;AACpE,EAAA,OAAO,GAAA,YAAe,cAAA;AACxB;;;ACLO,SAAS,UAAA,CAAsC,QAAiB,OAAA,EAA0C;AAK/G,EAAA,MAAM,IAAA,GAAO,QAA0B,EAAE,QAAA,EAAU,YAAY,MAAe,CAAA,EAAG,GAAG,OAAA,EAAS,CAAA;AAE7F,EAAA,MAAM,gBAAgB,CACpB,OAAA,EACA,kBAEA,IAAA,CAAK,YAAA,CAAa,OAAO,MAAA,KAAW;AAClC,IAAA,IAAI;AACF,MAAA,MAAM,QAAQ,MAAM,CAAA;AAAA,IACtB,SAAS,GAAA,EAAK;AACZ,MAAA,IAAI,CAAC,gBAAA,CAAiB,GAAG,CAAA,EAAG,MAAM,GAAA;AAElC,MAAA,IAAI,aAAA,EAAe,UAAA,GAAa,GAAA,EAAK,IAAI,CAAA,EAAG;AAE5C,MAAA,QAAQ,IAAI,IAAA;AAAM,QAChB,KAAK,SAAA;AACH,UAAA,IAAA,CAAK,QAAA,CAAS,QAAQ,EAAE,IAAA,EAAM,WAAW,OAAA,EAAS,GAAA,CAAI,SAAS,CAAA;AAC/D,UAAA;AAAA,QACF,KAAK,YAAA,EAAc;AACjB,UAAA,MAAM,WAAA,GAAc,IAAI,GAAA,CAAI,MAAA,CAAO,KAAK,IAAA,CAAK,SAAA,EAAW,CAAC,CAAA;AACzD,UAAA,IAAI,SAAA,GAAY,KAAA;AAChB,UAAA,KAAA,MAAW,UAAA,IAAc,GAAA,CAAI,WAAA,IAAe,EAAC,EAAG;AAC9C,YAAA,IAAI,WAAA,CAAY,GAAA,CAAI,UAAA,CAAW,IAAI,CAAA,EAAG;AACpC,cAAA,IAAA,CAAK,QAAA,CAAS,WAAW,IAAA,EAAqC;AAAA,gBAC5D,IAAA,EAAM,QAAA;AAAA,gBACN,SAAS,UAAA,CAAW;AAAA,eACrB,CAAA;AACD,cAAA,SAAA,GAAY,IAAA;AAAA,YACd;AAAA,UACF;AACA,UAAA,IAAI,CAAC,SAAA,EAAW,IAAA,CAAK,QAAA,CAAS,MAAA,EAAQ,EAAE,IAAA,EAAM,QAAA,EAAU,OAAA,EAAS,GAAA,CAAI,OAAA,EAAS,CAAA;AAC9E,UAAA;AAAA,QACF;AAAA,QACA,KAAK,MAAA;AAAA,QACL;AACE,UAAA,IAAA,CAAK,QAAA,CAAS,QAAQ,EAAE,IAAA,EAAM,UAAU,OAAA,EAAS,GAAA,CAAI,SAAS,CAAA;AAC9D,UAAA;AAAA;AACJ,IACF;AAAA,EACF,CAAC,CAAA;AAEH,EAAA,OAAO,EAAE,GAAG,IAAA,EAAM,aAAA,EAAc;AAClC","file":"forms.js","sourcesContent":["import axios from \"axios\";\nimport { z } from \"zod\";\nimport { apiErrorBodySchema } from \"./envelope\";\n\n/**\n * - \"network\": the request never got a response at all (server unreachable, timeout, CORS/preflight failure).\n * - \"validation\": HTTP 400 from our API where `error.details` is the Zod-issues shape produced by\n * `validate.middleware.ts` — field-mappable.\n * - \"http\": every other status our API returns (401, 403, 404, 409, 500, ...). Not field-specific by default.\n */\nexport type ApiClientErrorKind = \"network\" | \"validation\" | \"http\";\n\nexport interface ApiFieldError {\n /** Dot-path, e.g. \"email\" — matched against react-hook-form field names. */\n path: string;\n message: string;\n}\n\nexport interface ApiClientErrorOptions {\n statusCode?: number;\n details?: unknown;\n fieldErrors?: ApiFieldError[];\n}\n\nexport class ApiClientError extends Error {\n readonly kind: ApiClientErrorKind;\n readonly statusCode?: number;\n readonly details?: unknown;\n readonly fieldErrors?: ApiFieldError[];\n\n constructor(kind: ApiClientErrorKind, message: string, options: ApiClientErrorOptions = {}) {\n super(message);\n this.name = \"ApiClientError\";\n this.kind = kind;\n this.statusCode = options.statusCode;\n this.details = options.details;\n this.fieldErrors = options.fieldErrors;\n Object.setPrototypeOf(this, ApiClientError.prototype);\n }\n}\n\nexport function isApiClientError(err: unknown): err is ApiClientError {\n return err instanceof ApiClientError;\n}\n\ninterface ZodLikeIssue {\n path: Array<string | number>;\n message: string;\n}\n\nfunction isZodIssueArray(details: unknown): details is ZodLikeIssue[] {\n return (\n Array.isArray(details) &&\n details.length > 0 &&\n details.every(\n (issue) =>\n typeof issue === \"object\" &&\n issue !== null &&\n Array.isArray((issue as ZodLikeIssue).path) &&\n typeof (issue as ZodLikeIssue).message === \"string\",\n )\n );\n}\n\nfunction toFieldErrors(issues: ZodLikeIssue[]): ApiFieldError[] {\n return issues.map((issue) => ({\n path: issue.path.join(\".\"),\n message: issue.message,\n }));\n}\n\n/**\n * Normalizes anything that can be thrown while making a request (axios error,\n * envelope-shape mismatch, or an unexpected exception) into one `ApiClientError`,\n * classified by `kind` so callers like `useApiForm` can apply a sensible default\n * per failure mode instead of treating a dropped connection the same as a 409.\n */\nexport function normalizeAxiosError(err: unknown): ApiClientError {\n if (isApiClientError(err)) return err;\n\n if (axios.isAxiosError(err)) {\n if (!err.response) {\n return new ApiClientError(\"network\", \"Unable to reach the server. Check your connection and try again.\");\n }\n\n const status = err.response.status;\n const parsedBody = apiErrorBodySchema.safeParse(err.response.data);\n const message = parsedBody.success ? parsedBody.data.error.message : `Request failed with status ${status}`;\n const details = parsedBody.success ? parsedBody.data.error.details : undefined;\n\n if (status === 400 && isZodIssueArray(details)) {\n return new ApiClientError(\"validation\", message, {\n statusCode: status,\n details,\n fieldErrors: toFieldErrors(details),\n });\n }\n\n return new ApiClientError(\"http\", message, { statusCode: status, details });\n }\n\n if (err instanceof z.ZodError) {\n // The server responded 2xx but the payload didn't match the schema we expected -\n // a frontend/backend contract mismatch, not a user-facing form error.\n return new ApiClientError(\"http\", \"Unexpected response shape from the server.\", {\n details: err.issues,\n });\n }\n\n const message = err instanceof Error ? err.message : \"Something went wrong. Please try again.\";\n return new ApiClientError(\"http\", message);\n}\n","import { zodResolver } from \"@hookform/resolvers/zod\";\nimport {\n useForm,\n type FieldPath,\n type FieldValues,\n type UseFormProps,\n type UseFormReturn,\n} from \"react-hook-form\";\nimport type { z } from \"zod\";\nimport { isApiClientError, type ApiClientError } from \"../api/errors\";\n\nexport interface SubmitWithApiOptions<TFieldValues extends FieldValues> {\n /**\n * Called before the default per-`kind` handling below. Return `true` if you fully handled the\n * error yourself (e.g. mapped a 409 \"email already registered\" onto the `email` field) - this\n * skips the default handling. Return `false`/`undefined` to fall back to it.\n */\n onApiError?: (error: ApiClientError, form: UseFormReturn<TFieldValues>) => boolean | void;\n}\n\n/**\n * Wraps `react-hook-form` + the zod resolver, and gives `submitWithApi` a default error-handling\n * policy per `ApiClientError.kind`:\n *\n * - client-side validation (bad email, missing field) never reaches here - `zodResolver` blocks the\n * submit and populates per-field errors before `onValid` runs.\n * - \"network\" (server unreachable) -> root error, generic \"can't reach the server\" wording.\n * - \"validation\" (server-side 400 with field-mappable issues) -> mapped onto matching fields by name;\n * anything that can't be matched falls back to a root error.\n * - \"http\" (401/403/404/409/500, etc.) -> root error by default; not reliably field-mappable from the\n * backend's response shape, so a specific form can opt in via `onApiError` (e.g. map a register 409\n * onto the `email` field).\n *\n * A 401 caused by an expired session on an *authenticated* call has already been resolved one layer\n * down, inside `createApiClient`'s response interceptor (silent refresh-and-retry) - by the time an\n * error reaches here, refresh already failed and the app-level `onAuthExpired` callback already fired,\n * so this hook doesn't need auth-specific branching of its own.\n */\nexport function useApiForm<TSchema extends z.ZodType>(schema: TSchema, options?: UseFormProps<z.infer<TSchema>>) {\n // `zodResolver`'s overloads require a concrete Zod schema type it can pattern-match against\n // (Zod v3's `_def.typeName` vs. v4's `_zod`), which a generic `TSchema extends z.ZodType` can't\n // structurally prove even though it's always a real schema at runtime - this is a documented\n // typing limitation of `@hookform/resolvers`, not unsafe in practice.\n const form = useForm<z.infer<TSchema>>({ resolver: zodResolver(schema as never), ...options });\n\n const submitWithApi = (\n onValid: (values: z.infer<TSchema>) => Promise<void>,\n submitOptions?: SubmitWithApiOptions<z.infer<TSchema>>,\n ) =>\n form.handleSubmit(async (values) => {\n try {\n await onValid(values);\n } catch (err) {\n if (!isApiClientError(err)) throw err; // programmer bug - don't swallow, let it surface normally\n\n if (submitOptions?.onApiError?.(err, form)) return; // caller fully handled it\n\n switch (err.kind) {\n case \"network\":\n form.setError(\"root\", { type: \"network\", message: err.message });\n break;\n case \"validation\": {\n const knownFields = new Set(Object.keys(form.getValues()));\n let mappedAny = false;\n for (const fieldError of err.fieldErrors ?? []) {\n if (knownFields.has(fieldError.path)) {\n form.setError(fieldError.path as FieldPath<z.infer<TSchema>>, {\n type: \"server\",\n message: fieldError.message,\n });\n mappedAny = true;\n }\n }\n if (!mappedAny) form.setError(\"root\", { type: \"server\", message: err.message });\n break;\n }\n case \"http\":\n default:\n form.setError(\"root\", { type: \"server\", message: err.message });\n break;\n }\n }\n });\n\n return { ...form, submitWithApi };\n}\n"]}