rita-workspace 0.1.1

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 ADDED
@@ -0,0 +1,104 @@
1
+ # Rita Workspace
2
+
3
+ Multi-drawing workspace feature for Rita (Excalidraw fork based on B310-digital/excalidraw).
4
+
5
+ ## Problem
6
+
7
+ Currently Rita only supports one active drawing at a time. Users need to manually export/import drawings to switch between them.
8
+
9
+ ## Solution
10
+
11
+ Add a workspace feature that allows users to:
12
+ - Create multiple drawings within a single workspace
13
+ - Switch between drawings with a simple sidebar/tab GUI
14
+ - Store all drawings locally in the browser (IndexedDB/localStorage)
15
+ - Name and organize drawings
16
+
17
+ ## Features
18
+
19
+ ### MVP (v1.0)
20
+ - [ ] Sidebar with list of drawings
21
+ - [ ] Create new drawing button
22
+ - [ ] Switch between drawings (preserves state)
23
+ - [ ] Rename drawings
24
+ - [ ] Delete drawings
25
+ - [ ] Auto-save to browser storage (IndexedDB)
26
+ - [ ] Import existing .excalidraw files into workspace
27
+
28
+ ### Future (v2.0+)
29
+ - [ ] Folders/categories for drawings
30
+ - [ ] Search drawings
31
+ - [ ] Thumbnail previews
32
+ - [ ] Export entire workspace
33
+ - [ ] Cloud sync (optional)
34
+ - [ ] Link/reference between drawings
35
+
36
+ ## Technical Approach
37
+
38
+ ### Storage
39
+ - Use IndexedDB for persistent storage (better than localStorage for binary data)
40
+ - Each drawing stored as separate entry with metadata
41
+ - Workspace metadata stored separately
42
+
43
+ ### Data Structure
44
+ ```typescript
45
+ interface Drawing {
46
+ id: string;
47
+ name: string;
48
+ createdAt: Date;
49
+ updatedAt: Date;
50
+ elements: ExcalidrawElement[];
51
+ appState: Partial<AppState>;
52
+ files: BinaryFiles;
53
+ }
54
+
55
+ interface Workspace {
56
+ id: string;
57
+ name: string;
58
+ drawings: string[]; // Drawing IDs
59
+ activeDrawingId: string;
60
+ createdAt: Date;
61
+ updatedAt: Date;
62
+ }
63
+ ```
64
+
65
+ ### UI Components
66
+ - `WorkspaceSidebar` - Collapsible sidebar showing drawing list
67
+ - `DrawingListItem` - Individual drawing entry with actions
68
+ - `NewDrawingButton` - Creates new blank drawing
69
+ - `WorkspaceProvider` - React context for workspace state
70
+
71
+ ## Integration with B310 Fork
72
+
73
+ This feature will be implemented as a modular addition that can be:
74
+ 1. Merged into the main B310 fork as a PR
75
+ 2. Maintained as a separate layer/plugin for Rita
76
+
77
+ ## Development
78
+
79
+ ### Prerequisites
80
+ - Node.js 18+
81
+ - Yarn
82
+ - B310 Excalidraw fork cloned locally
83
+
84
+ ### Setup
85
+ ```bash
86
+ # Clone B310 fork
87
+ git clone https://github.com/b310-digital/excalidraw.git rita
88
+ cd rita
89
+
90
+ # Apply workspace patches
91
+ git remote add workspace https://github.com/farapholch/rita-workspace.git
92
+ git fetch workspace
93
+ git merge workspace/main
94
+ ```
95
+
96
+ ## Links
97
+
98
+ - **B310 Excalidraw Fork:** https://github.com/b310-digital/excalidraw
99
+ - **Original Excalidraw:** https://github.com/excalidraw/excalidraw
100
+ - **Rita:** Your local Rita deployment
101
+
102
+ ## License
103
+
104
+ MIT (same as Excalidraw)
package/dist/index.css ADDED
@@ -0,0 +1,168 @@
1
+ /* src/ui/DrawingList/DrawingList.module.css */
2
+ .list {
3
+ list-style: none;
4
+ margin: 0;
5
+ padding: 0;
6
+ }
7
+ .empty {
8
+ padding: 24px 16px;
9
+ text-align: center;
10
+ color: #868e96;
11
+ font-size: 14px;
12
+ }
13
+ .item {
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: space-between;
17
+ padding: 10px 16px;
18
+ cursor: pointer;
19
+ border-left: 3px solid transparent;
20
+ transition: background 0.1s, border-color 0.1s;
21
+ }
22
+ .item:hover {
23
+ background: #e9ecef;
24
+ }
25
+ .item.active {
26
+ background: #e7f5ff;
27
+ border-left-color: #228be6;
28
+ }
29
+ .itemContent {
30
+ flex: 1;
31
+ min-width: 0;
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: 2px;
35
+ }
36
+ .name {
37
+ font-size: 14px;
38
+ font-weight: 500;
39
+ color: #212529;
40
+ white-space: nowrap;
41
+ overflow: hidden;
42
+ text-overflow: ellipsis;
43
+ }
44
+ .date {
45
+ font-size: 11px;
46
+ color: #868e96;
47
+ }
48
+ .editInput {
49
+ font-size: 14px;
50
+ font-weight: 500;
51
+ padding: 2px 6px;
52
+ border: 1px solid #228be6;
53
+ border-radius: 4px;
54
+ outline: none;
55
+ width: 100%;
56
+ }
57
+ .actions {
58
+ display: flex;
59
+ gap: 4px;
60
+ opacity: 0;
61
+ transition: opacity 0.1s;
62
+ }
63
+ .item:hover .actions {
64
+ opacity: 1;
65
+ }
66
+ .actionButton {
67
+ background: none;
68
+ border: none;
69
+ padding: 4px;
70
+ cursor: pointer;
71
+ font-size: 14px;
72
+ border-radius: 4px;
73
+ line-height: 1;
74
+ }
75
+ .actionButton:hover {
76
+ background: #dee2e6;
77
+ }
78
+
79
+ /* src/ui/Sidebar/Sidebar.module.css */
80
+ .sidebar {
81
+ display: flex;
82
+ flex-direction: column;
83
+ height: 100%;
84
+ background: #f8f9fa;
85
+ border-right: 1px solid #dee2e6;
86
+ font-family:
87
+ -apple-system,
88
+ BlinkMacSystemFont,
89
+ "Segoe UI",
90
+ Roboto,
91
+ sans-serif;
92
+ }
93
+ .header {
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: space-between;
97
+ padding: 12px 16px;
98
+ border-bottom: 1px solid #dee2e6;
99
+ }
100
+ .title {
101
+ margin: 0;
102
+ font-size: 14px;
103
+ font-weight: 600;
104
+ color: #495057;
105
+ }
106
+ .closeButton {
107
+ background: none;
108
+ border: none;
109
+ font-size: 16px;
110
+ color: #6c757d;
111
+ cursor: pointer;
112
+ padding: 4px 8px;
113
+ border-radius: 4px;
114
+ }
115
+ .closeButton:hover {
116
+ background: #e9ecef;
117
+ color: #495057;
118
+ }
119
+ .content {
120
+ flex: 1;
121
+ overflow-y: auto;
122
+ padding: 8px 0;
123
+ }
124
+ .loading {
125
+ padding: 16px;
126
+ text-align: center;
127
+ color: #6c757d;
128
+ font-size: 14px;
129
+ }
130
+ .footer {
131
+ padding: 12px;
132
+ border-top: 1px solid #dee2e6;
133
+ }
134
+ .newButton {
135
+ width: 100%;
136
+ padding: 10px 16px;
137
+ background: #228be6;
138
+ color: white;
139
+ border: none;
140
+ border-radius: 6px;
141
+ font-size: 14px;
142
+ font-weight: 500;
143
+ cursor: pointer;
144
+ transition: background 0.15s;
145
+ }
146
+ .newButton:hover:not(:disabled) {
147
+ background: #1c7ed6;
148
+ }
149
+ .newButton:disabled {
150
+ opacity: 0.6;
151
+ cursor: not-allowed;
152
+ }
153
+ .toggleButton {
154
+ position: fixed;
155
+ top: 12px;
156
+ left: 12px;
157
+ z-index: 100;
158
+ padding: 8px 12px;
159
+ background: #f8f9fa;
160
+ border: 1px solid #dee2e6;
161
+ border-radius: 6px;
162
+ font-size: 16px;
163
+ cursor: pointer;
164
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
165
+ }
166
+ .toggleButton:hover {
167
+ background: #e9ecef;
168
+ }
@@ -0,0 +1,145 @@
1
+ import { IDBPDatabase, DBSchema } from 'idb';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+ import { ReactNode } from 'react';
4
+
5
+ interface Drawing {
6
+ id: string;
7
+ name: string;
8
+ elements: unknown[];
9
+ appState: Record<string, unknown>;
10
+ files: Record<string, unknown>;
11
+ createdAt: number;
12
+ updatedAt: number;
13
+ }
14
+ interface Workspace {
15
+ id: string;
16
+ name: string;
17
+ drawingIds: string[];
18
+ activeDrawingId: string | null;
19
+ createdAt: number;
20
+ updatedAt: number;
21
+ }
22
+ interface RitaWorkspaceDB extends DBSchema {
23
+ workspaces: {
24
+ key: string;
25
+ value: Workspace;
26
+ indexes: {
27
+ 'by-updated': number;
28
+ };
29
+ };
30
+ drawings: {
31
+ key: string;
32
+ value: Drawing;
33
+ indexes: {
34
+ 'by-updated': number;
35
+ };
36
+ };
37
+ }
38
+ declare function getDB(): Promise<IDBPDatabase<RitaWorkspaceDB>>;
39
+ declare function closeDB(): Promise<void>;
40
+
41
+ declare function createDrawing(name?: string, elements?: unknown[], appState?: Record<string, unknown>): Promise<Drawing>;
42
+ declare function getDrawing(id: string): Promise<Drawing | undefined>;
43
+ declare function getAllDrawings(): Promise<Drawing[]>;
44
+ declare function updateDrawing(id: string, updates: Partial<Omit<Drawing, 'id' | 'createdAt'>>): Promise<Drawing | undefined>;
45
+ declare function deleteDrawing(id: string): Promise<boolean>;
46
+ declare function duplicateDrawing(id: string, newName?: string): Promise<Drawing | undefined>;
47
+
48
+ declare function getOrCreateDefaultWorkspace(): Promise<Workspace>;
49
+ declare function getWorkspace(id: string): Promise<Workspace | undefined>;
50
+ declare function updateWorkspace(id: string, updates: Partial<Omit<Workspace, 'id' | 'createdAt'>>): Promise<Workspace | undefined>;
51
+ declare function addDrawingToWorkspace(workspaceId: string, drawingId: string): Promise<Workspace | undefined>;
52
+ declare function removeDrawingFromWorkspace(workspaceId: string, drawingId: string): Promise<Workspace | undefined>;
53
+ declare function setActiveDrawing(workspaceId: string, drawingId: string): Promise<Workspace | undefined>;
54
+
55
+ interface WorkspaceContextValue {
56
+ workspace: Workspace | null;
57
+ drawings: Drawing[];
58
+ activeDrawing: Drawing | null;
59
+ isLoading: boolean;
60
+ error: string | null;
61
+ createNewDrawing: (name?: string) => Promise<Drawing | null>;
62
+ switchDrawing: (id: string) => Promise<void>;
63
+ renameDrawing: (id: string, name: string) => Promise<void>;
64
+ removeDrawing: (id: string) => Promise<void>;
65
+ duplicateCurrentDrawing: () => Promise<Drawing | null>;
66
+ saveCurrentDrawing: (elements: unknown[], appState: Record<string, unknown>) => Promise<void>;
67
+ }
68
+ declare function useWorkspace(): WorkspaceContextValue;
69
+ interface WorkspaceProviderProps {
70
+ children: ReactNode;
71
+ }
72
+ declare function WorkspaceProvider({ children }: WorkspaceProviderProps): react_jsx_runtime.JSX.Element;
73
+
74
+ interface SidebarProps {
75
+ isOpen?: boolean;
76
+ onToggle?: () => void;
77
+ width?: number;
78
+ }
79
+ declare function Sidebar({ isOpen, onToggle, width }: SidebarProps): react_jsx_runtime.JSX.Element;
80
+
81
+ declare function DrawingList(): react_jsx_runtime.JSX.Element;
82
+
83
+ interface DrawingListItemProps {
84
+ drawing: Drawing;
85
+ isActive: boolean;
86
+ onSelect: () => void;
87
+ onRename: (name: string) => void;
88
+ onDelete: () => void;
89
+ canDelete: boolean;
90
+ }
91
+ declare function DrawingListItem({ drawing, isActive, onSelect, onRename, onDelete, canDelete, }: DrawingListItemProps): react_jsx_runtime.JSX.Element;
92
+
93
+ interface ExcalidrawAPI {
94
+ getSceneElements: () => unknown[];
95
+ getAppState: () => Record<string, unknown>;
96
+ updateScene: (scene: {
97
+ elements?: unknown[];
98
+ appState?: Record<string, unknown>;
99
+ }) => void;
100
+ resetScene: () => void;
101
+ }
102
+ interface UseExcalidrawBridgeOptions {
103
+ excalidrawAPI: ExcalidrawAPI | null;
104
+ autoSaveInterval?: number;
105
+ }
106
+ /**
107
+ * Hook to bridge workspace state with Excalidraw API
108
+ *
109
+ * @example
110
+ * ```tsx
111
+ * const [excalidrawAPI, setExcalidrawAPI] = useState(null);
112
+ *
113
+ * useExcalidrawBridge({ excalidrawAPI });
114
+ *
115
+ * return <Excalidraw excalidrawAPI={setExcalidrawAPI} />;
116
+ * ```
117
+ */
118
+ declare function useExcalidrawBridge({ excalidrawAPI, autoSaveInterval, }: UseExcalidrawBridgeOptions): {
119
+ scheduleSave: () => void;
120
+ };
121
+
122
+ interface WorkspacePluginProps {
123
+ children: ReactNode;
124
+ defaultSidebarOpen?: boolean;
125
+ sidebarWidth?: number;
126
+ }
127
+ /**
128
+ * WorkspacePlugin - Wraps Excalidraw to add multi-drawing workspace support
129
+ *
130
+ * @example
131
+ * ```tsx
132
+ * import { WorkspacePlugin } from '@rita/workspace';
133
+ *
134
+ * function App() {
135
+ * return (
136
+ * <WorkspacePlugin>
137
+ * <Excalidraw />
138
+ * </WorkspacePlugin>
139
+ * );
140
+ * }
141
+ * ```
142
+ */
143
+ declare function WorkspacePlugin(props: WorkspacePluginProps): react_jsx_runtime.JSX.Element;
144
+
145
+ export { type Drawing, DrawingList, DrawingListItem, Sidebar, type Workspace, type WorkspaceContextValue, WorkspacePlugin, WorkspaceProvider, addDrawingToWorkspace, closeDB, createDrawing, deleteDrawing, duplicateDrawing, getAllDrawings, getDB, getDrawing, getOrCreateDefaultWorkspace, getWorkspace, removeDrawingFromWorkspace, setActiveDrawing, updateDrawing, updateWorkspace, useExcalidrawBridge, useWorkspace };
@@ -0,0 +1,145 @@
1
+ import { IDBPDatabase, DBSchema } from 'idb';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+ import { ReactNode } from 'react';
4
+
5
+ interface Drawing {
6
+ id: string;
7
+ name: string;
8
+ elements: unknown[];
9
+ appState: Record<string, unknown>;
10
+ files: Record<string, unknown>;
11
+ createdAt: number;
12
+ updatedAt: number;
13
+ }
14
+ interface Workspace {
15
+ id: string;
16
+ name: string;
17
+ drawingIds: string[];
18
+ activeDrawingId: string | null;
19
+ createdAt: number;
20
+ updatedAt: number;
21
+ }
22
+ interface RitaWorkspaceDB extends DBSchema {
23
+ workspaces: {
24
+ key: string;
25
+ value: Workspace;
26
+ indexes: {
27
+ 'by-updated': number;
28
+ };
29
+ };
30
+ drawings: {
31
+ key: string;
32
+ value: Drawing;
33
+ indexes: {
34
+ 'by-updated': number;
35
+ };
36
+ };
37
+ }
38
+ declare function getDB(): Promise<IDBPDatabase<RitaWorkspaceDB>>;
39
+ declare function closeDB(): Promise<void>;
40
+
41
+ declare function createDrawing(name?: string, elements?: unknown[], appState?: Record<string, unknown>): Promise<Drawing>;
42
+ declare function getDrawing(id: string): Promise<Drawing | undefined>;
43
+ declare function getAllDrawings(): Promise<Drawing[]>;
44
+ declare function updateDrawing(id: string, updates: Partial<Omit<Drawing, 'id' | 'createdAt'>>): Promise<Drawing | undefined>;
45
+ declare function deleteDrawing(id: string): Promise<boolean>;
46
+ declare function duplicateDrawing(id: string, newName?: string): Promise<Drawing | undefined>;
47
+
48
+ declare function getOrCreateDefaultWorkspace(): Promise<Workspace>;
49
+ declare function getWorkspace(id: string): Promise<Workspace | undefined>;
50
+ declare function updateWorkspace(id: string, updates: Partial<Omit<Workspace, 'id' | 'createdAt'>>): Promise<Workspace | undefined>;
51
+ declare function addDrawingToWorkspace(workspaceId: string, drawingId: string): Promise<Workspace | undefined>;
52
+ declare function removeDrawingFromWorkspace(workspaceId: string, drawingId: string): Promise<Workspace | undefined>;
53
+ declare function setActiveDrawing(workspaceId: string, drawingId: string): Promise<Workspace | undefined>;
54
+
55
+ interface WorkspaceContextValue {
56
+ workspace: Workspace | null;
57
+ drawings: Drawing[];
58
+ activeDrawing: Drawing | null;
59
+ isLoading: boolean;
60
+ error: string | null;
61
+ createNewDrawing: (name?: string) => Promise<Drawing | null>;
62
+ switchDrawing: (id: string) => Promise<void>;
63
+ renameDrawing: (id: string, name: string) => Promise<void>;
64
+ removeDrawing: (id: string) => Promise<void>;
65
+ duplicateCurrentDrawing: () => Promise<Drawing | null>;
66
+ saveCurrentDrawing: (elements: unknown[], appState: Record<string, unknown>) => Promise<void>;
67
+ }
68
+ declare function useWorkspace(): WorkspaceContextValue;
69
+ interface WorkspaceProviderProps {
70
+ children: ReactNode;
71
+ }
72
+ declare function WorkspaceProvider({ children }: WorkspaceProviderProps): react_jsx_runtime.JSX.Element;
73
+
74
+ interface SidebarProps {
75
+ isOpen?: boolean;
76
+ onToggle?: () => void;
77
+ width?: number;
78
+ }
79
+ declare function Sidebar({ isOpen, onToggle, width }: SidebarProps): react_jsx_runtime.JSX.Element;
80
+
81
+ declare function DrawingList(): react_jsx_runtime.JSX.Element;
82
+
83
+ interface DrawingListItemProps {
84
+ drawing: Drawing;
85
+ isActive: boolean;
86
+ onSelect: () => void;
87
+ onRename: (name: string) => void;
88
+ onDelete: () => void;
89
+ canDelete: boolean;
90
+ }
91
+ declare function DrawingListItem({ drawing, isActive, onSelect, onRename, onDelete, canDelete, }: DrawingListItemProps): react_jsx_runtime.JSX.Element;
92
+
93
+ interface ExcalidrawAPI {
94
+ getSceneElements: () => unknown[];
95
+ getAppState: () => Record<string, unknown>;
96
+ updateScene: (scene: {
97
+ elements?: unknown[];
98
+ appState?: Record<string, unknown>;
99
+ }) => void;
100
+ resetScene: () => void;
101
+ }
102
+ interface UseExcalidrawBridgeOptions {
103
+ excalidrawAPI: ExcalidrawAPI | null;
104
+ autoSaveInterval?: number;
105
+ }
106
+ /**
107
+ * Hook to bridge workspace state with Excalidraw API
108
+ *
109
+ * @example
110
+ * ```tsx
111
+ * const [excalidrawAPI, setExcalidrawAPI] = useState(null);
112
+ *
113
+ * useExcalidrawBridge({ excalidrawAPI });
114
+ *
115
+ * return <Excalidraw excalidrawAPI={setExcalidrawAPI} />;
116
+ * ```
117
+ */
118
+ declare function useExcalidrawBridge({ excalidrawAPI, autoSaveInterval, }: UseExcalidrawBridgeOptions): {
119
+ scheduleSave: () => void;
120
+ };
121
+
122
+ interface WorkspacePluginProps {
123
+ children: ReactNode;
124
+ defaultSidebarOpen?: boolean;
125
+ sidebarWidth?: number;
126
+ }
127
+ /**
128
+ * WorkspacePlugin - Wraps Excalidraw to add multi-drawing workspace support
129
+ *
130
+ * @example
131
+ * ```tsx
132
+ * import { WorkspacePlugin } from '@rita/workspace';
133
+ *
134
+ * function App() {
135
+ * return (
136
+ * <WorkspacePlugin>
137
+ * <Excalidraw />
138
+ * </WorkspacePlugin>
139
+ * );
140
+ * }
141
+ * ```
142
+ */
143
+ declare function WorkspacePlugin(props: WorkspacePluginProps): react_jsx_runtime.JSX.Element;
144
+
145
+ export { type Drawing, DrawingList, DrawingListItem, Sidebar, type Workspace, type WorkspaceContextValue, WorkspacePlugin, WorkspaceProvider, addDrawingToWorkspace, closeDB, createDrawing, deleteDrawing, duplicateDrawing, getAllDrawings, getDB, getDrawing, getOrCreateDefaultWorkspace, getWorkspace, removeDrawingFromWorkspace, setActiveDrawing, updateDrawing, updateWorkspace, useExcalidrawBridge, useWorkspace };