@lobehub/lobehub 2.0.0-next.255 → 2.0.0-next.256

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.
Files changed (140) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/en-US/plugin.json +1 -0
  4. package/locales/zh-CN/plugin.json +1 -0
  5. package/package.json +1 -1
  6. package/packages/builtin-tool-notebook/src/client/Placeholder/CreateDocument.tsx +6 -6
  7. package/packages/builtin-tool-notebook/src/client/Render/CreateDocument/DocumentCard.tsx +23 -10
  8. package/packages/builtin-tool-notebook/src/client/Streaming/CreateDocument/index.tsx +1 -1
  9. package/packages/database/src/models/__tests__/agent.test.ts +91 -4
  10. package/packages/database/src/models/agent.ts +15 -7
  11. package/packages/editor-runtime/src/EditorRuntime.ts +1 -1
  12. package/packages/editor-runtime/src/__tests__/EditorRuntime.real.test.ts +65 -4
  13. package/packages/editor-runtime/src/__tests__/__snapshots__/EditorRuntime.real.test.ts.snap +108 -17
  14. package/packages/editor-runtime/src/__tests__/fixtures/remove-then-add.json +636 -0
  15. package/packages/editor-runtime/src/__tests__/fixtures/remove.json +1 -0
  16. package/packages/types/src/agent/agentConfig.ts +8 -8
  17. package/src/app/[variants]/(main)/chat/features/Portal/_layout/Mobile.tsx +2 -1
  18. package/src/app/[variants]/(main)/group/features/Portal/_layout/Mobile.tsx +2 -1
  19. package/src/app/[variants]/(main)/home/_layout/hooks/useCreateMenuItems.tsx +2 -2
  20. package/src/app/[variants]/(main)/page/{features/PageTitle → PageTitle}/index.tsx +0 -1
  21. package/src/app/[variants]/(main)/page/[id]/index.tsx +43 -1
  22. package/src/app/[variants]/(main)/page/_layout/Body/AllPagesDrawer/Content.tsx +15 -15
  23. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/Editing.tsx +3 -3
  24. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/index.tsx +7 -12
  25. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/useDropdownMenu.tsx +5 -5
  26. package/src/app/[variants]/(main)/page/_layout/Body/List/index.tsx +7 -7
  27. package/src/app/[variants]/(main)/page/_layout/Body/index.tsx +15 -9
  28. package/src/app/[variants]/(main)/page/_layout/Body/useDropdownMenu.tsx +3 -3
  29. package/src/app/[variants]/(main)/page/_layout/DataSync.tsx +15 -0
  30. package/src/app/[variants]/(main)/page/_layout/Header/AddButton.tsx +2 -2
  31. package/src/app/[variants]/(main)/page/_layout/index.tsx +2 -0
  32. package/src/app/[variants]/(main)/page/index.tsx +3 -7
  33. package/src/components/Editor/AutoSaveHint.tsx +1 -1
  34. package/src/features/Conversation/Messages/User/Actions/index.tsx +5 -1
  35. package/src/features/EditorCanvas/AutoSaveHint.tsx +37 -0
  36. package/src/features/{PageEditor → EditorCanvas}/DiffAllToolbar.tsx +57 -16
  37. package/src/features/EditorCanvas/DocumentIdMode.tsx +111 -0
  38. package/src/features/EditorCanvas/EditorCanvas.tsx +148 -0
  39. package/src/features/EditorCanvas/EditorDataMode.tsx +64 -0
  40. package/src/features/EditorCanvas/ErrorBoundary.tsx +66 -0
  41. package/src/features/EditorCanvas/InlineToolbar.tsx +245 -0
  42. package/src/features/EditorCanvas/InternalEditor.tsx +134 -0
  43. package/src/features/{PageEditor/EditorCanvas → EditorCanvas}/actions.ts +10 -8
  44. package/src/features/EditorCanvas/index.ts +9 -0
  45. package/src/features/PageEditor/EditorCanvas/index.tsx +14 -111
  46. package/src/features/PageEditor/EditorCanvas/useAskCopilotItem.tsx +95 -0
  47. package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +1 -1
  48. package/src/features/PageEditor/Header/Breadcrumb.tsx +2 -2
  49. package/src/features/PageEditor/Header/index.tsx +15 -18
  50. package/src/features/PageEditor/Header/useMenu.tsx +12 -9
  51. package/src/features/PageEditor/PageEditor.tsx +45 -21
  52. package/src/features/PageEditor/PageEditorProvider.tsx +13 -1
  53. package/src/features/PageEditor/PageTitle/index.tsx +2 -2
  54. package/src/features/PageEditor/StoreUpdater.tsx +35 -308
  55. package/src/features/PageEditor/{Body/Title.tsx → TitleSection.tsx} +16 -16
  56. package/src/features/PageEditor/store/action.ts +96 -188
  57. package/src/features/PageEditor/store/initialState.ts +16 -21
  58. package/src/features/PageEditor/store/selectors.ts +3 -4
  59. package/src/features/PageExplorer/PageExplorerPlaceholder.tsx +22 -14
  60. package/src/features/PageExplorer/index.tsx +34 -67
  61. package/src/features/Portal/Artifacts/index.ts +0 -2
  62. package/src/features/Portal/Document/AutoSaveHint.tsx +7 -6
  63. package/src/features/Portal/Document/Body.tsx +1 -3
  64. package/src/features/Portal/Document/EditorCanvas.tsx +7 -50
  65. package/src/features/Portal/Document/Header.tsx +13 -10
  66. package/src/features/Portal/Document/TodoList.tsx +6 -4
  67. package/src/features/Portal/Document/Wrapper.tsx +3 -11
  68. package/src/features/Portal/Document/index.ts +0 -2
  69. package/src/features/Portal/FilePreview/index.ts +0 -2
  70. package/src/features/Portal/GroupThread/index.ts +0 -3
  71. package/src/features/Portal/MessageDetail/index.ts +0 -2
  72. package/src/features/Portal/Notebook/index.ts +0 -2
  73. package/src/features/Portal/Plugins/index.ts +0 -2
  74. package/src/features/Portal/Thread/index.ts +0 -3
  75. package/src/features/Portal/components/Header.tsx +18 -6
  76. package/src/features/Portal/router.tsx +34 -97
  77. package/src/features/Portal/type.ts +0 -2
  78. package/src/locales/default/plugin.ts +1 -0
  79. package/src/store/chat/slices/portal/action.test.ts +218 -15
  80. package/src/store/chat/slices/portal/action.ts +194 -41
  81. package/src/store/chat/slices/portal/initialState.ts +40 -1
  82. package/src/store/chat/slices/portal/selectors/thread.ts +44 -3
  83. package/src/store/chat/slices/portal/selectors.test.ts +119 -17
  84. package/src/store/chat/slices/portal/selectors.ts +117 -36
  85. package/src/store/document/index.ts +17 -5
  86. package/src/store/document/slices/document/action.ts +209 -0
  87. package/src/store/document/slices/document/index.ts +6 -0
  88. package/src/store/document/slices/editor/action.test.ts +340 -0
  89. package/src/store/document/slices/editor/action.ts +133 -149
  90. package/src/store/document/slices/editor/index.ts +9 -2
  91. package/src/store/document/slices/editor/initialState.ts +66 -29
  92. package/src/store/document/slices/editor/reducer.test.ts +217 -0
  93. package/src/store/document/slices/editor/reducer.ts +67 -0
  94. package/src/store/document/slices/editor/selectors.test.ts +395 -0
  95. package/src/store/document/slices/editor/selectors.ts +107 -5
  96. package/src/store/document/store.ts +12 -13
  97. package/src/store/file/slices/document/action.ts +19 -188
  98. package/src/store/file/slices/document/initialState.ts +0 -30
  99. package/src/store/file/slices/document/selectors.ts +25 -59
  100. package/src/store/notebook/index.ts +5 -4
  101. package/src/store/page/index.ts +2 -0
  102. package/src/store/page/initialState.ts +92 -0
  103. package/src/store/page/selectors.ts +5 -0
  104. package/src/store/page/slices/crud/action.ts +477 -0
  105. package/src/store/page/slices/crud/index.ts +2 -0
  106. package/src/store/page/slices/crud/initialState.ts +7 -0
  107. package/src/store/page/slices/internal/action.ts +32 -0
  108. package/src/store/page/slices/internal/index.ts +2 -0
  109. package/src/store/page/slices/internal/reducer.ts +105 -0
  110. package/src/store/page/slices/list/action.ts +206 -0
  111. package/src/store/page/slices/list/index.ts +3 -0
  112. package/src/store/page/slices/list/initialState.ts +29 -0
  113. package/src/store/page/slices/list/selectors.ts +90 -0
  114. package/src/store/page/slices/selection/action.ts +67 -0
  115. package/src/store/page/slices/selection/index.ts +2 -0
  116. package/src/store/page/slices/selection/initialState.ts +11 -0
  117. package/src/store/page/store.ts +29 -0
  118. package/src/store/tool/slices/lobehubSkillStore/selectors.ts +10 -20
  119. package/src/utils/identifier.ts +8 -2
  120. package/src/features/PageEditor/Body/index.tsx +0 -68
  121. package/src/features/PageEditor/EditorCanvas/InlineToolbar.tsx +0 -316
  122. package/src/features/PageEditor/Header/AutoSaveHint.tsx +0 -27
  123. package/src/features/Portal/Artifacts/useEnable.ts +0 -4
  124. package/src/features/Portal/Document/DocumentEditorProvider.tsx +0 -34
  125. package/src/features/Portal/Document/StoreUpdater.tsx +0 -80
  126. package/src/features/Portal/Document/Title.tsx +0 -54
  127. package/src/features/Portal/Document/store/action.ts +0 -114
  128. package/src/features/Portal/Document/store/index.ts +0 -21
  129. package/src/features/Portal/Document/store/initialState.ts +0 -24
  130. package/src/features/Portal/Document/useEnable.ts +0 -8
  131. package/src/features/Portal/FilePreview/useEnable.ts +0 -6
  132. package/src/features/Portal/GroupThread/hook.ts +0 -9
  133. package/src/features/Portal/MessageDetail/useEnable.ts +0 -4
  134. package/src/features/Portal/Notebook/useEnable.ts +0 -6
  135. package/src/features/Portal/Plugins/useEnable.ts +0 -6
  136. package/src/features/Portal/Thread/hook.ts +0 -8
  137. package/src/store/document/slices/notebook/action.ts +0 -119
  138. package/src/store/document/slices/notebook/index.ts +0 -3
  139. package/src/store/document/slices/notebook/initialState.ts +0 -12
  140. package/src/store/document/slices/notebook/selectors.ts +0 -26
@@ -0,0 +1,395 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { DocumentStore } from '../../store';
4
+ import { editorSelectors } from './selectors';
5
+
6
+ describe('DocumentStore - Editor Selectors', () => {
7
+ const createMockState = (overrides?: Partial<DocumentStore>): DocumentStore =>
8
+ ({
9
+ activeDocumentId: undefined,
10
+ documents: {},
11
+ editor: undefined,
12
+ editorState: undefined,
13
+ ...overrides,
14
+ }) as DocumentStore;
15
+
16
+ describe('activeDocumentId', () => {
17
+ it('should return undefined when no document is active', () => {
18
+ const state = createMockState();
19
+ expect(editorSelectors.activeDocumentId(state)).toBeUndefined();
20
+ });
21
+
22
+ it('should return the active document ID', () => {
23
+ const state = createMockState({ activeDocumentId: 'doc-1' });
24
+ expect(editorSelectors.activeDocumentId(state)).toBe('doc-1');
25
+ });
26
+ });
27
+
28
+ describe('activeDocument', () => {
29
+ it('should return undefined when no document is active', () => {
30
+ const state = createMockState();
31
+ expect(editorSelectors.activeDocument(state)).toBeUndefined();
32
+ });
33
+
34
+ it('should return the active document', () => {
35
+ const doc = {
36
+ content: 'Hello',
37
+ editorData: null,
38
+ isDirty: false,
39
+ lastSavedContent: 'Hello',
40
+ lastUpdatedTime: null,
41
+ saveStatus: 'idle' as const,
42
+ sourceType: 'notebook' as const,
43
+ };
44
+ const state = createMockState({
45
+ activeDocumentId: 'doc-1',
46
+ documents: { 'doc-1': doc },
47
+ });
48
+ expect(editorSelectors.activeDocument(state)).toEqual(doc);
49
+ });
50
+ });
51
+
52
+ describe('isEditing', () => {
53
+ it('should return false when no document is active', () => {
54
+ const state = createMockState();
55
+ expect(editorSelectors.isEditing(state)).toBe(false);
56
+ });
57
+
58
+ it('should return true when a document is active', () => {
59
+ const state = createMockState({ activeDocumentId: 'doc-1' });
60
+ expect(editorSelectors.isEditing(state)).toBe(true);
61
+ });
62
+ });
63
+
64
+ describe('documentById', () => {
65
+ it('should return undefined for non-existent document', () => {
66
+ const state = createMockState();
67
+ expect(editorSelectors.documentById('non-existent')(state)).toBeUndefined();
68
+ });
69
+
70
+ it('should return the document by ID', () => {
71
+ const doc = {
72
+ content: 'Test',
73
+ editorData: null,
74
+ isDirty: false,
75
+ lastSavedContent: 'Test',
76
+ lastUpdatedTime: null,
77
+ saveStatus: 'idle' as const,
78
+ sourceType: 'page' as const,
79
+ };
80
+ const state = createMockState({
81
+ documents: { 'doc-1': doc },
82
+ });
83
+ expect(editorSelectors.documentById('doc-1')(state)).toEqual(doc);
84
+ });
85
+ });
86
+
87
+ describe('isDirty', () => {
88
+ it('should return false for non-existent document', () => {
89
+ const state = createMockState();
90
+ expect(editorSelectors.isDirty('non-existent')(state)).toBe(false);
91
+ });
92
+
93
+ it('should return isDirty status', () => {
94
+ const state = createMockState({
95
+ documents: {
96
+ 'doc-1': {
97
+ content: '',
98
+ editorData: null,
99
+ isDirty: true,
100
+ lastSavedContent: '',
101
+ lastUpdatedTime: null,
102
+ saveStatus: 'idle' as const,
103
+ sourceType: 'notebook' as const,
104
+ },
105
+ },
106
+ });
107
+ expect(editorSelectors.isDirty('doc-1')(state)).toBe(true);
108
+ });
109
+ });
110
+
111
+ describe('saveStatus', () => {
112
+ it('should return idle for non-existent document', () => {
113
+ const state = createMockState();
114
+ expect(editorSelectors.saveStatus('non-existent')(state)).toBe('idle');
115
+ });
116
+
117
+ it('should return save status', () => {
118
+ const state = createMockState({
119
+ documents: {
120
+ 'doc-1': {
121
+ content: '',
122
+ editorData: null,
123
+ isDirty: false,
124
+ lastSavedContent: '',
125
+ lastUpdatedTime: null,
126
+ saveStatus: 'saving' as const,
127
+ sourceType: 'notebook' as const,
128
+ },
129
+ },
130
+ });
131
+ expect(editorSelectors.saveStatus('doc-1')(state)).toBe('saving');
132
+ });
133
+ });
134
+
135
+ describe('content', () => {
136
+ it('should return empty string for non-existent document', () => {
137
+ const state = createMockState();
138
+ expect(editorSelectors.content('non-existent')(state)).toBe('');
139
+ });
140
+
141
+ it('should return content', () => {
142
+ const state = createMockState({
143
+ documents: {
144
+ 'doc-1': {
145
+ content: '# Hello',
146
+ editorData: null,
147
+ isDirty: false,
148
+ lastSavedContent: '',
149
+ lastUpdatedTime: null,
150
+ saveStatus: 'idle' as const,
151
+ sourceType: 'notebook' as const,
152
+ },
153
+ },
154
+ });
155
+ expect(editorSelectors.content('doc-1')(state)).toBe('# Hello');
156
+ });
157
+ });
158
+
159
+ describe('editorData', () => {
160
+ it('should return undefined for non-existent document', () => {
161
+ const state = createMockState();
162
+ expect(editorSelectors.editorData('non-existent')(state)).toBeUndefined();
163
+ });
164
+
165
+ it('should return editorData', () => {
166
+ const mockEditorData = { type: 'doc', content: [] };
167
+ const state = createMockState({
168
+ documents: {
169
+ 'doc-1': {
170
+ content: '',
171
+ editorData: mockEditorData,
172
+ isDirty: false,
173
+ lastSavedContent: '',
174
+ lastUpdatedTime: null,
175
+ saveStatus: 'idle' as const,
176
+ sourceType: 'page' as const,
177
+ },
178
+ },
179
+ });
180
+ expect(editorSelectors.editorData('doc-1')(state)).toEqual(mockEditorData);
181
+ });
182
+ });
183
+
184
+ describe('sourceType', () => {
185
+ it('should return undefined for non-existent document', () => {
186
+ const state = createMockState();
187
+ expect(editorSelectors.sourceType('non-existent')(state)).toBeUndefined();
188
+ });
189
+
190
+ it('should return sourceType', () => {
191
+ const state = createMockState({
192
+ documents: {
193
+ 'doc-1': {
194
+ content: '',
195
+ editorData: null,
196
+ isDirty: false,
197
+ lastSavedContent: '',
198
+ lastUpdatedTime: null,
199
+ saveStatus: 'idle' as const,
200
+ sourceType: 'page' as const,
201
+ },
202
+ },
203
+ });
204
+ expect(editorSelectors.sourceType('doc-1')(state)).toBe('page');
205
+ });
206
+ });
207
+
208
+ describe('activeIsDirty', () => {
209
+ it('should return false when no document is active', () => {
210
+ const state = createMockState();
211
+ expect(editorSelectors.activeIsDirty(state)).toBe(false);
212
+ });
213
+
214
+ it('should return isDirty of active document', () => {
215
+ const state = createMockState({
216
+ activeDocumentId: 'doc-1',
217
+ documents: {
218
+ 'doc-1': {
219
+ content: '',
220
+ editorData: null,
221
+ isDirty: true,
222
+ lastSavedContent: '',
223
+ lastUpdatedTime: null,
224
+ saveStatus: 'idle' as const,
225
+ sourceType: 'notebook' as const,
226
+ },
227
+ },
228
+ });
229
+ expect(editorSelectors.activeIsDirty(state)).toBe(true);
230
+ });
231
+ });
232
+
233
+ describe('activeContent', () => {
234
+ it('should return empty string when no document is active', () => {
235
+ const state = createMockState();
236
+ expect(editorSelectors.activeContent(state)).toBe('');
237
+ });
238
+
239
+ it('should return content of active document', () => {
240
+ const state = createMockState({
241
+ activeDocumentId: 'doc-1',
242
+ documents: {
243
+ 'doc-1': {
244
+ content: '# Active Doc',
245
+ editorData: null,
246
+ isDirty: false,
247
+ lastSavedContent: '',
248
+ lastUpdatedTime: null,
249
+ saveStatus: 'idle' as const,
250
+ sourceType: 'notebook' as const,
251
+ },
252
+ },
253
+ });
254
+ expect(editorSelectors.activeContent(state)).toBe('# Active Doc');
255
+ });
256
+ });
257
+
258
+ describe('canSave', () => {
259
+ it('should return false when no document is active', () => {
260
+ const state = createMockState();
261
+ expect(editorSelectors.canSave(state)).toBeFalsy();
262
+ });
263
+
264
+ it('should return false when document is not dirty', () => {
265
+ const state = createMockState({
266
+ activeDocumentId: 'doc-1',
267
+ documents: {
268
+ 'doc-1': {
269
+ content: '',
270
+ editorData: null,
271
+ isDirty: false,
272
+ lastSavedContent: '',
273
+ lastUpdatedTime: null,
274
+ saveStatus: 'idle' as const,
275
+ sourceType: 'notebook' as const,
276
+ },
277
+ },
278
+ });
279
+ expect(editorSelectors.canSave(state)).toBeFalsy();
280
+ });
281
+
282
+ it('should return false when document is saving', () => {
283
+ const state = createMockState({
284
+ activeDocumentId: 'doc-1',
285
+ documents: {
286
+ 'doc-1': {
287
+ content: '',
288
+ editorData: null,
289
+ isDirty: true,
290
+ lastSavedContent: '',
291
+ lastUpdatedTime: null,
292
+ saveStatus: 'saving' as const,
293
+ sourceType: 'notebook' as const,
294
+ },
295
+ },
296
+ });
297
+ expect(editorSelectors.canSave(state)).toBeFalsy();
298
+ });
299
+
300
+ it('should return true when document is dirty and not saving', () => {
301
+ const state = createMockState({
302
+ activeDocumentId: 'doc-1',
303
+ documents: {
304
+ 'doc-1': {
305
+ content: '',
306
+ editorData: null,
307
+ isDirty: true,
308
+ lastSavedContent: '',
309
+ lastUpdatedTime: null,
310
+ saveStatus: 'idle' as const,
311
+ sourceType: 'notebook' as const,
312
+ },
313
+ },
314
+ });
315
+ expect(editorSelectors.canSave(state)).toBe(true);
316
+ });
317
+ });
318
+
319
+ describe('documentIds', () => {
320
+ it('should return empty array when no documents', () => {
321
+ const state = createMockState();
322
+ expect(editorSelectors.documentIds(state)).toEqual([]);
323
+ });
324
+
325
+ it('should return all document IDs', () => {
326
+ const state = createMockState({
327
+ documents: {
328
+ 'doc-1': {} as any,
329
+ 'doc-2': {} as any,
330
+ },
331
+ });
332
+ expect(editorSelectors.documentIds(state)).toEqual(['doc-1', 'doc-2']);
333
+ });
334
+ });
335
+
336
+ describe('documentCount', () => {
337
+ it('should return 0 when no documents', () => {
338
+ const state = createMockState();
339
+ expect(editorSelectors.documentCount(state)).toBe(0);
340
+ });
341
+
342
+ it('should return document count', () => {
343
+ const state = createMockState({
344
+ documents: {
345
+ 'doc-1': {} as any,
346
+ 'doc-2': {} as any,
347
+ 'doc-3': {} as any,
348
+ },
349
+ });
350
+ expect(editorSelectors.documentCount(state)).toBe(3);
351
+ });
352
+ });
353
+
354
+ describe('hasDocument', () => {
355
+ it('should return false for non-existent document', () => {
356
+ const state = createMockState();
357
+ expect(editorSelectors.hasDocument('non-existent')(state)).toBe(false);
358
+ });
359
+
360
+ it('should return true for existing document', () => {
361
+ const state = createMockState({
362
+ documents: {
363
+ 'doc-1': {} as any,
364
+ },
365
+ });
366
+ expect(editorSelectors.hasDocument('doc-1')(state)).toBe(true);
367
+ });
368
+ });
369
+
370
+ describe('editor', () => {
371
+ it('should return undefined when no editor', () => {
372
+ const state = createMockState();
373
+ expect(editorSelectors.editor(state)).toBeUndefined();
374
+ });
375
+
376
+ it('should return editor instance', () => {
377
+ const mockEditor = { focus: () => {} } as any;
378
+ const state = createMockState({ editor: mockEditor });
379
+ expect(editorSelectors.editor(state)).toBe(mockEditor);
380
+ });
381
+ });
382
+
383
+ describe('editorState', () => {
384
+ it('should return undefined when no editorState', () => {
385
+ const state = createMockState();
386
+ expect(editorSelectors.editorState(state)).toBeUndefined();
387
+ });
388
+
389
+ it('should return editorState', () => {
390
+ const mockEditorState = { isBold: true } as any;
391
+ const state = createMockState({ editorState: mockEditorState });
392
+ expect(editorSelectors.editorState(state)).toBe(mockEditorState);
393
+ });
394
+ });
395
+ });
@@ -1,16 +1,118 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix */
1
2
  import type { DocumentStore } from '../../store';
3
+ import type { EditorContentState } from './initialState';
4
+
5
+ // ===== Active Document Selectors =====
6
+
7
+ const activeDocumentId = (s: DocumentStore) => s.activeDocumentId;
8
+
9
+ const activeDocument = (s: DocumentStore): EditorContentState | undefined =>
10
+ s.activeDocumentId ? s.documents[s.activeDocumentId] : undefined;
2
11
 
3
12
  const isEditing = (s: DocumentStore) => !!s.activeDocumentId;
4
13
 
5
- const isEditMode = (s: DocumentStore) => s.mode === 'edit';
14
+ // ===== Document by ID Selectors =====
15
+
16
+ const documentById = (id: string) => (s: DocumentStore) => s.documents[id];
17
+
18
+ const isDirty = (id: string) => (s: DocumentStore) => s.documents[id]?.isDirty ?? false;
19
+
20
+ const saveStatus = (id: string) => (s: DocumentStore) => s.documents[id]?.saveStatus ?? 'idle';
21
+
22
+ const content = (id: string) => (s: DocumentStore) => s.documents[id]?.content ?? '';
23
+
24
+ const editorData = (id: string) => (s: DocumentStore) => s.documents[id]?.editorData;
25
+
26
+ const sourceType = (id: string) => (s: DocumentStore) => s.documents[id]?.sourceType;
27
+
28
+ const lastUpdatedTime = (id: string) => (s: DocumentStore) =>
29
+ s.documents[id]?.lastUpdatedTime?.toISOString();
30
+
31
+ // ===== Active Document Convenience Selectors =====
32
+
33
+ const activeIsDirty = (s: DocumentStore) => {
34
+ const doc = activeDocument(s);
35
+ return doc?.isDirty ?? false;
36
+ };
37
+
38
+ const activeSaveStatus = (s: DocumentStore) => {
39
+ const doc = activeDocument(s);
40
+ return doc?.saveStatus ?? 'idle';
41
+ };
42
+
43
+ const activeContent = (s: DocumentStore) => {
44
+ const doc = activeDocument(s);
45
+ return doc?.content ?? '';
46
+ };
47
+
48
+ const activeEditorData = (s: DocumentStore) => {
49
+ const doc = activeDocument(s);
50
+ return doc?.editorData;
51
+ };
6
52
 
7
- const isPreviewMode = (s: DocumentStore) => s.mode === 'preview';
53
+ const activeSourceType = (s: DocumentStore) => {
54
+ const doc = activeDocument(s);
55
+ return doc?.sourceType;
56
+ };
57
+
58
+ const activeLastUpdatedTime = (s: DocumentStore) => {
59
+ const doc = activeDocument(s);
60
+ return doc?.lastUpdatedTime;
61
+ };
8
62
 
9
- const canSave = (s: DocumentStore) => s.isDirty && s.saveStatus !== 'saving';
63
+ // ===== Editor State Selectors =====
64
+
65
+ const editor = (s: DocumentStore) => s.editor;
66
+
67
+ const editorState = (s: DocumentStore) => s.editorState;
68
+
69
+ const canSave = (s: DocumentStore) => {
70
+ const doc = activeDocument(s);
71
+ return doc?.isDirty && doc?.saveStatus !== 'saving';
72
+ };
73
+
74
+ // ===== Document List Selectors =====
75
+
76
+ const documentIds = (s: DocumentStore) => Object.keys(s.documents);
77
+
78
+ const documentCount = (s: DocumentStore) => Object.keys(s.documents).length;
79
+
80
+ const hasDocument = (id: string) => (s: DocumentStore) => id in s.documents;
81
+
82
+ /**
83
+ * Check if a document is still loading (not yet in the store)
84
+ */
85
+ const isDocumentLoading = (id: string | undefined) => (s: DocumentStore) => !id || !s.documents[id];
10
86
 
11
87
  export const editorSelectors = {
88
+ // Active document
89
+ activeContent,
90
+ activeDocument,
91
+ activeDocumentId,
92
+ activeEditorData,
93
+ activeIsDirty,
94
+ activeLastUpdatedTime,
95
+ activeSaveStatus,
96
+ activeSourceType,
97
+
98
+ // By ID
99
+ content,
100
+ documentById,
101
+ editorData,
102
+ hasDocument,
103
+ isDocumentLoading,
104
+ isDirty,
105
+ lastUpdatedTime,
106
+ saveStatus,
107
+ sourceType,
108
+
109
+ // Editor
12
110
  canSave,
13
- isEditMode,
111
+ editor,
112
+ editorState,
14
113
  isEditing,
15
- isPreviewMode,
114
+
115
+ // List
116
+ documentCount,
117
+ documentIds,
16
118
  };
@@ -3,35 +3,34 @@ import { createWithEqualityFn } from 'zustand/traditional';
3
3
  import { type StateCreator } from 'zustand/vanilla';
4
4
 
5
5
  import { createDevtools } from '../middleware/createDevtools';
6
- import { type EditorAction, type EditorState, createEditorSlice, initialEditorState } from './slices/editor';
6
+ import { type DocumentAction, createDocumentSlice } from './slices/document';
7
7
  import {
8
- type NotebookAction,
9
- type NotebookState,
10
- createNotebookSlice,
11
- initialNotebookState,
12
- } from './slices/notebook';
8
+ type EditorAction,
9
+ type EditorState,
10
+ createEditorSlice,
11
+ initialEditorState,
12
+ } from './slices/editor';
13
13
 
14
- // Combined state type
15
- export type DocumentState = EditorState & NotebookState;
14
+ // State type
15
+ export type DocumentState = EditorState;
16
16
 
17
- // Combined action type
18
- export type DocumentAction = EditorAction & NotebookAction;
17
+ // Action type
18
+ export type DocumentStoreAction = DocumentAction & EditorAction;
19
19
 
20
20
  // Full store type
21
- export type DocumentStore = DocumentState & DocumentAction;
21
+ export type DocumentStore = DocumentState & DocumentStoreAction;
22
22
 
23
23
  // Initial state
24
24
  const initialState: DocumentState = {
25
25
  ...initialEditorState,
26
- ...initialNotebookState,
27
26
  };
28
27
 
29
28
  const createStore: StateCreator<DocumentStore, [['zustand/devtools', never]]> = (
30
29
  ...parameters
31
30
  ) => ({
32
31
  ...initialState,
32
+ ...createDocumentSlice(...parameters),
33
33
  ...createEditorSlice(...parameters),
34
- ...createNotebookSlice(...parameters),
35
34
  });
36
35
 
37
36
  const devtools = createDevtools('document');