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 +136 -133
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +78 -12
- package/dist/index.mjs +78 -12
- 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,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
|
|
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
|
|
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
|
-
|
|
735
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1518
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
664
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1447
|
-
|
|
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
|
{
|