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 +136 -133
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +76 -11
- package/dist/index.mjs +76 -11
- 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
|
@@ -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
|
|
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
|
|
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
|
-
|
|
736
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1519
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
665
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1448
|
-
|
|
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
|
{
|