rita-workspace 0.5.21 → 0.5.23
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 +136 -133
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +109 -13
- package/dist/index.mjs +109 -13
- package/package.json +1 -1
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
|
-
- **
|
|
8
|
+
- **Folders** - Organize drawings in folders
|
|
9
9
|
- **Auto-save** - All drawings saved locally in IndexedDB
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
38
|
+
### 2. Use workspace in your component
|
|
35
39
|
|
|
36
40
|
```tsx
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
67
|
+
### 3. Add DrawingsDialog for management UI
|
|
51
68
|
|
|
52
69
|
```tsx
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
80
|
+
## Multi-Tab Conflict Detection
|
|
74
81
|
|
|
75
|
-
**
|
|
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
|
-
|
|
78
|
-
import React, { useState } from "react";
|
|
84
|
+
### How it works
|
|
79
85
|
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
92
|
+
### External conflict check
|
|
85
93
|
|
|
86
94
|
```tsx
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
103
|
+
### Communication between tabs
|
|
120
104
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
##
|
|
109
|
+
## Workspace Toggle (Preview Feature)
|
|
130
110
|
|
|
131
|
-
|
|
111
|
+
The workspace can be enabled/disabled per browser tab using `sessionStorage`:
|
|
132
112
|
|
|
133
113
|
```tsx
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
| `
|
|
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 |
|
|
160
|
-
|
|
161
|
-
| `useWorkspace()` |
|
|
162
|
-
| `useWorkspaceLang()` |
|
|
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
|
-
|
|
170
|
-
|
|
152
|
+
workspace, // Workspace | null
|
|
153
|
+
drawings, // Drawing[]
|
|
154
|
+
folders, // Folder[]
|
|
155
|
+
activeDrawing, // Drawing | null
|
|
171
156
|
isLoading, // boolean
|
|
172
157
|
error, // string | null
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
205
|
+
## Language Support
|
|
206
|
+
|
|
207
|
+
| Code | Language |
|
|
208
|
+
|------|----------|
|
|
209
|
+
| `sv`, `sv-SE` | Swedish |
|
|
210
|
+
| `en`, `en-US` | English (default) |
|
|
213
211
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
@@ -431,9 +431,30 @@ function useWorkspaceLang() {
|
|
|
431
431
|
}
|
|
432
432
|
return { lang: context.lang, t: context.t };
|
|
433
433
|
}
|
|
434
|
-
var
|
|
434
|
+
var TAB_ID_KEY = "rita-workspace-tab-id";
|
|
435
|
+
var TAB_ID = (() => {
|
|
436
|
+
try {
|
|
437
|
+
const existing = sessionStorage.getItem(TAB_ID_KEY);
|
|
438
|
+
if (existing) return existing;
|
|
439
|
+
} catch {
|
|
440
|
+
}
|
|
441
|
+
const fresh = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2);
|
|
442
|
+
try {
|
|
443
|
+
sessionStorage.setItem(TAB_ID_KEY, fresh);
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
return fresh;
|
|
447
|
+
})();
|
|
435
448
|
var TABS_KEY = "rita-workspace-tabs";
|
|
436
449
|
var TAB_CHANNEL = "rita-workspace-tabs";
|
|
450
|
+
function broadcastWorkspaceChange() {
|
|
451
|
+
try {
|
|
452
|
+
const channel = new BroadcastChannel(TAB_CHANNEL);
|
|
453
|
+
channel.postMessage({ type: "workspace-changed", tabId: TAB_ID });
|
|
454
|
+
channel.close();
|
|
455
|
+
} catch {
|
|
456
|
+
}
|
|
457
|
+
}
|
|
437
458
|
var tabsMapCache = null;
|
|
438
459
|
var tabsMapRaw = null;
|
|
439
460
|
function getTabsMap() {
|
|
@@ -456,16 +477,34 @@ function getTabsMap() {
|
|
|
456
477
|
return {};
|
|
457
478
|
}
|
|
458
479
|
}
|
|
480
|
+
var TAB_ENTRY_KEY = "rita-workspace-tab-entry";
|
|
459
481
|
function setTabDrawing(drawingId) {
|
|
460
482
|
const tabs = getTabsMap();
|
|
461
483
|
if (drawingId) {
|
|
462
484
|
const existing = tabs[TAB_ID];
|
|
485
|
+
let openedAt;
|
|
463
486
|
if (existing && existing.drawingId === drawingId) {
|
|
487
|
+
openedAt = existing.openedAt;
|
|
464
488
|
} else {
|
|
465
|
-
|
|
489
|
+
let sessionEntry = null;
|
|
490
|
+
try {
|
|
491
|
+
const raw = sessionStorage.getItem(TAB_ENTRY_KEY);
|
|
492
|
+
if (raw) sessionEntry = JSON.parse(raw);
|
|
493
|
+
} catch {
|
|
494
|
+
}
|
|
495
|
+
openedAt = sessionEntry && sessionEntry.drawingId === drawingId ? sessionEntry.openedAt : Date.now();
|
|
496
|
+
}
|
|
497
|
+
tabs[TAB_ID] = { drawingId, openedAt };
|
|
498
|
+
try {
|
|
499
|
+
sessionStorage.setItem(TAB_ENTRY_KEY, JSON.stringify(tabs[TAB_ID]));
|
|
500
|
+
} catch {
|
|
466
501
|
}
|
|
467
502
|
} else {
|
|
468
503
|
delete tabs[TAB_ID];
|
|
504
|
+
try {
|
|
505
|
+
sessionStorage.removeItem(TAB_ENTRY_KEY);
|
|
506
|
+
} catch {
|
|
507
|
+
}
|
|
469
508
|
}
|
|
470
509
|
const json = JSON.stringify(tabs);
|
|
471
510
|
localStorage.setItem(TABS_KEY, json);
|
|
@@ -562,6 +601,8 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
562
601
|
channel.onmessage = (event) => {
|
|
563
602
|
if (event.data?.type === "ping") {
|
|
564
603
|
channel?.postMessage({ type: "pong", tabId: TAB_ID });
|
|
604
|
+
} else if (event.data?.type === "workspace-changed" && event.data?.tabId !== TAB_ID) {
|
|
605
|
+
refreshDrawingsRef.current();
|
|
565
606
|
}
|
|
566
607
|
};
|
|
567
608
|
} catch {
|
|
@@ -699,11 +740,16 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
699
740
|
const refreshDrawings = (0, import_react.useCallback)(async () => {
|
|
700
741
|
if (!workspace) return;
|
|
701
742
|
try {
|
|
702
|
-
const [allDrawings, allFolders] = await Promise.all([
|
|
743
|
+
const [freshWorkspace, allDrawings, allFolders] = await Promise.all([
|
|
744
|
+
getWorkspace(workspace.id),
|
|
703
745
|
getAllDrawings(),
|
|
704
746
|
getAllFolders()
|
|
705
747
|
]);
|
|
706
|
-
const
|
|
748
|
+
const drawingIds = freshWorkspace?.drawingIds || workspace.drawingIds;
|
|
749
|
+
const wsDrawings = allDrawings.filter((d) => drawingIds.includes(d.id));
|
|
750
|
+
if (freshWorkspace) {
|
|
751
|
+
setWorkspace((prev) => prev ? { ...freshWorkspace, activeDrawingId: prev.activeDrawingId } : freshWorkspace);
|
|
752
|
+
}
|
|
707
753
|
setDrawings(wsDrawings);
|
|
708
754
|
setFolders(allFolders);
|
|
709
755
|
} catch (err) {
|
|
@@ -715,7 +761,9 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
715
761
|
foldersRef.current = folders;
|
|
716
762
|
const activeDrawingIdRef = (0, import_react.useRef)(activeDrawing?.id ?? null);
|
|
717
763
|
activeDrawingIdRef.current = activeDrawing?.id ?? null;
|
|
718
|
-
const
|
|
764
|
+
const refreshDrawingsRef = (0, import_react.useRef)(refreshDrawings);
|
|
765
|
+
refreshDrawingsRef.current = refreshDrawings;
|
|
766
|
+
const createNewDrawing = (0, import_react.useCallback)(async (name, folderId, activate = true) => {
|
|
719
767
|
if (!workspace) return null;
|
|
720
768
|
const now = Date.now();
|
|
721
769
|
const tempId = `temp-${now}`;
|
|
@@ -732,19 +780,24 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
732
780
|
updatedAt: now
|
|
733
781
|
};
|
|
734
782
|
setDrawings((prev) => [...prev, tempDrawing]);
|
|
735
|
-
|
|
736
|
-
|
|
783
|
+
if (activate) {
|
|
784
|
+
setActiveDrawing2(tempDrawing);
|
|
785
|
+
sessionStorage.setItem("rita-workspace-tab-drawing", tempId);
|
|
786
|
+
}
|
|
737
787
|
try {
|
|
738
788
|
const drawing = await createDrawing(name || defaultName, [], {}, folderId);
|
|
739
789
|
await addDrawingToWorkspace(workspace.id, drawing.id);
|
|
740
790
|
setDrawings((prev) => prev.map((d) => d.id === tempId ? drawing : d));
|
|
741
|
-
|
|
791
|
+
if (activate) {
|
|
792
|
+
setActiveDrawing2(drawing);
|
|
793
|
+
sessionStorage.setItem("rita-workspace-tab-drawing", drawing.id);
|
|
794
|
+
}
|
|
742
795
|
setWorkspace((prev) => prev ? {
|
|
743
796
|
...prev,
|
|
744
797
|
drawingIds: [...prev.drawingIds, drawing.id],
|
|
745
|
-
activeDrawingId: drawing.id
|
|
798
|
+
...activate ? { activeDrawingId: drawing.id } : {}
|
|
746
799
|
} : null);
|
|
747
|
-
|
|
800
|
+
broadcastWorkspaceChange();
|
|
748
801
|
return drawing;
|
|
749
802
|
} catch (err) {
|
|
750
803
|
setDrawings((prev) => prev.filter((d) => d.id !== tempId));
|
|
@@ -772,6 +825,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
772
825
|
}
|
|
773
826
|
try {
|
|
774
827
|
await updateDrawing(id, { name });
|
|
828
|
+
broadcastWorkspaceChange();
|
|
775
829
|
} catch (err) {
|
|
776
830
|
refreshDrawings();
|
|
777
831
|
setError(err instanceof Error ? err.message : "Failed to rename drawing");
|
|
@@ -793,6 +847,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
793
847
|
if (updatedWorkspace) {
|
|
794
848
|
setWorkspace(updatedWorkspace);
|
|
795
849
|
}
|
|
850
|
+
broadcastWorkspaceChange();
|
|
796
851
|
} catch (err) {
|
|
797
852
|
if (removedDrawing) {
|
|
798
853
|
setDrawings((prev) => [...prev, removedDrawing]);
|
|
@@ -823,6 +878,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
823
878
|
...prev,
|
|
824
879
|
drawingIds: [...prev.drawingIds, duplicate.id]
|
|
825
880
|
} : null);
|
|
881
|
+
broadcastWorkspaceChange();
|
|
826
882
|
return duplicate;
|
|
827
883
|
}
|
|
828
884
|
setDrawings((prev) => prev.filter((d) => d.id !== tempId));
|
|
@@ -954,6 +1010,36 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
954
1010
|
setError(err instanceof Error ? err.message : "Failed to export drawing");
|
|
955
1011
|
}
|
|
956
1012
|
}, [drawings]);
|
|
1013
|
+
const exportAllDrawingsAsExcalidraw = (0, import_react.useCallback)(async () => {
|
|
1014
|
+
try {
|
|
1015
|
+
const all = await getAllDrawings();
|
|
1016
|
+
const wsDrawings = workspace ? all.filter((d) => workspace.drawingIds.includes(d.id)) : all;
|
|
1017
|
+
for (let i = 0; i < wsDrawings.length; i++) {
|
|
1018
|
+
const drawing = wsDrawings[i];
|
|
1019
|
+
const excalidrawData = {
|
|
1020
|
+
type: "excalidraw",
|
|
1021
|
+
version: 2,
|
|
1022
|
+
source: "rita-workspace",
|
|
1023
|
+
elements: drawing.elements || [],
|
|
1024
|
+
appState: { viewBackgroundColor: "#ffffff", ...drawing.appState || {} },
|
|
1025
|
+
files: drawing.files || {}
|
|
1026
|
+
};
|
|
1027
|
+
const blob = new Blob([JSON.stringify(excalidrawData, null, 2)], { type: "application/json" });
|
|
1028
|
+
const url = URL.createObjectURL(blob);
|
|
1029
|
+
const a = document.createElement("a");
|
|
1030
|
+
a.href = url;
|
|
1031
|
+
const safeName = (drawing.name || "ritning").replace(/[\\/:*?"<>|]/g, "_");
|
|
1032
|
+
a.download = `${String(i + 1).padStart(2, "0")}_${safeName}.excalidraw`;
|
|
1033
|
+
document.body.appendChild(a);
|
|
1034
|
+
a.click();
|
|
1035
|
+
document.body.removeChild(a);
|
|
1036
|
+
URL.revokeObjectURL(url);
|
|
1037
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1038
|
+
}
|
|
1039
|
+
} catch (err) {
|
|
1040
|
+
setError(err instanceof Error ? err.message : "Failed to export all drawings");
|
|
1041
|
+
}
|
|
1042
|
+
}, [workspace]);
|
|
957
1043
|
const importExcalidrawFile = (0, import_react.useCallback)(async () => {
|
|
958
1044
|
if (!workspace) return;
|
|
959
1045
|
try {
|
|
@@ -1068,6 +1154,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
1068
1154
|
exportWorkspace,
|
|
1069
1155
|
importWorkspace,
|
|
1070
1156
|
exportDrawingAsExcalidraw,
|
|
1157
|
+
exportAllDrawingsAsExcalidraw,
|
|
1071
1158
|
importExcalidrawFile
|
|
1072
1159
|
};
|
|
1073
1160
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WorkspaceContext.Provider, { value, children });
|
|
@@ -1432,6 +1519,7 @@ var DrawingsDialog = ({
|
|
|
1432
1519
|
exportWorkspace,
|
|
1433
1520
|
importWorkspace,
|
|
1434
1521
|
exportDrawingAsExcalidraw,
|
|
1522
|
+
exportAllDrawingsAsExcalidraw,
|
|
1435
1523
|
importExcalidrawFile,
|
|
1436
1524
|
refreshDrawings,
|
|
1437
1525
|
t: contextT,
|
|
@@ -1515,9 +1603,8 @@ var DrawingsDialog = ({
|
|
|
1515
1603
|
setSwitchingId(null);
|
|
1516
1604
|
}, [switchDrawing, onDrawingSelect]);
|
|
1517
1605
|
const handleCreate = (0, import_react5.useCallback)(async (folderId) => {
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
}, [createNewDrawing, onDrawingSelect]);
|
|
1606
|
+
await createNewDrawing(void 0, folderId, false);
|
|
1607
|
+
}, [createNewDrawing]);
|
|
1521
1608
|
const handleStartEdit = (0, import_react5.useCallback)((drawing) => {
|
|
1522
1609
|
setEditingId(drawing.id);
|
|
1523
1610
|
setEditName(drawing.name);
|
|
@@ -2244,6 +2331,15 @@ var DrawingsDialog = ({
|
|
|
2244
2331
|
}
|
|
2245
2332
|
}
|
|
2246
2333
|
),
|
|
2334
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
2335
|
+
ActionButton,
|
|
2336
|
+
{
|
|
2337
|
+
icon: "\u{1F4E6}",
|
|
2338
|
+
label: "Exportera alla som .excalidraw",
|
|
2339
|
+
description: "Laddar ner varje ritning som separat fil",
|
|
2340
|
+
onClick: exportAllDrawingsAsExcalidraw
|
|
2341
|
+
}
|
|
2342
|
+
),
|
|
2247
2343
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
2248
2344
|
ActionButton,
|
|
2249
2345
|
{
|
package/dist/index.mjs
CHANGED
|
@@ -360,9 +360,30 @@ function useWorkspaceLang() {
|
|
|
360
360
|
}
|
|
361
361
|
return { lang: context.lang, t: context.t };
|
|
362
362
|
}
|
|
363
|
-
var
|
|
363
|
+
var TAB_ID_KEY = "rita-workspace-tab-id";
|
|
364
|
+
var TAB_ID = (() => {
|
|
365
|
+
try {
|
|
366
|
+
const existing = sessionStorage.getItem(TAB_ID_KEY);
|
|
367
|
+
if (existing) return existing;
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
const fresh = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2);
|
|
371
|
+
try {
|
|
372
|
+
sessionStorage.setItem(TAB_ID_KEY, fresh);
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
return fresh;
|
|
376
|
+
})();
|
|
364
377
|
var TABS_KEY = "rita-workspace-tabs";
|
|
365
378
|
var TAB_CHANNEL = "rita-workspace-tabs";
|
|
379
|
+
function broadcastWorkspaceChange() {
|
|
380
|
+
try {
|
|
381
|
+
const channel = new BroadcastChannel(TAB_CHANNEL);
|
|
382
|
+
channel.postMessage({ type: "workspace-changed", tabId: TAB_ID });
|
|
383
|
+
channel.close();
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
}
|
|
366
387
|
var tabsMapCache = null;
|
|
367
388
|
var tabsMapRaw = null;
|
|
368
389
|
function getTabsMap() {
|
|
@@ -385,16 +406,34 @@ function getTabsMap() {
|
|
|
385
406
|
return {};
|
|
386
407
|
}
|
|
387
408
|
}
|
|
409
|
+
var TAB_ENTRY_KEY = "rita-workspace-tab-entry";
|
|
388
410
|
function setTabDrawing(drawingId) {
|
|
389
411
|
const tabs = getTabsMap();
|
|
390
412
|
if (drawingId) {
|
|
391
413
|
const existing = tabs[TAB_ID];
|
|
414
|
+
let openedAt;
|
|
392
415
|
if (existing && existing.drawingId === drawingId) {
|
|
416
|
+
openedAt = existing.openedAt;
|
|
393
417
|
} else {
|
|
394
|
-
|
|
418
|
+
let sessionEntry = null;
|
|
419
|
+
try {
|
|
420
|
+
const raw = sessionStorage.getItem(TAB_ENTRY_KEY);
|
|
421
|
+
if (raw) sessionEntry = JSON.parse(raw);
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
openedAt = sessionEntry && sessionEntry.drawingId === drawingId ? sessionEntry.openedAt : Date.now();
|
|
425
|
+
}
|
|
426
|
+
tabs[TAB_ID] = { drawingId, openedAt };
|
|
427
|
+
try {
|
|
428
|
+
sessionStorage.setItem(TAB_ENTRY_KEY, JSON.stringify(tabs[TAB_ID]));
|
|
429
|
+
} catch {
|
|
395
430
|
}
|
|
396
431
|
} else {
|
|
397
432
|
delete tabs[TAB_ID];
|
|
433
|
+
try {
|
|
434
|
+
sessionStorage.removeItem(TAB_ENTRY_KEY);
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
398
437
|
}
|
|
399
438
|
const json = JSON.stringify(tabs);
|
|
400
439
|
localStorage.setItem(TABS_KEY, json);
|
|
@@ -491,6 +530,8 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
491
530
|
channel.onmessage = (event) => {
|
|
492
531
|
if (event.data?.type === "ping") {
|
|
493
532
|
channel?.postMessage({ type: "pong", tabId: TAB_ID });
|
|
533
|
+
} else if (event.data?.type === "workspace-changed" && event.data?.tabId !== TAB_ID) {
|
|
534
|
+
refreshDrawingsRef.current();
|
|
494
535
|
}
|
|
495
536
|
};
|
|
496
537
|
} catch {
|
|
@@ -628,11 +669,16 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
628
669
|
const refreshDrawings = useCallback(async () => {
|
|
629
670
|
if (!workspace) return;
|
|
630
671
|
try {
|
|
631
|
-
const [allDrawings, allFolders] = await Promise.all([
|
|
672
|
+
const [freshWorkspace, allDrawings, allFolders] = await Promise.all([
|
|
673
|
+
getWorkspace(workspace.id),
|
|
632
674
|
getAllDrawings(),
|
|
633
675
|
getAllFolders()
|
|
634
676
|
]);
|
|
635
|
-
const
|
|
677
|
+
const drawingIds = freshWorkspace?.drawingIds || workspace.drawingIds;
|
|
678
|
+
const wsDrawings = allDrawings.filter((d) => drawingIds.includes(d.id));
|
|
679
|
+
if (freshWorkspace) {
|
|
680
|
+
setWorkspace((prev) => prev ? { ...freshWorkspace, activeDrawingId: prev.activeDrawingId } : freshWorkspace);
|
|
681
|
+
}
|
|
636
682
|
setDrawings(wsDrawings);
|
|
637
683
|
setFolders(allFolders);
|
|
638
684
|
} catch (err) {
|
|
@@ -644,7 +690,9 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
644
690
|
foldersRef.current = folders;
|
|
645
691
|
const activeDrawingIdRef = useRef(activeDrawing?.id ?? null);
|
|
646
692
|
activeDrawingIdRef.current = activeDrawing?.id ?? null;
|
|
647
|
-
const
|
|
693
|
+
const refreshDrawingsRef = useRef(refreshDrawings);
|
|
694
|
+
refreshDrawingsRef.current = refreshDrawings;
|
|
695
|
+
const createNewDrawing = useCallback(async (name, folderId, activate = true) => {
|
|
648
696
|
if (!workspace) return null;
|
|
649
697
|
const now = Date.now();
|
|
650
698
|
const tempId = `temp-${now}`;
|
|
@@ -661,19 +709,24 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
661
709
|
updatedAt: now
|
|
662
710
|
};
|
|
663
711
|
setDrawings((prev) => [...prev, tempDrawing]);
|
|
664
|
-
|
|
665
|
-
|
|
712
|
+
if (activate) {
|
|
713
|
+
setActiveDrawing2(tempDrawing);
|
|
714
|
+
sessionStorage.setItem("rita-workspace-tab-drawing", tempId);
|
|
715
|
+
}
|
|
666
716
|
try {
|
|
667
717
|
const drawing = await createDrawing(name || defaultName, [], {}, folderId);
|
|
668
718
|
await addDrawingToWorkspace(workspace.id, drawing.id);
|
|
669
719
|
setDrawings((prev) => prev.map((d) => d.id === tempId ? drawing : d));
|
|
670
|
-
|
|
720
|
+
if (activate) {
|
|
721
|
+
setActiveDrawing2(drawing);
|
|
722
|
+
sessionStorage.setItem("rita-workspace-tab-drawing", drawing.id);
|
|
723
|
+
}
|
|
671
724
|
setWorkspace((prev) => prev ? {
|
|
672
725
|
...prev,
|
|
673
726
|
drawingIds: [...prev.drawingIds, drawing.id],
|
|
674
|
-
activeDrawingId: drawing.id
|
|
727
|
+
...activate ? { activeDrawingId: drawing.id } : {}
|
|
675
728
|
} : null);
|
|
676
|
-
|
|
729
|
+
broadcastWorkspaceChange();
|
|
677
730
|
return drawing;
|
|
678
731
|
} catch (err) {
|
|
679
732
|
setDrawings((prev) => prev.filter((d) => d.id !== tempId));
|
|
@@ -701,6 +754,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
701
754
|
}
|
|
702
755
|
try {
|
|
703
756
|
await updateDrawing(id, { name });
|
|
757
|
+
broadcastWorkspaceChange();
|
|
704
758
|
} catch (err) {
|
|
705
759
|
refreshDrawings();
|
|
706
760
|
setError(err instanceof Error ? err.message : "Failed to rename drawing");
|
|
@@ -722,6 +776,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
722
776
|
if (updatedWorkspace) {
|
|
723
777
|
setWorkspace(updatedWorkspace);
|
|
724
778
|
}
|
|
779
|
+
broadcastWorkspaceChange();
|
|
725
780
|
} catch (err) {
|
|
726
781
|
if (removedDrawing) {
|
|
727
782
|
setDrawings((prev) => [...prev, removedDrawing]);
|
|
@@ -752,6 +807,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
752
807
|
...prev,
|
|
753
808
|
drawingIds: [...prev.drawingIds, duplicate.id]
|
|
754
809
|
} : null);
|
|
810
|
+
broadcastWorkspaceChange();
|
|
755
811
|
return duplicate;
|
|
756
812
|
}
|
|
757
813
|
setDrawings((prev) => prev.filter((d) => d.id !== tempId));
|
|
@@ -883,6 +939,36 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
883
939
|
setError(err instanceof Error ? err.message : "Failed to export drawing");
|
|
884
940
|
}
|
|
885
941
|
}, [drawings]);
|
|
942
|
+
const exportAllDrawingsAsExcalidraw = useCallback(async () => {
|
|
943
|
+
try {
|
|
944
|
+
const all = await getAllDrawings();
|
|
945
|
+
const wsDrawings = workspace ? all.filter((d) => workspace.drawingIds.includes(d.id)) : all;
|
|
946
|
+
for (let i = 0; i < wsDrawings.length; i++) {
|
|
947
|
+
const drawing = wsDrawings[i];
|
|
948
|
+
const excalidrawData = {
|
|
949
|
+
type: "excalidraw",
|
|
950
|
+
version: 2,
|
|
951
|
+
source: "rita-workspace",
|
|
952
|
+
elements: drawing.elements || [],
|
|
953
|
+
appState: { viewBackgroundColor: "#ffffff", ...drawing.appState || {} },
|
|
954
|
+
files: drawing.files || {}
|
|
955
|
+
};
|
|
956
|
+
const blob = new Blob([JSON.stringify(excalidrawData, null, 2)], { type: "application/json" });
|
|
957
|
+
const url = URL.createObjectURL(blob);
|
|
958
|
+
const a = document.createElement("a");
|
|
959
|
+
a.href = url;
|
|
960
|
+
const safeName = (drawing.name || "ritning").replace(/[\\/:*?"<>|]/g, "_");
|
|
961
|
+
a.download = `${String(i + 1).padStart(2, "0")}_${safeName}.excalidraw`;
|
|
962
|
+
document.body.appendChild(a);
|
|
963
|
+
a.click();
|
|
964
|
+
document.body.removeChild(a);
|
|
965
|
+
URL.revokeObjectURL(url);
|
|
966
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
967
|
+
}
|
|
968
|
+
} catch (err) {
|
|
969
|
+
setError(err instanceof Error ? err.message : "Failed to export all drawings");
|
|
970
|
+
}
|
|
971
|
+
}, [workspace]);
|
|
886
972
|
const importExcalidrawFile = useCallback(async () => {
|
|
887
973
|
if (!workspace) return;
|
|
888
974
|
try {
|
|
@@ -997,6 +1083,7 @@ function WorkspaceProvider({ children, lang = "en" }) {
|
|
|
997
1083
|
exportWorkspace,
|
|
998
1084
|
importWorkspace,
|
|
999
1085
|
exportDrawingAsExcalidraw,
|
|
1086
|
+
exportAllDrawingsAsExcalidraw,
|
|
1000
1087
|
importExcalidrawFile
|
|
1001
1088
|
};
|
|
1002
1089
|
return /* @__PURE__ */ jsx(WorkspaceContext.Provider, { value, children });
|
|
@@ -1361,6 +1448,7 @@ var DrawingsDialog = ({
|
|
|
1361
1448
|
exportWorkspace,
|
|
1362
1449
|
importWorkspace,
|
|
1363
1450
|
exportDrawingAsExcalidraw,
|
|
1451
|
+
exportAllDrawingsAsExcalidraw,
|
|
1364
1452
|
importExcalidrawFile,
|
|
1365
1453
|
refreshDrawings,
|
|
1366
1454
|
t: contextT,
|
|
@@ -1444,9 +1532,8 @@ var DrawingsDialog = ({
|
|
|
1444
1532
|
setSwitchingId(null);
|
|
1445
1533
|
}, [switchDrawing, onDrawingSelect]);
|
|
1446
1534
|
const handleCreate = useCallback2(async (folderId) => {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
}, [createNewDrawing, onDrawingSelect]);
|
|
1535
|
+
await createNewDrawing(void 0, folderId, false);
|
|
1536
|
+
}, [createNewDrawing]);
|
|
1450
1537
|
const handleStartEdit = useCallback2((drawing) => {
|
|
1451
1538
|
setEditingId(drawing.id);
|
|
1452
1539
|
setEditName(drawing.name);
|
|
@@ -2173,6 +2260,15 @@ var DrawingsDialog = ({
|
|
|
2173
2260
|
}
|
|
2174
2261
|
}
|
|
2175
2262
|
),
|
|
2263
|
+
/* @__PURE__ */ jsx6(
|
|
2264
|
+
ActionButton,
|
|
2265
|
+
{
|
|
2266
|
+
icon: "\u{1F4E6}",
|
|
2267
|
+
label: "Exportera alla som .excalidraw",
|
|
2268
|
+
description: "Laddar ner varje ritning som separat fil",
|
|
2269
|
+
onClick: exportAllDrawingsAsExcalidraw
|
|
2270
|
+
}
|
|
2271
|
+
),
|
|
2176
2272
|
/* @__PURE__ */ jsx6(
|
|
2177
2273
|
ActionButton,
|
|
2178
2274
|
{
|