rita-workspace 0.5.20 → 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,11 +730,14 @@ 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}`;
722
- const defaultName = `${t.newDrawing} ${drawingsRef.current.length + 1}`;
739
+ const allDrawings = await getAllDrawings();
740
+ const defaultName = `${t.newDrawing} ${allDrawings.length + 1}`;
723
741
  const tempDrawing = {
724
742
  id: tempId,
725
743
  name: name || defaultName,
@@ -731,19 +749,24 @@ function WorkspaceProvider({ children, lang = "en" }) {
731
749
  updatedAt: now
732
750
  };
733
751
  setDrawings((prev) => [...prev, tempDrawing]);
734
- setActiveDrawing2(tempDrawing);
735
- sessionStorage.setItem("rita-workspace-tab-drawing", tempId);
752
+ if (activate) {
753
+ setActiveDrawing2(tempDrawing);
754
+ sessionStorage.setItem("rita-workspace-tab-drawing", tempId);
755
+ }
736
756
  try {
737
757
  const drawing = await createDrawing(name || defaultName, [], {}, folderId);
738
758
  await addDrawingToWorkspace(workspace.id, drawing.id);
739
759
  setDrawings((prev) => prev.map((d) => d.id === tempId ? drawing : d));
740
- setActiveDrawing2(drawing);
760
+ if (activate) {
761
+ setActiveDrawing2(drawing);
762
+ sessionStorage.setItem("rita-workspace-tab-drawing", drawing.id);
763
+ }
741
764
  setWorkspace((prev) => prev ? {
742
765
  ...prev,
743
766
  drawingIds: [...prev.drawingIds, drawing.id],
744
- activeDrawingId: drawing.id
767
+ ...activate ? { activeDrawingId: drawing.id } : {}
745
768
  } : null);
746
- sessionStorage.setItem("rita-workspace-tab-drawing", drawing.id);
769
+ broadcastWorkspaceChange();
747
770
  return drawing;
748
771
  } catch (err) {
749
772
  setDrawings((prev) => prev.filter((d) => d.id !== tempId));
@@ -771,6 +794,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
771
794
  }
772
795
  try {
773
796
  await updateDrawing(id, { name });
797
+ broadcastWorkspaceChange();
774
798
  } catch (err) {
775
799
  refreshDrawings();
776
800
  setError(err instanceof Error ? err.message : "Failed to rename drawing");
@@ -792,6 +816,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
792
816
  if (updatedWorkspace) {
793
817
  setWorkspace(updatedWorkspace);
794
818
  }
819
+ broadcastWorkspaceChange();
795
820
  } catch (err) {
796
821
  if (removedDrawing) {
797
822
  setDrawings((prev) => [...prev, removedDrawing]);
@@ -822,6 +847,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
822
847
  ...prev,
823
848
  drawingIds: [...prev.drawingIds, duplicate.id]
824
849
  } : null);
850
+ broadcastWorkspaceChange();
825
851
  return duplicate;
826
852
  }
827
853
  setDrawings((prev) => prev.filter((d) => d.id !== tempId));
@@ -953,6 +979,36 @@ function WorkspaceProvider({ children, lang = "en" }) {
953
979
  setError(err instanceof Error ? err.message : "Failed to export drawing");
954
980
  }
955
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]);
956
1012
  const importExcalidrawFile = (0, import_react.useCallback)(async () => {
957
1013
  if (!workspace) return;
958
1014
  try {
@@ -1067,6 +1123,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
1067
1123
  exportWorkspace,
1068
1124
  importWorkspace,
1069
1125
  exportDrawingAsExcalidraw,
1126
+ exportAllDrawingsAsExcalidraw,
1070
1127
  importExcalidrawFile
1071
1128
  };
1072
1129
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WorkspaceContext.Provider, { value, children });
@@ -1431,6 +1488,7 @@ var DrawingsDialog = ({
1431
1488
  exportWorkspace,
1432
1489
  importWorkspace,
1433
1490
  exportDrawingAsExcalidraw,
1491
+ exportAllDrawingsAsExcalidraw,
1434
1492
  importExcalidrawFile,
1435
1493
  refreshDrawings,
1436
1494
  t: contextT,
@@ -1514,9 +1572,8 @@ var DrawingsDialog = ({
1514
1572
  setSwitchingId(null);
1515
1573
  }, [switchDrawing, onDrawingSelect]);
1516
1574
  const handleCreate = (0, import_react5.useCallback)(async (folderId) => {
1517
- const newDrawing = await createNewDrawing(void 0, folderId);
1518
- if (newDrawing) onDrawingSelect?.(newDrawing);
1519
- }, [createNewDrawing, onDrawingSelect]);
1575
+ await createNewDrawing(void 0, folderId, false);
1576
+ }, [createNewDrawing]);
1520
1577
  const handleStartEdit = (0, import_react5.useCallback)((drawing) => {
1521
1578
  setEditingId(drawing.id);
1522
1579
  setEditName(drawing.name);
@@ -2243,6 +2300,15 @@ var DrawingsDialog = ({
2243
2300
  }
2244
2301
  }
2245
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
+ ),
2246
2312
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2247
2313
  ActionButton,
2248
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,11 +659,14 @@ 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}`;
651
- const defaultName = `${t.newDrawing} ${drawingsRef.current.length + 1}`;
668
+ const allDrawings = await getAllDrawings();
669
+ const defaultName = `${t.newDrawing} ${allDrawings.length + 1}`;
652
670
  const tempDrawing = {
653
671
  id: tempId,
654
672
  name: name || defaultName,
@@ -660,19 +678,24 @@ function WorkspaceProvider({ children, lang = "en" }) {
660
678
  updatedAt: now
661
679
  };
662
680
  setDrawings((prev) => [...prev, tempDrawing]);
663
- setActiveDrawing2(tempDrawing);
664
- sessionStorage.setItem("rita-workspace-tab-drawing", tempId);
681
+ if (activate) {
682
+ setActiveDrawing2(tempDrawing);
683
+ sessionStorage.setItem("rita-workspace-tab-drawing", tempId);
684
+ }
665
685
  try {
666
686
  const drawing = await createDrawing(name || defaultName, [], {}, folderId);
667
687
  await addDrawingToWorkspace(workspace.id, drawing.id);
668
688
  setDrawings((prev) => prev.map((d) => d.id === tempId ? drawing : d));
669
- setActiveDrawing2(drawing);
689
+ if (activate) {
690
+ setActiveDrawing2(drawing);
691
+ sessionStorage.setItem("rita-workspace-tab-drawing", drawing.id);
692
+ }
670
693
  setWorkspace((prev) => prev ? {
671
694
  ...prev,
672
695
  drawingIds: [...prev.drawingIds, drawing.id],
673
- activeDrawingId: drawing.id
696
+ ...activate ? { activeDrawingId: drawing.id } : {}
674
697
  } : null);
675
- sessionStorage.setItem("rita-workspace-tab-drawing", drawing.id);
698
+ broadcastWorkspaceChange();
676
699
  return drawing;
677
700
  } catch (err) {
678
701
  setDrawings((prev) => prev.filter((d) => d.id !== tempId));
@@ -700,6 +723,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
700
723
  }
701
724
  try {
702
725
  await updateDrawing(id, { name });
726
+ broadcastWorkspaceChange();
703
727
  } catch (err) {
704
728
  refreshDrawings();
705
729
  setError(err instanceof Error ? err.message : "Failed to rename drawing");
@@ -721,6 +745,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
721
745
  if (updatedWorkspace) {
722
746
  setWorkspace(updatedWorkspace);
723
747
  }
748
+ broadcastWorkspaceChange();
724
749
  } catch (err) {
725
750
  if (removedDrawing) {
726
751
  setDrawings((prev) => [...prev, removedDrawing]);
@@ -751,6 +776,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
751
776
  ...prev,
752
777
  drawingIds: [...prev.drawingIds, duplicate.id]
753
778
  } : null);
779
+ broadcastWorkspaceChange();
754
780
  return duplicate;
755
781
  }
756
782
  setDrawings((prev) => prev.filter((d) => d.id !== tempId));
@@ -882,6 +908,36 @@ function WorkspaceProvider({ children, lang = "en" }) {
882
908
  setError(err instanceof Error ? err.message : "Failed to export drawing");
883
909
  }
884
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]);
885
941
  const importExcalidrawFile = useCallback(async () => {
886
942
  if (!workspace) return;
887
943
  try {
@@ -996,6 +1052,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
996
1052
  exportWorkspace,
997
1053
  importWorkspace,
998
1054
  exportDrawingAsExcalidraw,
1055
+ exportAllDrawingsAsExcalidraw,
999
1056
  importExcalidrawFile
1000
1057
  };
1001
1058
  return /* @__PURE__ */ jsx(WorkspaceContext.Provider, { value, children });
@@ -1360,6 +1417,7 @@ var DrawingsDialog = ({
1360
1417
  exportWorkspace,
1361
1418
  importWorkspace,
1362
1419
  exportDrawingAsExcalidraw,
1420
+ exportAllDrawingsAsExcalidraw,
1363
1421
  importExcalidrawFile,
1364
1422
  refreshDrawings,
1365
1423
  t: contextT,
@@ -1443,9 +1501,8 @@ var DrawingsDialog = ({
1443
1501
  setSwitchingId(null);
1444
1502
  }, [switchDrawing, onDrawingSelect]);
1445
1503
  const handleCreate = useCallback2(async (folderId) => {
1446
- const newDrawing = await createNewDrawing(void 0, folderId);
1447
- if (newDrawing) onDrawingSelect?.(newDrawing);
1448
- }, [createNewDrawing, onDrawingSelect]);
1504
+ await createNewDrawing(void 0, folderId, false);
1505
+ }, [createNewDrawing]);
1449
1506
  const handleStartEdit = useCallback2((drawing) => {
1450
1507
  setEditingId(drawing.id);
1451
1508
  setEditName(drawing.name);
@@ -2172,6 +2229,15 @@ var DrawingsDialog = ({
2172
2229
  }
2173
2230
  }
2174
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
+ ),
2175
2241
  /* @__PURE__ */ jsx6(
2176
2242
  ActionButton,
2177
2243
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rita-workspace",
3
- "version": "0.5.20",
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",