rita-workspace 0.5.21 → 0.5.22

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.
package/README.md CHANGED
@@ -5,11 +5,13 @@ Multi-drawing workspace feature for Rita (Excalidraw fork based on B310-digital/
5
5
  ## Features
6
6
 
7
7
  - **Multiple drawings** - Create and manage multiple drawings in one workspace
8
- - **Menu integration** - Seamlessly integrates with Excalidraw's hamburger menu
8
+ - **Folders** - Organize drawings in folders
9
9
  - **Auto-save** - All drawings saved locally in IndexedDB
10
- - **Auto-sync** - Automatic sync between workspace and Excalidraw canvas
11
- - **Rename & delete** - Full drawing management via dialog
10
+ - **Multi-tab conflict detection** - Prevents data loss when same drawing is open in multiple tabs
11
+ - **Workspace toggle** - Preview feature that can be enabled/disabled per browser tab
12
+ - **Export/Import** - Export workspace as JSON, import `.excalidraw` files
12
13
  - **i18n support** - Swedish and English with automatic Excalidraw language sync
14
+ - **Optimized loading** - DB pre-warming and parallel initialization
13
15
 
14
16
  ## Installation
15
17
 
@@ -21,127 +23,103 @@ yarn add rita-workspace
21
23
 
22
24
  ## Integration Guide
23
25
 
24
- Three files need to be modified in the B310/Excalidraw fork:
25
-
26
- ### 1. `excalidraw-app/App.tsx` - Add Provider and Bridge
27
-
28
- **Add imports:**
26
+ ### 1. `App.tsx` - Add Provider
29
27
 
30
28
  ```tsx
31
- import { WorkspaceProvider, WorkspaceBridge } from "rita-workspace";
29
+ import { WorkspaceProvider, useWorkspace, DrawingsDialog } from "rita-workspace";
30
+
31
+ const ExcalidrawApp = () => (
32
+ <WorkspaceProvider lang="sv">
33
+ <ExcalidrawWrapper />
34
+ </WorkspaceProvider>
35
+ );
32
36
  ```
33
37
 
34
- **Wrap ExcalidrawApp with WorkspaceProvider:**
38
+ ### 2. Use workspace in your component
35
39
 
36
40
  ```tsx
37
- const ExcalidrawApp = () => {
38
- return (
39
- <TopErrorBoundary>
40
- <Provider store={appJotaiStore}>
41
- <WorkspaceProvider lang="sv"> {/* <-- Add this */}
42
- <ExcalidrawWrapper />
43
- </WorkspaceProvider> {/* <-- And this */}
44
- </Provider>
45
- </TopErrorBoundary>
46
- );
41
+ const ExcalidrawWrapper = () => {
42
+ const {
43
+ activeDrawing,
44
+ saveCurrentDrawing,
45
+ saveDrawingById,
46
+ isDrawingConflict,
47
+ } = useWorkspace();
48
+
49
+ // Load drawing into canvas when activeDrawing changes
50
+ useEffect(() => {
51
+ if (!excalidrawAPI || !activeDrawing) return;
52
+ excalidrawAPI.updateScene({
53
+ elements: activeDrawing.elements || [],
54
+ appState: activeDrawing.appState || {},
55
+ });
56
+ }, [activeDrawing?.id]);
57
+
58
+ // Auto-save on canvas changes (debounced)
59
+ const onChange = (elements, appState, files) => {
60
+ if (activeDrawing && !isDrawingConflict) {
61
+ saveCurrentDrawing(elements, { viewBackgroundColor: appState.viewBackgroundColor }, files);
62
+ }
63
+ };
47
64
  };
48
65
  ```
49
66
 
50
- **Add WorkspaceBridge inside ExcalidrawWrapper** (this syncs the canvas automatically):
67
+ ### 3. Add DrawingsDialog for management UI
51
68
 
52
69
  ```tsx
53
- const ExcalidrawWrapper = () => {
54
- const [excalidrawAPI, excalidrawRefCallback] =
55
- useCallbackRefState<ExcalidrawImperativeAPI>();
56
-
57
- // ... existing code ...
58
-
59
- return (
60
- <div style={{ height: "100%" }}>
61
- {/* === ADD THIS - Auto-syncs workspace with Excalidraw === */}
62
- <WorkspaceBridge excalidrawAPI={excalidrawAPI} />
63
-
64
- <Excalidraw
65
- excalidrawAPI={excalidrawRefCallback}
66
- // ... rest of props ...
67
- />
68
- </div>
69
- );
70
- };
70
+ const [showDialog, setShowDialog] = useState(false);
71
+
72
+ <DrawingsDialog
73
+ open={showDialog}
74
+ onClose={() => setShowDialog(false)}
75
+ onDrawingSelect={() => setShowDialog(false)}
76
+ renderThumbnail={(drawing) => <DrawingThumbnail drawing={drawing} />}
77
+ />
71
78
  ```
72
79
 
73
- ### 2. `excalidraw-app/components/AppMainMenu.tsx` - Add Menu Items
80
+ ## Multi-Tab Conflict Detection
74
81
 
75
- **Add imports:**
82
+ When the same drawing is open in multiple browser tabs, the workspace automatically detects this and makes the later tab **read-only** to prevent data loss.
76
83
 
77
- ```tsx
78
- import React, { useState } from "react";
84
+ ### How it works
79
85
 
80
- import { WorkspaceMenuItems, DrawingsDialog } from "rita-workspace";
81
- import { LoadIcon } from "../components/icons"; // Excalidraw's folder icon
82
- ```
86
+ 1. Each tab registers itself with a unique `TAB_ID` in `localStorage`
87
+ 2. When a drawing is opened, the tab records which drawing it has and when it opened it
88
+ 3. If another tab already has the same drawing open (opened earlier), `isDrawingConflict` becomes `true`
89
+ 4. The conflicted tab is read-only — `saveCurrentDrawing` and `saveDrawingById` silently skip saves
90
+ 5. When the first tab closes or switches to another drawing, the conflict resolves automatically
83
91
 
84
- **Add state and menu items:**
92
+ ### External conflict check
85
93
 
86
94
  ```tsx
87
- export const AppMainMenu: React.FC<{...}> = React.memo((props) => {
88
- const [showDrawingsDialog, setShowDrawingsDialog] = useState(false);
89
-
90
- return (
91
- <>
92
- <MainMenu>
93
- <MainMenu.DefaultItems.LoadScene />
94
- <MainMenu.DefaultItems.SaveToActiveFile />
95
-
96
- {/* === RITA WORKSPACE === */}
97
- <MainMenu.Sub>
98
- <MainMenu.Sub.Trigger>{LoadIcon} Arbetsyta</MainMenu.Sub.Trigger>
99
- <MainMenu.Sub.Content>
100
- <WorkspaceMenuItems
101
- onManageDrawings={() => setShowDrawingsDialog(true)}
102
- />
103
- </MainMenu.Sub.Content>
104
- </MainMenu.Sub>
105
-
106
- <MainMenu.DefaultItems.Export />
107
- {/* ... rest of menu items ... */}
108
- </MainMenu>
109
-
110
- <DrawingsDialog
111
- open={showDrawingsDialog}
112
- onClose={() => setShowDrawingsDialog(false)}
113
- />
114
- </>
115
- );
116
- });
95
+ import { isDrawingOpenedEarlierInOtherTab } from "rita-workspace";
96
+
97
+ // Returns true if another tab opened this drawing before the current tab
98
+ if (isDrawingOpenedEarlierInOtherTab(drawingId)) {
99
+ // Don't save — another tab owns this drawing
100
+ }
117
101
  ```
118
102
 
119
- ## How It Works
103
+ ### Communication between tabs
120
104
 
121
- 1. **WorkspaceProvider** - Manages workspace state (drawings list, active drawing)
122
- 2. **WorkspaceBridge** - Automatically syncs between workspace and Excalidraw:
123
- - Loads drawing into canvas when you switch drawings
124
- - Auto-saves canvas changes back to workspace
125
- - Saves current drawing before switching to another
126
- 3. **WorkspaceMenuItems** - Provides the menu UI for switching drawings
127
- 4. **DrawingsDialog** - Full management UI (rename, delete, create)
105
+ - **BroadcastChannel** (`rita-workspace-tabs`) instant notification when tabs open/close/switch drawings
106
+ - **localStorage** (`rita-workspace-tabs`) persistent tab registry, backup for BroadcastChannel
107
+ - **Stale tab cleanup** on mount, pings other tabs via BroadcastChannel and removes entries that don't respond
128
108
 
129
- ## Language Support (i18n)
109
+ ## Workspace Toggle (Preview Feature)
130
110
 
131
- Pass Excalidraw's `langCode` to `WorkspaceProvider`:
111
+ The workspace can be enabled/disabled per browser tab using `sessionStorage`:
132
112
 
133
113
  ```tsx
134
- const [langCode] = useAppLangCode();
135
-
136
- <WorkspaceProvider lang={langCode}>
137
- {/* All components automatically use the same language */}
138
- </WorkspaceProvider>
114
+ // Each tab reads its own toggle state
115
+ const [workspaceEnabled] = useState(() =>
116
+ sessionStorage.getItem("rita-workspace-enabled") === "true"
117
+ );
139
118
  ```
140
119
 
141
- | Code | Language |
142
- |------|----------|
143
- | `sv`, `sv-SE` | 🇸🇪 Swedish |
144
- | `en`, `en-US` | 🇬🇧 English (default) |
120
+ - Default: **off** (each new tab starts without workspace)
121
+ - State stored in `sessionStorage` (not shared between tabs)
122
+ - When disabled: auto-save to workspace skipped, drawing-switch disabled, footer hidden
145
123
 
146
124
  ## API Reference
147
125
 
@@ -149,71 +127,96 @@ const [langCode] = useAppLangCode();
149
127
 
150
128
  | Component | Description |
151
129
  |-----------|-------------|
152
- | `WorkspaceProvider` | React context provider. Props: `lang` |
153
- | `WorkspaceBridge` | Auto-syncs workspace ↔ Excalidraw. Props: `excalidrawAPI`, `autoSaveInterval` |
154
- | `WorkspaceMenuItems` | Menu items for MainMenu. Props: `onManageDrawings`, `lang` |
155
- | `DrawingsDialog` | Management dialog. Props: `open`, `onClose`, `lang` |
130
+ | `WorkspaceProvider` | React context provider. Props: `lang`, `children` |
131
+ | `DrawingsDialog` | Management dialog. Props: `open`, `onClose`, `onDrawingSelect`, `renderThumbnail` |
156
132
 
157
133
  ### Hooks
158
134
 
159
- | Hook | Description |
160
- |------|-------------|
161
- | `useWorkspace()` | Access workspace state and actions |
162
- | `useWorkspaceLang()` | Get current language and translations |
135
+ | Hook | Returns |
136
+ |------|---------|
137
+ | `useWorkspace()` | Full workspace state and actions |
138
+ | `useWorkspaceLang()` | `{ lang, t }` — current language and translations |
139
+
140
+ ### Exported functions
141
+
142
+ | Function | Description |
143
+ |----------|-------------|
144
+ | `isDrawingOpenedEarlierInOtherTab(id)` | Check if another tab has this drawing open |
145
+ | `warmDB()` | Pre-warm IndexedDB connection (called automatically at import) |
163
146
 
164
147
  ### useWorkspace() returns
165
148
 
166
149
  ```tsx
167
150
  const {
168
151
  // State
169
- drawings, // Drawing[] - all drawings
170
- activeDrawing, // Drawing | null - currently active
152
+ workspace, // Workspace | null
153
+ drawings, // Drawing[]
154
+ folders, // Folder[]
155
+ activeDrawing, // Drawing | null
171
156
  isLoading, // boolean
172
157
  error, // string | null
173
- lang, // string - current language code
174
- t, // Translations object
175
-
176
- // Actions
177
- createNewDrawing, // (name?: string) => Promise<Drawing>
178
- switchDrawing, // (id: string) => Promise<void>
179
- renameDrawing, // (id: string, name: string) => Promise<void>
180
- removeDrawing, // (id: string) => Promise<void>
158
+ isDrawingConflict, // boolean — true if read-only (another tab has this drawing)
159
+ lang, // string
160
+ t, // Translations
161
+
162
+ // Drawing actions
163
+ createNewDrawing, // (name?, folderId?) => Promise<Drawing | null>
164
+ switchDrawing, // (id) => Promise<void>
165
+ renameDrawing, // (id, name) => Promise<void>
166
+ removeDrawing, // (id) => Promise<void>
167
+ duplicateCurrentDrawing, // () => Promise<Drawing | null>
168
+
169
+ // Folder actions
170
+ createFolder, // (name) => Promise<Folder | null>
171
+ renameFolder, // (id, name) => Promise<void>
172
+ deleteFolder, // (id) => Promise<void>
173
+ moveDrawingToFolder, // (drawingId, folderId) => Promise<void>
174
+
175
+ // Save (blocked if drawing is in conflict)
181
176
  saveCurrentDrawing, // (elements, appState, files?) => Promise<void>
177
+ saveDrawingById, // (id, elements, appState, files?) => Promise<void>
178
+
179
+ // Utilities
180
+ refreshDrawings, // () => Promise<void>
181
+ exportWorkspace, // () => Promise<void>
182
+ importWorkspace, // () => Promise<void>
183
+ exportDrawingAsExcalidraw, // (id) => Promise<void>
184
+ importExcalidrawFile, // () => Promise<void>
182
185
  } = useWorkspace();
183
186
  ```
184
187
 
185
- ### WorkspaceBridge Props
186
-
187
- ```tsx
188
- <WorkspaceBridge
189
- excalidrawAPI={excalidrawAPI} // Required - from Excalidraw
190
- autoSaveInterval={2000} // Optional - ms between saves (default: 2000)
191
- onDrawingLoad={(id) => {}} // Optional - called when drawing loads
192
- onDrawingSave={(id) => {}} // Optional - called when drawing saves
193
- />
194
- ```
195
-
196
188
  ## Data Storage
197
189
 
198
- Drawings are stored in IndexedDB:
190
+ Drawings are stored in **IndexedDB** (`rita-workspace` database, version 2):
199
191
 
200
192
  ```typescript
201
193
  interface Drawing {
202
- id: string;
194
+ id: string; // nanoid
203
195
  name: string;
204
- elements: ExcalidrawElement[];
196
+ folderId: string | null;
197
+ elements: unknown[]; // Excalidraw elements
205
198
  appState: Record<string, unknown>;
206
- files: Record<string, unknown>;
199
+ files: Record<string, unknown>; // Image files
207
200
  createdAt: number;
208
201
  updatedAt: number;
209
202
  }
210
203
  ```
211
204
 
212
- ## Links
205
+ ## Language Support
206
+
207
+ | Code | Language |
208
+ |------|----------|
209
+ | `sv`, `sv-SE` | Swedish |
210
+ | `en`, `en-US` | English (default) |
213
211
 
214
- - **npm:** https://www.npmjs.com/package/rita-workspace
215
- - **B310 Excalidraw Fork:** https://github.com/b310-digital/excalidraw
216
- - **Original Excalidraw:** https://github.com/excalidraw/excalidraw
212
+ ## Development
213
+
214
+ ```bash
215
+ yarn build # Build with tsup (cjs + esm + dts)
216
+ yarn dev # Watch mode
217
+ yarn test # Run tests with vitest
218
+ yarn typecheck # TypeScript check
219
+ ```
217
220
 
218
221
  ## License
219
222
 
package/dist/index.d.mts CHANGED
@@ -138,7 +138,7 @@ interface WorkspaceContextValue {
138
138
  isDrawingConflict: boolean;
139
139
  lang: string;
140
140
  t: Translations;
141
- createNewDrawing: (name?: string, folderId?: string | null) => Promise<Drawing | null>;
141
+ createNewDrawing: (name?: string, folderId?: string | null, activate?: boolean) => Promise<Drawing | null>;
142
142
  switchDrawing: (id: string) => Promise<void>;
143
143
  renameDrawing: (id: string, name: string) => Promise<void>;
144
144
  removeDrawing: (id: string) => Promise<void>;
@@ -153,6 +153,7 @@ interface WorkspaceContextValue {
153
153
  exportWorkspace: () => Promise<void>;
154
154
  importWorkspace: () => Promise<void>;
155
155
  exportDrawingAsExcalidraw: (id: string) => Promise<void>;
156
+ exportAllDrawingsAsExcalidraw: () => Promise<void>;
156
157
  importExcalidrawFile: () => Promise<void>;
157
158
  }
158
159
  declare function useWorkspace(): WorkspaceContextValue;
package/dist/index.d.ts CHANGED
@@ -138,7 +138,7 @@ interface WorkspaceContextValue {
138
138
  isDrawingConflict: boolean;
139
139
  lang: string;
140
140
  t: Translations;
141
- createNewDrawing: (name?: string, folderId?: string | null) => Promise<Drawing | null>;
141
+ createNewDrawing: (name?: string, folderId?: string | null, activate?: boolean) => Promise<Drawing | null>;
142
142
  switchDrawing: (id: string) => Promise<void>;
143
143
  renameDrawing: (id: string, name: string) => Promise<void>;
144
144
  removeDrawing: (id: string) => Promise<void>;
@@ -153,6 +153,7 @@ interface WorkspaceContextValue {
153
153
  exportWorkspace: () => Promise<void>;
154
154
  importWorkspace: () => Promise<void>;
155
155
  exportDrawingAsExcalidraw: (id: string) => Promise<void>;
156
+ exportAllDrawingsAsExcalidraw: () => Promise<void>;
156
157
  importExcalidrawFile: () => Promise<void>;
157
158
  }
158
159
  declare function useWorkspace(): WorkspaceContextValue;
package/dist/index.js CHANGED
@@ -434,6 +434,14 @@ function useWorkspaceLang() {
434
434
  var TAB_ID = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2);
435
435
  var TABS_KEY = "rita-workspace-tabs";
436
436
  var TAB_CHANNEL = "rita-workspace-tabs";
437
+ function broadcastWorkspaceChange() {
438
+ try {
439
+ const channel = new BroadcastChannel(TAB_CHANNEL);
440
+ channel.postMessage({ type: "workspace-changed", tabId: TAB_ID });
441
+ channel.close();
442
+ } catch {
443
+ }
444
+ }
437
445
  var tabsMapCache = null;
438
446
  var tabsMapRaw = null;
439
447
  function getTabsMap() {
@@ -562,6 +570,8 @@ function WorkspaceProvider({ children, lang = "en" }) {
562
570
  channel.onmessage = (event) => {
563
571
  if (event.data?.type === "ping") {
564
572
  channel?.postMessage({ type: "pong", tabId: TAB_ID });
573
+ } else if (event.data?.type === "workspace-changed" && event.data?.tabId !== TAB_ID) {
574
+ refreshDrawingsRef.current();
565
575
  }
566
576
  };
567
577
  } catch {
@@ -699,11 +709,16 @@ function WorkspaceProvider({ children, lang = "en" }) {
699
709
  const refreshDrawings = (0, import_react.useCallback)(async () => {
700
710
  if (!workspace) return;
701
711
  try {
702
- const [allDrawings, allFolders] = await Promise.all([
712
+ const [freshWorkspace, allDrawings, allFolders] = await Promise.all([
713
+ getWorkspace(workspace.id),
703
714
  getAllDrawings(),
704
715
  getAllFolders()
705
716
  ]);
706
- const wsDrawings = allDrawings.filter((d) => workspace.drawingIds.includes(d.id));
717
+ const drawingIds = freshWorkspace?.drawingIds || workspace.drawingIds;
718
+ const wsDrawings = allDrawings.filter((d) => drawingIds.includes(d.id));
719
+ if (freshWorkspace) {
720
+ setWorkspace((prev) => prev ? { ...freshWorkspace, activeDrawingId: prev.activeDrawingId } : freshWorkspace);
721
+ }
707
722
  setDrawings(wsDrawings);
708
723
  setFolders(allFolders);
709
724
  } catch (err) {
@@ -715,7 +730,9 @@ function WorkspaceProvider({ children, lang = "en" }) {
715
730
  foldersRef.current = folders;
716
731
  const activeDrawingIdRef = (0, import_react.useRef)(activeDrawing?.id ?? null);
717
732
  activeDrawingIdRef.current = activeDrawing?.id ?? null;
718
- const createNewDrawing = (0, import_react.useCallback)(async (name, folderId) => {
733
+ const refreshDrawingsRef = (0, import_react.useRef)(refreshDrawings);
734
+ refreshDrawingsRef.current = refreshDrawings;
735
+ const createNewDrawing = (0, import_react.useCallback)(async (name, folderId, activate = true) => {
719
736
  if (!workspace) return null;
720
737
  const now = Date.now();
721
738
  const tempId = `temp-${now}`;
@@ -732,19 +749,24 @@ function WorkspaceProvider({ children, lang = "en" }) {
732
749
  updatedAt: now
733
750
  };
734
751
  setDrawings((prev) => [...prev, tempDrawing]);
735
- setActiveDrawing2(tempDrawing);
736
- sessionStorage.setItem("rita-workspace-tab-drawing", tempId);
752
+ if (activate) {
753
+ setActiveDrawing2(tempDrawing);
754
+ sessionStorage.setItem("rita-workspace-tab-drawing", tempId);
755
+ }
737
756
  try {
738
757
  const drawing = await createDrawing(name || defaultName, [], {}, folderId);
739
758
  await addDrawingToWorkspace(workspace.id, drawing.id);
740
759
  setDrawings((prev) => prev.map((d) => d.id === tempId ? drawing : d));
741
- setActiveDrawing2(drawing);
760
+ if (activate) {
761
+ setActiveDrawing2(drawing);
762
+ sessionStorage.setItem("rita-workspace-tab-drawing", drawing.id);
763
+ }
742
764
  setWorkspace((prev) => prev ? {
743
765
  ...prev,
744
766
  drawingIds: [...prev.drawingIds, drawing.id],
745
- activeDrawingId: drawing.id
767
+ ...activate ? { activeDrawingId: drawing.id } : {}
746
768
  } : null);
747
- sessionStorage.setItem("rita-workspace-tab-drawing", drawing.id);
769
+ broadcastWorkspaceChange();
748
770
  return drawing;
749
771
  } catch (err) {
750
772
  setDrawings((prev) => prev.filter((d) => d.id !== tempId));
@@ -772,6 +794,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
772
794
  }
773
795
  try {
774
796
  await updateDrawing(id, { name });
797
+ broadcastWorkspaceChange();
775
798
  } catch (err) {
776
799
  refreshDrawings();
777
800
  setError(err instanceof Error ? err.message : "Failed to rename drawing");
@@ -793,6 +816,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
793
816
  if (updatedWorkspace) {
794
817
  setWorkspace(updatedWorkspace);
795
818
  }
819
+ broadcastWorkspaceChange();
796
820
  } catch (err) {
797
821
  if (removedDrawing) {
798
822
  setDrawings((prev) => [...prev, removedDrawing]);
@@ -823,6 +847,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
823
847
  ...prev,
824
848
  drawingIds: [...prev.drawingIds, duplicate.id]
825
849
  } : null);
850
+ broadcastWorkspaceChange();
826
851
  return duplicate;
827
852
  }
828
853
  setDrawings((prev) => prev.filter((d) => d.id !== tempId));
@@ -954,6 +979,36 @@ function WorkspaceProvider({ children, lang = "en" }) {
954
979
  setError(err instanceof Error ? err.message : "Failed to export drawing");
955
980
  }
956
981
  }, [drawings]);
982
+ const exportAllDrawingsAsExcalidraw = (0, import_react.useCallback)(async () => {
983
+ try {
984
+ const all = await getAllDrawings();
985
+ const wsDrawings = workspace ? all.filter((d) => workspace.drawingIds.includes(d.id)) : all;
986
+ for (let i = 0; i < wsDrawings.length; i++) {
987
+ const drawing = wsDrawings[i];
988
+ const excalidrawData = {
989
+ type: "excalidraw",
990
+ version: 2,
991
+ source: "rita-workspace",
992
+ elements: drawing.elements || [],
993
+ appState: { viewBackgroundColor: "#ffffff", ...drawing.appState || {} },
994
+ files: drawing.files || {}
995
+ };
996
+ const blob = new Blob([JSON.stringify(excalidrawData, null, 2)], { type: "application/json" });
997
+ const url = URL.createObjectURL(blob);
998
+ const a = document.createElement("a");
999
+ a.href = url;
1000
+ const safeName = (drawing.name || "ritning").replace(/[\\/:*?"<>|]/g, "_");
1001
+ a.download = `${String(i + 1).padStart(2, "0")}_${safeName}.excalidraw`;
1002
+ document.body.appendChild(a);
1003
+ a.click();
1004
+ document.body.removeChild(a);
1005
+ URL.revokeObjectURL(url);
1006
+ await new Promise((r) => setTimeout(r, 150));
1007
+ }
1008
+ } catch (err) {
1009
+ setError(err instanceof Error ? err.message : "Failed to export all drawings");
1010
+ }
1011
+ }, [workspace]);
957
1012
  const importExcalidrawFile = (0, import_react.useCallback)(async () => {
958
1013
  if (!workspace) return;
959
1014
  try {
@@ -1068,6 +1123,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
1068
1123
  exportWorkspace,
1069
1124
  importWorkspace,
1070
1125
  exportDrawingAsExcalidraw,
1126
+ exportAllDrawingsAsExcalidraw,
1071
1127
  importExcalidrawFile
1072
1128
  };
1073
1129
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WorkspaceContext.Provider, { value, children });
@@ -1432,6 +1488,7 @@ var DrawingsDialog = ({
1432
1488
  exportWorkspace,
1433
1489
  importWorkspace,
1434
1490
  exportDrawingAsExcalidraw,
1491
+ exportAllDrawingsAsExcalidraw,
1435
1492
  importExcalidrawFile,
1436
1493
  refreshDrawings,
1437
1494
  t: contextT,
@@ -1515,9 +1572,8 @@ var DrawingsDialog = ({
1515
1572
  setSwitchingId(null);
1516
1573
  }, [switchDrawing, onDrawingSelect]);
1517
1574
  const handleCreate = (0, import_react5.useCallback)(async (folderId) => {
1518
- const newDrawing = await createNewDrawing(void 0, folderId);
1519
- if (newDrawing) onDrawingSelect?.(newDrawing);
1520
- }, [createNewDrawing, onDrawingSelect]);
1575
+ await createNewDrawing(void 0, folderId, false);
1576
+ }, [createNewDrawing]);
1521
1577
  const handleStartEdit = (0, import_react5.useCallback)((drawing) => {
1522
1578
  setEditingId(drawing.id);
1523
1579
  setEditName(drawing.name);
@@ -2244,6 +2300,15 @@ var DrawingsDialog = ({
2244
2300
  }
2245
2301
  }
2246
2302
  ),
2303
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2304
+ ActionButton,
2305
+ {
2306
+ icon: "\u{1F4E6}",
2307
+ label: "Exportera alla som .excalidraw",
2308
+ description: "Laddar ner varje ritning som separat fil",
2309
+ onClick: exportAllDrawingsAsExcalidraw
2310
+ }
2311
+ ),
2247
2312
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2248
2313
  ActionButton,
2249
2314
  {
package/dist/index.mjs CHANGED
@@ -363,6 +363,14 @@ function useWorkspaceLang() {
363
363
  var TAB_ID = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2);
364
364
  var TABS_KEY = "rita-workspace-tabs";
365
365
  var TAB_CHANNEL = "rita-workspace-tabs";
366
+ function broadcastWorkspaceChange() {
367
+ try {
368
+ const channel = new BroadcastChannel(TAB_CHANNEL);
369
+ channel.postMessage({ type: "workspace-changed", tabId: TAB_ID });
370
+ channel.close();
371
+ } catch {
372
+ }
373
+ }
366
374
  var tabsMapCache = null;
367
375
  var tabsMapRaw = null;
368
376
  function getTabsMap() {
@@ -491,6 +499,8 @@ function WorkspaceProvider({ children, lang = "en" }) {
491
499
  channel.onmessage = (event) => {
492
500
  if (event.data?.type === "ping") {
493
501
  channel?.postMessage({ type: "pong", tabId: TAB_ID });
502
+ } else if (event.data?.type === "workspace-changed" && event.data?.tabId !== TAB_ID) {
503
+ refreshDrawingsRef.current();
494
504
  }
495
505
  };
496
506
  } catch {
@@ -628,11 +638,16 @@ function WorkspaceProvider({ children, lang = "en" }) {
628
638
  const refreshDrawings = useCallback(async () => {
629
639
  if (!workspace) return;
630
640
  try {
631
- const [allDrawings, allFolders] = await Promise.all([
641
+ const [freshWorkspace, allDrawings, allFolders] = await Promise.all([
642
+ getWorkspace(workspace.id),
632
643
  getAllDrawings(),
633
644
  getAllFolders()
634
645
  ]);
635
- const wsDrawings = allDrawings.filter((d) => workspace.drawingIds.includes(d.id));
646
+ const drawingIds = freshWorkspace?.drawingIds || workspace.drawingIds;
647
+ const wsDrawings = allDrawings.filter((d) => drawingIds.includes(d.id));
648
+ if (freshWorkspace) {
649
+ setWorkspace((prev) => prev ? { ...freshWorkspace, activeDrawingId: prev.activeDrawingId } : freshWorkspace);
650
+ }
636
651
  setDrawings(wsDrawings);
637
652
  setFolders(allFolders);
638
653
  } catch (err) {
@@ -644,7 +659,9 @@ function WorkspaceProvider({ children, lang = "en" }) {
644
659
  foldersRef.current = folders;
645
660
  const activeDrawingIdRef = useRef(activeDrawing?.id ?? null);
646
661
  activeDrawingIdRef.current = activeDrawing?.id ?? null;
647
- const createNewDrawing = useCallback(async (name, folderId) => {
662
+ const refreshDrawingsRef = useRef(refreshDrawings);
663
+ refreshDrawingsRef.current = refreshDrawings;
664
+ const createNewDrawing = useCallback(async (name, folderId, activate = true) => {
648
665
  if (!workspace) return null;
649
666
  const now = Date.now();
650
667
  const tempId = `temp-${now}`;
@@ -661,19 +678,24 @@ function WorkspaceProvider({ children, lang = "en" }) {
661
678
  updatedAt: now
662
679
  };
663
680
  setDrawings((prev) => [...prev, tempDrawing]);
664
- setActiveDrawing2(tempDrawing);
665
- sessionStorage.setItem("rita-workspace-tab-drawing", tempId);
681
+ if (activate) {
682
+ setActiveDrawing2(tempDrawing);
683
+ sessionStorage.setItem("rita-workspace-tab-drawing", tempId);
684
+ }
666
685
  try {
667
686
  const drawing = await createDrawing(name || defaultName, [], {}, folderId);
668
687
  await addDrawingToWorkspace(workspace.id, drawing.id);
669
688
  setDrawings((prev) => prev.map((d) => d.id === tempId ? drawing : d));
670
- setActiveDrawing2(drawing);
689
+ if (activate) {
690
+ setActiveDrawing2(drawing);
691
+ sessionStorage.setItem("rita-workspace-tab-drawing", drawing.id);
692
+ }
671
693
  setWorkspace((prev) => prev ? {
672
694
  ...prev,
673
695
  drawingIds: [...prev.drawingIds, drawing.id],
674
- activeDrawingId: drawing.id
696
+ ...activate ? { activeDrawingId: drawing.id } : {}
675
697
  } : null);
676
- sessionStorage.setItem("rita-workspace-tab-drawing", drawing.id);
698
+ broadcastWorkspaceChange();
677
699
  return drawing;
678
700
  } catch (err) {
679
701
  setDrawings((prev) => prev.filter((d) => d.id !== tempId));
@@ -701,6 +723,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
701
723
  }
702
724
  try {
703
725
  await updateDrawing(id, { name });
726
+ broadcastWorkspaceChange();
704
727
  } catch (err) {
705
728
  refreshDrawings();
706
729
  setError(err instanceof Error ? err.message : "Failed to rename drawing");
@@ -722,6 +745,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
722
745
  if (updatedWorkspace) {
723
746
  setWorkspace(updatedWorkspace);
724
747
  }
748
+ broadcastWorkspaceChange();
725
749
  } catch (err) {
726
750
  if (removedDrawing) {
727
751
  setDrawings((prev) => [...prev, removedDrawing]);
@@ -752,6 +776,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
752
776
  ...prev,
753
777
  drawingIds: [...prev.drawingIds, duplicate.id]
754
778
  } : null);
779
+ broadcastWorkspaceChange();
755
780
  return duplicate;
756
781
  }
757
782
  setDrawings((prev) => prev.filter((d) => d.id !== tempId));
@@ -883,6 +908,36 @@ function WorkspaceProvider({ children, lang = "en" }) {
883
908
  setError(err instanceof Error ? err.message : "Failed to export drawing");
884
909
  }
885
910
  }, [drawings]);
911
+ const exportAllDrawingsAsExcalidraw = useCallback(async () => {
912
+ try {
913
+ const all = await getAllDrawings();
914
+ const wsDrawings = workspace ? all.filter((d) => workspace.drawingIds.includes(d.id)) : all;
915
+ for (let i = 0; i < wsDrawings.length; i++) {
916
+ const drawing = wsDrawings[i];
917
+ const excalidrawData = {
918
+ type: "excalidraw",
919
+ version: 2,
920
+ source: "rita-workspace",
921
+ elements: drawing.elements || [],
922
+ appState: { viewBackgroundColor: "#ffffff", ...drawing.appState || {} },
923
+ files: drawing.files || {}
924
+ };
925
+ const blob = new Blob([JSON.stringify(excalidrawData, null, 2)], { type: "application/json" });
926
+ const url = URL.createObjectURL(blob);
927
+ const a = document.createElement("a");
928
+ a.href = url;
929
+ const safeName = (drawing.name || "ritning").replace(/[\\/:*?"<>|]/g, "_");
930
+ a.download = `${String(i + 1).padStart(2, "0")}_${safeName}.excalidraw`;
931
+ document.body.appendChild(a);
932
+ a.click();
933
+ document.body.removeChild(a);
934
+ URL.revokeObjectURL(url);
935
+ await new Promise((r) => setTimeout(r, 150));
936
+ }
937
+ } catch (err) {
938
+ setError(err instanceof Error ? err.message : "Failed to export all drawings");
939
+ }
940
+ }, [workspace]);
886
941
  const importExcalidrawFile = useCallback(async () => {
887
942
  if (!workspace) return;
888
943
  try {
@@ -997,6 +1052,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
997
1052
  exportWorkspace,
998
1053
  importWorkspace,
999
1054
  exportDrawingAsExcalidraw,
1055
+ exportAllDrawingsAsExcalidraw,
1000
1056
  importExcalidrawFile
1001
1057
  };
1002
1058
  return /* @__PURE__ */ jsx(WorkspaceContext.Provider, { value, children });
@@ -1361,6 +1417,7 @@ var DrawingsDialog = ({
1361
1417
  exportWorkspace,
1362
1418
  importWorkspace,
1363
1419
  exportDrawingAsExcalidraw,
1420
+ exportAllDrawingsAsExcalidraw,
1364
1421
  importExcalidrawFile,
1365
1422
  refreshDrawings,
1366
1423
  t: contextT,
@@ -1444,9 +1501,8 @@ var DrawingsDialog = ({
1444
1501
  setSwitchingId(null);
1445
1502
  }, [switchDrawing, onDrawingSelect]);
1446
1503
  const handleCreate = useCallback2(async (folderId) => {
1447
- const newDrawing = await createNewDrawing(void 0, folderId);
1448
- if (newDrawing) onDrawingSelect?.(newDrawing);
1449
- }, [createNewDrawing, onDrawingSelect]);
1504
+ await createNewDrawing(void 0, folderId, false);
1505
+ }, [createNewDrawing]);
1450
1506
  const handleStartEdit = useCallback2((drawing) => {
1451
1507
  setEditingId(drawing.id);
1452
1508
  setEditName(drawing.name);
@@ -2173,6 +2229,15 @@ var DrawingsDialog = ({
2173
2229
  }
2174
2230
  }
2175
2231
  ),
2232
+ /* @__PURE__ */ jsx6(
2233
+ ActionButton,
2234
+ {
2235
+ icon: "\u{1F4E6}",
2236
+ label: "Exportera alla som .excalidraw",
2237
+ description: "Laddar ner varje ritning som separat fil",
2238
+ onClick: exportAllDrawingsAsExcalidraw
2239
+ }
2240
+ ),
2176
2241
  /* @__PURE__ */ jsx6(
2177
2242
  ActionButton,
2178
2243
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rita-workspace",
3
- "version": "0.5.21",
3
+ "version": "0.5.22",
4
4
  "description": "Multi-drawing workspace feature for Rita (Excalidraw fork)",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",