@spacesync/client 0.1.5 → 0.1.7

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.
@@ -1,4 +1,4 @@
1
- import { ResponsiveDialog, ResponsiveDialogContent, ResponsiveDialogTitle } from './chunk-3DI67RMP.js';
1
+ import { ResponsiveDialog, ResponsiveDialogContent, ResponsiveDialogTitle } from './chunk-H3CNZCYU.js';
2
2
  import './chunk-RV5PMZYZ.js';
3
3
  import './chunk-L65F4EMK.js';
4
4
  import './chunk-PBNBLCC2.js';
@@ -9,9 +9,9 @@ import './chunk-LUB7QWGU.js';
9
9
  import { Button } from './chunk-VHINHHZQ.js';
10
10
  import './chunk-FCY5FYJB.js';
11
11
  import './chunk-OWNUMPUC.js';
12
- import './chunk-DMPSQ22Q.js';
12
+ import { cn } from './chunk-DMPSQ22Q.js';
13
13
  import { useState, useRef, useEffect, useMemo } from 'react';
14
- import { Minus, Plus, RotateCcw, Download, ExternalLink, FileText } from 'lucide-react';
14
+ import { Minus, Plus, RotateCcw, Download, ExternalLink, Loader2, FileText } from 'lucide-react';
15
15
  import { jsx, jsxs } from 'react/jsx-runtime';
16
16
 
17
17
  function FileViewerDialog({
@@ -21,10 +21,12 @@ function FileViewerDialog({
21
21
  }) {
22
22
  const [zoom, setZoom] = useState(1);
23
23
  const [dragging, setDragging] = useState(false);
24
+ const [imageStatus, setImageStatus] = useState("loading");
24
25
  const scrollRef = useRef(null);
25
26
  const dragStateRef = useRef(null);
26
27
  useEffect(() => {
27
28
  if (open) setZoom(1);
29
+ setImageStatus("loading");
28
30
  }, [open, file?.url]);
29
31
  const kind = useMemo(() => {
30
32
  if (!file) return "other";
@@ -146,21 +148,38 @@ function FileViewerDialog({
146
148
  onPointerMove: handleImagePointerMove,
147
149
  onPointerUp: handleImagePointerEnd,
148
150
  onPointerCancel: handleImagePointerEnd,
149
- children: /* @__PURE__ */ jsx(
150
- "img",
151
- {
152
- src: file.url,
153
- alt: file.name,
154
- className: "block shrink-0 select-none rounded-lg border border-border bg-card shadow-sm",
155
- draggable: false,
156
- style: {
157
- width: `${zoom * 100}%`,
158
- maxWidth: "none",
159
- cursor: isDraggable ? dragging ? "grabbing" : "grab" : "default",
160
- touchAction: isDraggable ? "none" : "auto"
151
+ children: /* @__PURE__ */ jsxs("div", { className: "relative flex min-h-[240px] w-full items-center justify-center", children: [
152
+ imageStatus !== "ready" ? /* @__PURE__ */ jsx(
153
+ "div",
154
+ {
155
+ className: "absolute inset-0 z-10 flex items-center justify-center",
156
+ role: "status",
157
+ "aria-live": "polite",
158
+ "aria-label": imageStatus === "error" ? "Couldn't load image" : "Loading image",
159
+ children: imageStatus === "error" ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Couldn't load image" }) : /* @__PURE__ */ jsx(Loader2, { size: 28, className: "animate-spin text-muted-foreground" })
161
160
  }
162
- }
163
- )
161
+ ) : null,
162
+ /* @__PURE__ */ jsx(
163
+ "img",
164
+ {
165
+ src: file.url,
166
+ alt: file.name,
167
+ className: cn(
168
+ "block shrink-0 select-none rounded-lg border border-border bg-card shadow-sm",
169
+ imageStatus !== "ready" && "opacity-0"
170
+ ),
171
+ draggable: false,
172
+ onLoad: () => setImageStatus("ready"),
173
+ onError: () => setImageStatus("error"),
174
+ style: {
175
+ width: `${zoom * 100}%`,
176
+ maxWidth: "none",
177
+ cursor: isDraggable ? dragging ? "grabbing" : "grab" : "default",
178
+ touchAction: isDraggable ? "none" : "auto"
179
+ }
180
+ }
181
+ )
182
+ ] })
164
183
  }
165
184
  ) }) : kind === "pdf" ? /* @__PURE__ */ jsx("object", { data: file.url, type: "application/pdf", className: "h-full w-full", children: /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col items-center justify-center gap-3 p-6 text-center", children: [
166
185
  /* @__PURE__ */ jsx(FileText, { size: 34, className: "text-muted-foreground" }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/files/file-viewer-dialog.tsx"],"names":[],"mappings":"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","file":"file-viewer-dialog.js","sourcesContent":["import { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Download, ExternalLink, FileText, Minus, Plus, RotateCcw } from \"lucide-react\";\nimport {\n Button,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogTitle,\n} from \"../ui\";\n\nexport interface FileViewerFile {\n name: string;\n mimeType: string;\n sizeLabel?: string;\n url: string;\n}\n\nexport function FileViewerDialog({\n file,\n open,\n onOpenChange,\n}: {\n file: FileViewerFile | null;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n}) {\n const [zoom, setZoom] = useState(1);\n const [dragging, setDragging] = useState(false);\n const scrollRef = useRef<HTMLDivElement | null>(null);\n const dragStateRef = useRef<{\n pointerId: number;\n startX: number;\n startY: number;\n scrollLeft: number;\n scrollTop: number;\n } | null>(null);\n\n useEffect(() => {\n if (open) setZoom(1);\n }, [open, file?.url]);\n\n const kind = useMemo(() => {\n if (!file) return \"other\";\n if (file.mimeType.startsWith(\"image/\")) return \"image\";\n if (file.mimeType === \"application/pdf\" || file.name.toLowerCase().endsWith(\".pdf\")) return \"pdf\";\n return \"other\";\n }, [file]);\n\n const zoomPercent = Math.round(zoom * 100);\n const isDraggable = kind === \"image\" && zoom > 1;\n\n const handleImagePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {\n if (!isDraggable || !scrollRef.current) return;\n event.preventDefault();\n dragStateRef.current = {\n pointerId: event.pointerId,\n startX: event.clientX,\n startY: event.clientY,\n scrollLeft: scrollRef.current.scrollLeft,\n scrollTop: scrollRef.current.scrollTop,\n };\n setDragging(true);\n event.currentTarget.setPointerCapture(event.pointerId);\n };\n\n const handleImagePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {\n const state = dragStateRef.current;\n if (!state || state.pointerId !== event.pointerId || !scrollRef.current) return;\n scrollRef.current.scrollLeft = state.scrollLeft - (event.clientX - state.startX);\n scrollRef.current.scrollTop = state.scrollTop - (event.clientY - state.startY);\n };\n\n const handleImagePointerEnd = (event: React.PointerEvent<HTMLDivElement>) => {\n const state = dragStateRef.current;\n if (!state || state.pointerId !== event.pointerId) return;\n dragStateRef.current = null;\n setDragging(false);\n if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n event.currentTarget.releasePointerCapture(event.pointerId);\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"flex h-[min(88vh,900px)] w-[min(96vw,1100px)] max-w-none flex-col gap-0 overflow-hidden rounded-xl border-border bg-card p-0\">\n <div className=\"flex shrink-0 flex-col gap-3 border-b border-border py-3 pl-4 pr-14 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"min-w-0 flex-1\">\n <ResponsiveDialogTitle className=\"truncate text-base font-semibold text-foreground\">\n {file?.name ?? \"File preview\"}\n </ResponsiveDialogTitle>\n {file?.sizeLabel ? (\n <p className=\"mt-1 text-xs text-muted-foreground\">{file.sizeLabel}</p>\n ) : null}\n </div>\n {file ? (\n <div className=\"flex shrink-0 flex-wrap items-center gap-1.5 lg:justify-end\">\n {kind === \"image\" ? (\n <div className=\"flex items-center gap-0.5 rounded-lg border border-border bg-background p-0.5\">\n <button\n type=\"button\"\n className=\"inline-flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40\"\n onClick={() => setZoom((value) => Math.max(0.25, Number((value - 0.25).toFixed(2))))}\n disabled={zoom <= 0.25}\n aria-label=\"Zoom out\"\n >\n <Minus size={14} />\n </button>\n <span className=\"min-w-11 text-center text-xs font-medium tabular-nums text-muted-foreground\">\n {zoomPercent}%\n </span>\n <button\n type=\"button\"\n className=\"inline-flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40\"\n onClick={() => setZoom((value) => Math.min(4, Number((value + 0.25).toFixed(2))))}\n disabled={zoom >= 4}\n aria-label=\"Zoom in\"\n >\n <Plus size={14} />\n </button>\n <button\n type=\"button\"\n className=\"inline-flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground\"\n onClick={() => setZoom(1)}\n aria-label=\"Reset zoom\"\n >\n <RotateCcw size={14} />\n </button>\n </div>\n ) : null}\n <Button\n asChild\n variant=\"ghost\"\n size=\"icon\"\n className=\"size-9\"\n title=\"Download\"\n >\n <a href={file.url} download={file.name}>\n <Download size={14} />\n <span className=\"sr-only\">Download</span>\n </a>\n </Button>\n <Button\n asChild\n variant=\"ghost\"\n size=\"icon\"\n className=\"size-9\"\n title=\"Open in new tab\"\n >\n <a href={file.url} target=\"_blank\" rel=\"noreferrer\">\n <ExternalLink size={14} />\n <span className=\"sr-only\">Open in new tab</span>\n </a>\n </Button>\n </div>\n ) : null}\n </div>\n <div className=\"min-h-0 flex-1 overflow-hidden bg-muted/60\">\n {!file ? (\n <div className=\"flex h-full items-center justify-center text-sm text-muted-foreground\">\n No file selected.\n </div>\n ) : kind === \"image\" ? (\n <div ref={scrollRef} className=\"h-full overflow-auto p-4\">\n <div\n className=\"flex min-h-full items-center justify-center\"\n style={{ minWidth: `${zoom * 100}%` }}\n onPointerDown={handleImagePointerDown}\n onPointerMove={handleImagePointerMove}\n onPointerUp={handleImagePointerEnd}\n onPointerCancel={handleImagePointerEnd}\n >\n <img\n src={file.url}\n alt={file.name}\n className=\"block shrink-0 select-none rounded-lg border border-border bg-card shadow-sm\"\n draggable={false}\n style={{\n width: `${zoom * 100}%`,\n maxWidth: \"none\",\n cursor: isDraggable ? (dragging ? \"grabbing\" : \"grab\") : \"default\",\n touchAction: isDraggable ? \"none\" : \"auto\",\n }}\n />\n </div>\n </div>\n ) : kind === \"pdf\" ? (\n <object data={file.url} type=\"application/pdf\" className=\"h-full w-full\">\n <div className=\"flex h-full flex-col items-center justify-center gap-3 p-6 text-center\">\n <FileText size={34} className=\"text-muted-foreground\" />\n <p className=\"max-w-md text-sm text-muted-foreground\">\n This browser cannot preview the PDF inline. You can still download or open it in a new tab.\n </p>\n </div>\n </object>\n ) : (\n <div className=\"flex h-full flex-col items-center justify-center gap-3 p-6 text-center\">\n <FileText size={34} className=\"text-muted-foreground\" />\n <p className=\"max-w-md text-sm text-muted-foreground\">\n Preview is not available for this file type. Download or open it in a new tab.\n </p>\n </div>\n )}\n </div>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/files/file-viewer-dialog.tsx"],"names":[],"mappings":"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","file":"file-viewer-dialog.js","sourcesContent":["import { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Download, ExternalLink, FileText, Loader2, Minus, Plus, RotateCcw } from \"lucide-react\";\nimport {\n Button,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogTitle,\n cn,\n} from \"../ui\";\n\nexport interface FileViewerFile {\n name: string;\n mimeType: string;\n sizeLabel?: string;\n url: string;\n}\n\nexport function FileViewerDialog({\n file,\n open,\n onOpenChange,\n}: {\n file: FileViewerFile | null;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n}) {\n const [zoom, setZoom] = useState(1);\n const [dragging, setDragging] = useState(false);\n const [imageStatus, setImageStatus] = useState<\"loading\" | \"ready\" | \"error\">(\"loading\");\n const scrollRef = useRef<HTMLDivElement | null>(null);\n const dragStateRef = useRef<{\n pointerId: number;\n startX: number;\n startY: number;\n scrollLeft: number;\n scrollTop: number;\n } | null>(null);\n\n useEffect(() => {\n if (open) setZoom(1);\n setImageStatus(\"loading\");\n }, [open, file?.url]);\n\n const kind = useMemo(() => {\n if (!file) return \"other\";\n if (file.mimeType.startsWith(\"image/\")) return \"image\";\n if (file.mimeType === \"application/pdf\" || file.name.toLowerCase().endsWith(\".pdf\")) return \"pdf\";\n return \"other\";\n }, [file]);\n\n const zoomPercent = Math.round(zoom * 100);\n const isDraggable = kind === \"image\" && zoom > 1;\n\n const handleImagePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {\n if (!isDraggable || !scrollRef.current) return;\n event.preventDefault();\n dragStateRef.current = {\n pointerId: event.pointerId,\n startX: event.clientX,\n startY: event.clientY,\n scrollLeft: scrollRef.current.scrollLeft,\n scrollTop: scrollRef.current.scrollTop,\n };\n setDragging(true);\n event.currentTarget.setPointerCapture(event.pointerId);\n };\n\n const handleImagePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {\n const state = dragStateRef.current;\n if (!state || state.pointerId !== event.pointerId || !scrollRef.current) return;\n scrollRef.current.scrollLeft = state.scrollLeft - (event.clientX - state.startX);\n scrollRef.current.scrollTop = state.scrollTop - (event.clientY - state.startY);\n };\n\n const handleImagePointerEnd = (event: React.PointerEvent<HTMLDivElement>) => {\n const state = dragStateRef.current;\n if (!state || state.pointerId !== event.pointerId) return;\n dragStateRef.current = null;\n setDragging(false);\n if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n event.currentTarget.releasePointerCapture(event.pointerId);\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"flex h-[min(88vh,900px)] w-[min(96vw,1100px)] max-w-none flex-col gap-0 overflow-hidden rounded-xl border-border bg-card p-0\">\n <div className=\"flex shrink-0 flex-col gap-3 border-b border-border py-3 pl-4 pr-14 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"min-w-0 flex-1\">\n <ResponsiveDialogTitle className=\"truncate text-base font-semibold text-foreground\">\n {file?.name ?? \"File preview\"}\n </ResponsiveDialogTitle>\n {file?.sizeLabel ? (\n <p className=\"mt-1 text-xs text-muted-foreground\">{file.sizeLabel}</p>\n ) : null}\n </div>\n {file ? (\n <div className=\"flex shrink-0 flex-wrap items-center gap-1.5 lg:justify-end\">\n {kind === \"image\" ? (\n <div className=\"flex items-center gap-0.5 rounded-lg border border-border bg-background p-0.5\">\n <button\n type=\"button\"\n className=\"inline-flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40\"\n onClick={() => setZoom((value) => Math.max(0.25, Number((value - 0.25).toFixed(2))))}\n disabled={zoom <= 0.25}\n aria-label=\"Zoom out\"\n >\n <Minus size={14} />\n </button>\n <span className=\"min-w-11 text-center text-xs font-medium tabular-nums text-muted-foreground\">\n {zoomPercent}%\n </span>\n <button\n type=\"button\"\n className=\"inline-flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40\"\n onClick={() => setZoom((value) => Math.min(4, Number((value + 0.25).toFixed(2))))}\n disabled={zoom >= 4}\n aria-label=\"Zoom in\"\n >\n <Plus size={14} />\n </button>\n <button\n type=\"button\"\n className=\"inline-flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground\"\n onClick={() => setZoom(1)}\n aria-label=\"Reset zoom\"\n >\n <RotateCcw size={14} />\n </button>\n </div>\n ) : null}\n <Button\n asChild\n variant=\"ghost\"\n size=\"icon\"\n className=\"size-9\"\n title=\"Download\"\n >\n <a href={file.url} download={file.name}>\n <Download size={14} />\n <span className=\"sr-only\">Download</span>\n </a>\n </Button>\n <Button\n asChild\n variant=\"ghost\"\n size=\"icon\"\n className=\"size-9\"\n title=\"Open in new tab\"\n >\n <a href={file.url} target=\"_blank\" rel=\"noreferrer\">\n <ExternalLink size={14} />\n <span className=\"sr-only\">Open in new tab</span>\n </a>\n </Button>\n </div>\n ) : null}\n </div>\n <div className=\"min-h-0 flex-1 overflow-hidden bg-muted/60\">\n {!file ? (\n <div className=\"flex h-full items-center justify-center text-sm text-muted-foreground\">\n No file selected.\n </div>\n ) : kind === \"image\" ? (\n <div ref={scrollRef} className=\"h-full overflow-auto p-4\">\n <div\n className=\"flex min-h-full items-center justify-center\"\n style={{ minWidth: `${zoom * 100}%` }}\n onPointerDown={handleImagePointerDown}\n onPointerMove={handleImagePointerMove}\n onPointerUp={handleImagePointerEnd}\n onPointerCancel={handleImagePointerEnd}\n >\n <div className=\"relative flex min-h-[240px] w-full items-center justify-center\">\n {imageStatus !== \"ready\" ? (\n <div\n className=\"absolute inset-0 z-10 flex items-center justify-center\"\n role=\"status\"\n aria-live=\"polite\"\n aria-label={imageStatus === \"error\" ? \"Couldn't load image\" : \"Loading image\"}\n >\n {imageStatus === \"error\" ? (\n <p className=\"text-sm text-muted-foreground\">Couldn't load image</p>\n ) : (\n <Loader2 size={28} className=\"animate-spin text-muted-foreground\" />\n )}\n </div>\n ) : null}\n <img\n src={file.url}\n alt={file.name}\n className={cn(\n \"block shrink-0 select-none rounded-lg border border-border bg-card shadow-sm\",\n imageStatus !== \"ready\" && \"opacity-0\",\n )}\n draggable={false}\n onLoad={() => setImageStatus(\"ready\")}\n onError={() => setImageStatus(\"error\")}\n style={{\n width: `${zoom * 100}%`,\n maxWidth: \"none\",\n cursor: isDraggable ? (dragging ? \"grabbing\" : \"grab\") : \"default\",\n touchAction: isDraggable ? \"none\" : \"auto\",\n }}\n />\n </div>\n </div>\n </div>\n ) : kind === \"pdf\" ? (\n <object data={file.url} type=\"application/pdf\" className=\"h-full w-full\">\n <div className=\"flex h-full flex-col items-center justify-center gap-3 p-6 text-center\">\n <FileText size={34} className=\"text-muted-foreground\" />\n <p className=\"max-w-md text-sm text-muted-foreground\">\n This browser cannot preview the PDF inline. You can still download or open it in a new tab.\n </p>\n </div>\n </object>\n ) : (\n <div className=\"flex h-full flex-col items-center justify-center gap-3 p-6 text-center\">\n <FileText size={34} className=\"text-muted-foreground\" />\n <p className=\"max-w-md text-sm text-muted-foreground\">\n Preview is not available for this file type. Download or open it in a new tab.\n </p>\n </div>\n )}\n </div>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"]}
@@ -4,11 +4,12 @@ interface FloorSwitcherFloor {
4
4
  id: string;
5
5
  name: string;
6
6
  }
7
- declare function FloorSwitcher({ floors, activeFloorId, onFloorChange, onAddFloor, }: {
7
+ declare function FloorSwitcher({ floors, activeFloorId, onFloorChange, onAddFloor, onDeleteFloor, }: {
8
8
  floors: FloorSwitcherFloor[];
9
9
  activeFloorId: string;
10
10
  onFloorChange: (floorId: string) => void;
11
11
  onAddFloor?: (name: string) => void;
12
+ onDeleteFloor?: (floorId: string) => void;
12
13
  }): React.JSX.Element;
13
14
 
14
15
  export { FloorSwitcher, type FloorSwitcherFloor };
@@ -1,4 +1,4 @@
1
- import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from './chunk-3DI67RMP.js';
1
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from './chunk-H3CNZCYU.js';
2
2
  import { Label } from './chunk-RV5PMZYZ.js';
3
3
  import './chunk-L65F4EMK.js';
4
4
  import './chunk-PBNBLCC2.js';
@@ -10,7 +10,7 @@ import { Button } from './chunk-VHINHHZQ.js';
10
10
  import { shellTopBarMutedIconClass, shellTopBarSecondaryButtonClass } from './chunk-FCY5FYJB.js';
11
11
  import './chunk-OWNUMPUC.js';
12
12
  import { cn } from './chunk-DMPSQ22Q.js';
13
- import { Layers, ChevronDown, Check, Plus } from 'lucide-react';
13
+ import { Layers, ChevronDown, Check, Plus, Trash2 } from 'lucide-react';
14
14
  import { useState, useEffect } from 'react';
15
15
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
16
16
 
@@ -18,11 +18,13 @@ function FloorSwitcher({
18
18
  floors,
19
19
  activeFloorId,
20
20
  onFloorChange,
21
- onAddFloor
21
+ onAddFloor,
22
+ onDeleteFloor
22
23
  }) {
23
24
  const activeFloor = floors.find((f) => f.id === activeFloorId) ?? floors[0];
24
25
  const [addOpen, setAddOpen] = useState(false);
25
26
  const [name, setName] = useState("");
27
+ const canDeleteFloor = Boolean(onDeleteFloor) && floors.length > 1;
26
28
  useEffect(() => {
27
29
  if (addOpen) {
28
30
  setName(`Floor ${floors.length + 1}`);
@@ -63,12 +65,23 @@ function FloorSwitcher({
63
65
  },
64
66
  floor.id
65
67
  )),
66
- onAddFloor ? /* @__PURE__ */ jsxs(Fragment, { children: [
68
+ onAddFloor || canDeleteFloor ? /* @__PURE__ */ jsxs(Fragment, { children: [
67
69
  /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
68
- /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => setAddOpen(true), children: [
70
+ onAddFloor ? /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => setAddOpen(true), children: [
69
71
  /* @__PURE__ */ jsx(Plus, { size: 14, className: "mr-1.5" }),
70
72
  "Add floor\u2026"
71
- ] })
73
+ ] }) : null,
74
+ canDeleteFloor ? /* @__PURE__ */ jsxs(
75
+ DropdownMenuItem,
76
+ {
77
+ variant: "destructive",
78
+ onClick: () => onDeleteFloor?.(activeFloorId),
79
+ children: [
80
+ /* @__PURE__ */ jsx(Trash2, { size: 14, className: "mr-1.5" }),
81
+ "Delete floor\u2026"
82
+ ]
83
+ }
84
+ ) : null
72
85
  ] }) : null
73
86
  ] })
74
87
  ] }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/live-space/floor-switcher.tsx"],"names":[],"mappings":"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","file":"floor-switcher.js","sourcesContent":["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"]}
1
+ {"version":3,"sources":["../src/live-space/floor-switcher.tsx"],"names":[],"mappings":"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","file":"floor-switcher.js","sourcesContent":["import { Check, ChevronDown, Layers, Plus, Trash2 } 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 onDeleteFloor,\n}: {\n floors: FloorSwitcherFloor[];\n activeFloorId: string;\n onFloorChange: (floorId: string) => void;\n onAddFloor?: (name: string) => void;\n onDeleteFloor?: (floorId: 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 const canDeleteFloor = Boolean(onDeleteFloor) && floors.length > 1;\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 || canDeleteFloor ? (\n <>\n <DropdownMenuSeparator />\n {onAddFloor ? (\n <DropdownMenuItem onClick={() => setAddOpen(true)}>\n <Plus size={14} className=\"mr-1.5\" />\n Add floor…\n </DropdownMenuItem>\n ) : null}\n {canDeleteFloor ? (\n <DropdownMenuItem\n variant=\"destructive\"\n onClick={() => onDeleteFloor?.(activeFloorId)}\n >\n <Trash2 size={14} className=\"mr-1.5\" />\n Delete floor…\n </DropdownMenuItem>\n ) : null}\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"]}
@@ -1,7 +1,7 @@
1
1
  import { useTheme } from './chunk-XU3EFSQX.js';
2
2
  import './chunk-4Y5DBM6Z.js';
3
3
  import { LIVE_RESOURCE_TYPE_META } from './chunk-ZIJQ7BXU.js';
4
- import { Tooltip, TooltipTrigger, TooltipContent, Popover, PopoverAnchor, PopoverContent } from './chunk-3DI67RMP.js';
4
+ import { Tooltip, TooltipTrigger, TooltipContent, Popover, PopoverAnchor, PopoverContent } from './chunk-H3CNZCYU.js';
5
5
  import './chunk-RV5PMZYZ.js';
6
6
  import './chunk-L65F4EMK.js';
7
7
  import './chunk-PBNBLCC2.js';
@@ -1,5 +1,5 @@
1
1
  import { FilePickerField } from './chunk-OMSJWTSZ.js';
2
- import { Tooltip, TooltipTrigger, TooltipContent, Textarea } from './chunk-3DI67RMP.js';
2
+ import { Tooltip, TooltipTrigger, TooltipContent, Textarea } from './chunk-H3CNZCYU.js';
3
3
  import './chunk-RV5PMZYZ.js';
4
4
  import './chunk-L65F4EMK.js';
5
5
  import './chunk-PBNBLCC2.js';
package/dist/ui.js CHANGED
@@ -1,4 +1,4 @@
1
- export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, AspectRatio, Avatar, AvatarFallback, AvatarImage, Badge, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Calendar, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent, Checkbox, CheckboxField, Collapsible, CollapsibleContent, CollapsibleTrigger, ColorPicker, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, ContentFade, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, DashFade, DateField, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, FeedSkeleton, FieldLabel, FilterPopover, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, IconInput, InputGroup, InputGroupInput, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, MenubarPortal, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, NumberInput, PasswordInput, PhoneInput, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Progress, ReceiptCard, ReceiptSummaryRow, ResizableHandle, ResizablePanel, ResizablePanelGroup, ResponsiveAlertDialog, ResponsiveAlertDialogAction, ResponsiveAlertDialogCancel, ResponsiveAlertDialogContent, ResponsiveAlertDialogDescription, AlertDialogFooter as ResponsiveAlertDialogFooter, AlertDialogHeader as ResponsiveAlertDialogHeader, ResponsiveAlertDialogTitle, ResponsiveDialog, ResponsiveDialogClose, ResponsiveDialogContent, ResponsiveDialogDescription, ResponsiveDialogTitle, ResponsiveDialogTrigger, ScrollArea, ScrollBar, SegmentedTabList, SegmentedTabTrigger, SelectableCard, SelectableListRow, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, ShellTopBarViewToggle, ShellTopBarViewToggleItem, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, Slider, SmoothSize, Switch, TabCountBadge, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Text, Textarea, TimeField, Toaster, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, badgeVariants, fieldLabelClass, formatTimeForDisplay, navigationMenuTriggerStyle, parseHm, tabsListVariants, tabsTriggerVariants, textVariants, toTimeStr, toggleVariants, useFormField, useIsMobile, useResponsiveDialogDrawer, useSidebar } from './chunk-3DI67RMP.js';
1
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, AspectRatio, Avatar, AvatarFallback, AvatarImage, Badge, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Calendar, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent, Checkbox, CheckboxField, Collapsible, CollapsibleContent, CollapsibleTrigger, ColorPicker, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, ContentFade, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, DashFade, DateField, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, FeedSkeleton, FieldLabel, FilterPopover, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, IconInput, InputGroup, InputGroupInput, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, MenubarPortal, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, NumberInput, PasswordInput, PhoneInput, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Progress, ReceiptCard, ReceiptSummaryRow, ResizableHandle, ResizablePanel, ResizablePanelGroup, ResponsiveAlertDialog, ResponsiveAlertDialogAction, ResponsiveAlertDialogCancel, ResponsiveAlertDialogContent, ResponsiveAlertDialogDescription, AlertDialogFooter as ResponsiveAlertDialogFooter, AlertDialogHeader as ResponsiveAlertDialogHeader, ResponsiveAlertDialogTitle, ResponsiveDialog, ResponsiveDialogClose, ResponsiveDialogContent, ResponsiveDialogDescription, ResponsiveDialogTitle, ResponsiveDialogTrigger, ScrollArea, ScrollBar, SegmentedTabList, SegmentedTabTrigger, SelectableCard, SelectableListRow, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, ShellTopBarViewToggle, ShellTopBarViewToggleItem, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, Slider, SmoothSize, Switch, TabCountBadge, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Text, Textarea, TimeField, Toaster, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, badgeVariants, fieldLabelClass, formatTimeForDisplay, navigationMenuTriggerStyle, parseHm, tabsListVariants, tabsTriggerVariants, textVariants, toTimeStr, toggleVariants, useFormField, useIsMobile, useResponsiveDialogDrawer, useSidebar } from './chunk-H3CNZCYU.js';
2
2
  export { Label, RadioGroup, RadioGroupItem } from './chunk-RV5PMZYZ.js';
3
3
  export { HoverCard, HoverCardContent, HoverCardTrigger } from './chunk-L65F4EMK.js';
4
4
  import './chunk-PBNBLCC2.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spacesync/client",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Reusable SpaceSync React UI, shell, theme, and API client primitives.",
5
5
  "type": "module",
6
6
  "files": [