@bendyline/docblocks-react 1.1.2 → 2.1.0

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 (225) hide show
  1. package/README.md +37 -5
  2. package/dist/ExportDialog-7LDIXJZ3.js +10 -0
  3. package/dist/ExportDialog-7LDIXJZ3.js.map +1 -0
  4. package/dist/ExportToolbarControls-GHAOJMH6.js +508 -0
  5. package/dist/ExportToolbarControls-GHAOJMH6.js.map +1 -0
  6. package/dist/GitToolbarControl-GNMRVKWQ.js +136 -0
  7. package/dist/GitToolbarControl-GNMRVKWQ.js.map +1 -0
  8. package/dist/GitUI-VHOBQY43.js +1133 -0
  9. package/dist/GitUI-VHOBQY43.js.map +1 -0
  10. package/dist/LazyEditorShell-EF2BWRXU.js +6 -0
  11. package/dist/LazyEditorShell-EF2BWRXU.js.map +1 -0
  12. package/dist/ShareDialog-63ENBEDZ.js +213 -0
  13. package/dist/ShareDialog-63ENBEDZ.js.map +1 -0
  14. package/dist/chunk-4XXSWN3T.js +13 -0
  15. package/dist/chunk-4XXSWN3T.js.map +1 -0
  16. package/dist/chunk-57ZJJOC5.js +89 -0
  17. package/dist/chunk-57ZJJOC5.js.map +1 -0
  18. package/dist/chunk-5A4KEWKT.js +97 -0
  19. package/dist/chunk-5A4KEWKT.js.map +1 -0
  20. package/dist/chunk-DD4VFJQI.js +51 -0
  21. package/dist/chunk-DD4VFJQI.js.map +1 -0
  22. package/dist/chunk-FEWQUQML.js +256 -0
  23. package/dist/chunk-FEWQUQML.js.map +1 -0
  24. package/dist/chunk-FMGTHSTX.js +110 -0
  25. package/dist/chunk-FMGTHSTX.js.map +1 -0
  26. package/dist/chunk-UNYZQXWR.js +324 -0
  27. package/dist/chunk-UNYZQXWR.js.map +1 -0
  28. package/dist/chunk-VHDP4IPL.js +70 -0
  29. package/dist/chunk-VHDP4IPL.js.map +1 -0
  30. package/dist/chunk-ZI56BNB7.js +270 -0
  31. package/dist/chunk-ZI56BNB7.js.map +1 -0
  32. package/dist/editor.d.ts +11 -0
  33. package/dist/editor.js +9 -0
  34. package/dist/editor.js.map +1 -0
  35. package/dist/export/index.d.ts +93 -0
  36. package/dist/export/index.js +27 -0
  37. package/dist/export/index.js.map +1 -0
  38. package/dist/index.d.ts +346 -18
  39. package/dist/index.js +6169 -18
  40. package/dist/index.js.map +1 -1
  41. package/dist/run-export-ICTZITXC.js +10 -0
  42. package/dist/run-export-ICTZITXC.js.map +1 -0
  43. package/dist/settings/index.d.ts +36 -0
  44. package/dist/settings/index.js +14 -0
  45. package/dist/settings/index.js.map +1 -0
  46. package/package.json +33 -13
  47. package/src/styles/docblocks.css +2738 -132
  48. package/dist/AppMenu/AppMenu.d.ts +0 -26
  49. package/dist/AppMenu/AppMenu.d.ts.map +0 -1
  50. package/dist/AppMenu/AppMenu.js +0 -28
  51. package/dist/AppMenu/AppMenu.js.map +0 -1
  52. package/dist/AppMenu/index.d.ts +0 -3
  53. package/dist/AppMenu/index.d.ts.map +0 -1
  54. package/dist/AppMenu/index.js +0 -2
  55. package/dist/AppMenu/index.js.map +0 -1
  56. package/dist/DocBlocksShell/DocBlocksShell.d.ts +0 -36
  57. package/dist/DocBlocksShell/DocBlocksShell.d.ts.map +0 -1
  58. package/dist/DocBlocksShell/DocBlocksShell.js +0 -1351
  59. package/dist/DocBlocksShell/DocBlocksShell.js.map +0 -1
  60. package/dist/DocBlocksShell/index.d.ts +0 -3
  61. package/dist/DocBlocksShell/index.d.ts.map +0 -1
  62. package/dist/DocBlocksShell/index.js +0 -2
  63. package/dist/DocBlocksShell/index.js.map +0 -1
  64. package/dist/Export/ExportDialog.d.ts +0 -16
  65. package/dist/Export/ExportDialog.d.ts.map +0 -1
  66. package/dist/Export/ExportDialog.js +0 -158
  67. package/dist/Export/ExportDialog.js.map +0 -1
  68. package/dist/Export/ExportToolbarControls.d.ts +0 -17
  69. package/dist/Export/ExportToolbarControls.d.ts.map +0 -1
  70. package/dist/Export/ExportToolbarControls.js +0 -200
  71. package/dist/Export/ExportToolbarControls.js.map +0 -1
  72. package/dist/Export/export-options.d.ts +0 -53
  73. package/dist/Export/export-options.d.ts.map +0 -1
  74. package/dist/Export/export-options.js +0 -49
  75. package/dist/Export/export-options.js.map +0 -1
  76. package/dist/Export/index.d.ts +0 -6
  77. package/dist/Export/index.d.ts.map +0 -1
  78. package/dist/Export/index.js +0 -3
  79. package/dist/Export/index.js.map +0 -1
  80. package/dist/Export/run-export.d.ts +0 -8
  81. package/dist/Export/run-export.d.ts.map +0 -1
  82. package/dist/Export/run-export.js +0 -296
  83. package/dist/Export/run-export.js.map +0 -1
  84. package/dist/Export/transform-summaries.d.ts +0 -7
  85. package/dist/Export/transform-summaries.d.ts.map +0 -1
  86. package/dist/Export/transform-summaries.js +0 -6
  87. package/dist/Export/transform-summaries.js.map +0 -1
  88. package/dist/FileExplorer/FileExplorer.d.ts +0 -21
  89. package/dist/FileExplorer/FileExplorer.d.ts.map +0 -1
  90. package/dist/FileExplorer/FileExplorer.js +0 -140
  91. package/dist/FileExplorer/FileExplorer.js.map +0 -1
  92. package/dist/FileExplorer/FileTreeNode.d.ts +0 -18
  93. package/dist/FileExplorer/FileTreeNode.d.ts.map +0 -1
  94. package/dist/FileExplorer/FileTreeNode.js +0 -112
  95. package/dist/FileExplorer/FileTreeNode.js.map +0 -1
  96. package/dist/FileExplorer/index.d.ts +0 -6
  97. package/dist/FileExplorer/index.d.ts.map +0 -1
  98. package/dist/FileExplorer/index.js +0 -4
  99. package/dist/FileExplorer/index.js.map +0 -1
  100. package/dist/FileExplorer/useFileTree.d.ts +0 -35
  101. package/dist/FileExplorer/useFileTree.d.ts.map +0 -1
  102. package/dist/FileExplorer/useFileTree.js +0 -124
  103. package/dist/FileExplorer/useFileTree.js.map +0 -1
  104. package/dist/WorkspacePicker/WorkspacePicker.d.ts +0 -17
  105. package/dist/WorkspacePicker/WorkspacePicker.d.ts.map +0 -1
  106. package/dist/WorkspacePicker/WorkspacePicker.js +0 -63
  107. package/dist/WorkspacePicker/WorkspacePicker.js.map +0 -1
  108. package/dist/WorkspacePicker/WorkspaceSettingsButton.d.ts +0 -11
  109. package/dist/WorkspacePicker/WorkspaceSettingsButton.d.ts.map +0 -1
  110. package/dist/WorkspacePicker/WorkspaceSettingsButton.js +0 -27
  111. package/dist/WorkspacePicker/WorkspaceSettingsButton.js.map +0 -1
  112. package/dist/WorkspacePicker/WorkspaceSettingsDialog.d.ts +0 -20
  113. package/dist/WorkspacePicker/WorkspaceSettingsDialog.d.ts.map +0 -1
  114. package/dist/WorkspacePicker/WorkspaceSettingsDialog.js +0 -36
  115. package/dist/WorkspacePicker/WorkspaceSettingsDialog.js.map +0 -1
  116. package/dist/WorkspacePicker/index.d.ts +0 -3
  117. package/dist/WorkspacePicker/index.d.ts.map +0 -1
  118. package/dist/WorkspacePicker/index.js +0 -2
  119. package/dist/WorkspacePicker/index.js.map +0 -1
  120. package/dist/hooks/index.d.ts +0 -2
  121. package/dist/hooks/index.d.ts.map +0 -1
  122. package/dist/hooks/index.js +0 -2
  123. package/dist/hooks/index.js.map +0 -1
  124. package/dist/hooks/useAutoSave.d.ts +0 -15
  125. package/dist/hooks/useAutoSave.d.ts.map +0 -1
  126. package/dist/hooks/useAutoSave.js +0 -83
  127. package/dist/hooks/useAutoSave.js.map +0 -1
  128. package/dist/icons.d.ts +0 -7
  129. package/dist/icons.d.ts.map +0 -1
  130. package/dist/icons.js +0 -27
  131. package/dist/icons.js.map +0 -1
  132. package/dist/index.d.ts.map +0 -1
  133. package/dist/monaco-slim.d.ts +0 -19
  134. package/dist/monaco-slim.d.ts.map +0 -1
  135. package/dist/monaco-slim.js +0 -19
  136. package/dist/monaco-slim.js.map +0 -1
  137. package/dist/preferences/versioning.d.ts +0 -27
  138. package/dist/preferences/versioning.d.ts.map +0 -1
  139. package/dist/preferences/versioning.js +0 -62
  140. package/dist/preferences/versioning.js.map +0 -1
  141. package/src/AppMenu/AppMenu.tsx +0 -264
  142. package/src/AppMenu/index.ts +0 -2
  143. package/src/DocBlocksShell/DocBlocksShell.tsx +0 -1778
  144. package/src/DocBlocksShell/index.ts +0 -2
  145. package/src/Export/ExportDialog.tsx +0 -383
  146. package/src/Export/ExportToolbarControls.tsx +0 -313
  147. package/src/Export/export-options.ts +0 -99
  148. package/src/Export/index.ts +0 -5
  149. package/src/Export/run-export.ts +0 -349
  150. package/src/Export/transform-summaries.ts +0 -14
  151. package/src/FileExplorer/FileExplorer.tsx +0 -282
  152. package/src/FileExplorer/FileTreeNode.tsx +0 -214
  153. package/src/FileExplorer/index.ts +0 -5
  154. package/src/FileExplorer/useFileTree.ts +0 -178
  155. package/src/WorkspacePicker/WorkspacePicker.tsx +0 -137
  156. package/src/WorkspacePicker/WorkspaceSettingsButton.tsx +0 -88
  157. package/src/WorkspacePicker/WorkspaceSettingsDialog.tsx +0 -121
  158. package/src/WorkspacePicker/index.ts +0 -2
  159. package/src/fonts/fonts.css +0 -362
  160. package/src/fonts/licenses/LICENSE-cormorant-garamond.txt +0 -93
  161. package/src/fonts/licenses/LICENSE-crimson-text.txt +0 -93
  162. package/src/fonts/licenses/LICENSE-dm-sans.txt +0 -93
  163. package/src/fonts/licenses/LICENSE-dm-serif-display.txt +0 -93
  164. package/src/fonts/licenses/LICENSE-hanken-grotesk.txt +0 -94
  165. package/src/fonts/licenses/LICENSE-ibm-plex-sans.txt +0 -93
  166. package/src/fonts/licenses/LICENSE-inter.txt +0 -93
  167. package/src/fonts/licenses/LICENSE-jetbrains-mono.txt +0 -93
  168. package/src/fonts/licenses/LICENSE-lora.txt +0 -93
  169. package/src/fonts/licenses/LICENSE-merriweather.txt +0 -93
  170. package/src/fonts/licenses/LICENSE-oswald.txt +0 -93
  171. package/src/fonts/licenses/LICENSE-playfair-display.txt +0 -93
  172. package/src/fonts/licenses/LICENSE-pt-serif.txt +0 -93
  173. package/src/fonts/licenses/LICENSE-roboto.txt +0 -93
  174. package/src/fonts/licenses/LICENSE-source-serif-4.txt +0 -93
  175. package/src/fonts/woff2/cormorant-garamond-400.woff2 +0 -0
  176. package/src/fonts/woff2/cormorant-garamond-700.woff2 +0 -0
  177. package/src/fonts/woff2/crimson-text-400.woff2 +0 -0
  178. package/src/fonts/woff2/crimson-text-400i.woff2 +0 -0
  179. package/src/fonts/woff2/crimson-text-700.woff2 +0 -0
  180. package/src/fonts/woff2/dm-sans-400.woff2 +0 -0
  181. package/src/fonts/woff2/dm-sans-400i.woff2 +0 -0
  182. package/src/fonts/woff2/dm-sans-500.woff2 +0 -0
  183. package/src/fonts/woff2/dm-sans-700.woff2 +0 -0
  184. package/src/fonts/woff2/dm-serif-display-400.woff2 +0 -0
  185. package/src/fonts/woff2/hanken-grotesk-400.woff2 +0 -0
  186. package/src/fonts/woff2/hanken-grotesk-400i.woff2 +0 -0
  187. package/src/fonts/woff2/hanken-grotesk-500.woff2 +0 -0
  188. package/src/fonts/woff2/hanken-grotesk-600.woff2 +0 -0
  189. package/src/fonts/woff2/hanken-grotesk-700.woff2 +0 -0
  190. package/src/fonts/woff2/ibm-plex-sans-400.woff2 +0 -0
  191. package/src/fonts/woff2/ibm-plex-sans-400i.woff2 +0 -0
  192. package/src/fonts/woff2/ibm-plex-sans-500.woff2 +0 -0
  193. package/src/fonts/woff2/ibm-plex-sans-600.woff2 +0 -0
  194. package/src/fonts/woff2/ibm-plex-sans-700.woff2 +0 -0
  195. package/src/fonts/woff2/inter-400.woff2 +0 -0
  196. package/src/fonts/woff2/inter-400i.woff2 +0 -0
  197. package/src/fonts/woff2/inter-500.woff2 +0 -0
  198. package/src/fonts/woff2/inter-600.woff2 +0 -0
  199. package/src/fonts/woff2/inter-700.woff2 +0 -0
  200. package/src/fonts/woff2/jetbrains-mono-400.woff2 +0 -0
  201. package/src/fonts/woff2/jetbrains-mono-400i.woff2 +0 -0
  202. package/src/fonts/woff2/jetbrains-mono-700.woff2 +0 -0
  203. package/src/fonts/woff2/lora-400.woff2 +0 -0
  204. package/src/fonts/woff2/lora-400i.woff2 +0 -0
  205. package/src/fonts/woff2/lora-700.woff2 +0 -0
  206. package/src/fonts/woff2/merriweather-400.woff2 +0 -0
  207. package/src/fonts/woff2/merriweather-700.woff2 +0 -0
  208. package/src/fonts/woff2/oswald-400.woff2 +0 -0
  209. package/src/fonts/woff2/oswald-700.woff2 +0 -0
  210. package/src/fonts/woff2/playfair-display-400.woff2 +0 -0
  211. package/src/fonts/woff2/playfair-display-700.woff2 +0 -0
  212. package/src/fonts/woff2/pt-serif-400.woff2 +0 -0
  213. package/src/fonts/woff2/pt-serif-700.woff2 +0 -0
  214. package/src/fonts/woff2/roboto-400.woff2 +0 -0
  215. package/src/fonts/woff2/roboto-400i.woff2 +0 -0
  216. package/src/fonts/woff2/roboto-700.woff2 +0 -0
  217. package/src/fonts/woff2/source-serif-4-400.woff2 +0 -0
  218. package/src/fonts/woff2/source-serif-4-400i.woff2 +0 -0
  219. package/src/fonts/woff2/source-serif-4-700.woff2 +0 -0
  220. package/src/hooks/index.ts +0 -1
  221. package/src/hooks/useAutoSave.ts +0 -116
  222. package/src/icons.tsx +0 -70
  223. package/src/index.ts +0 -31
  224. package/src/monaco-slim.ts +0 -20
  225. package/src/preferences/versioning.ts +0 -69
@@ -1,1351 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- /**
3
- * DocBlocksShell — top-level layout component.
4
- *
5
- * Composes the left sidebar (WorkspacePicker + FileExplorer) with
6
- * the center editor area (squisq EditorShell).
7
- */
8
- import { useState, useCallback, useEffect, useRef } from 'react';
9
- import { EditorShell } from '@bendyline/squisq-editor-react';
10
- import '@bendyline/squisq-editor-react/styles';
11
- import { MediaContext } from '@bendyline/squisq-react';
12
- import { DocumentVersionManager, } from '@bendyline/squisq/versions';
13
- import { IndexedDBFileSystemProvider, ElectronFileSystemProvider, FileSystemContentContainer, createFileMediaProvider, openNativeFolder, restoreNativeFolder, removeDirectoryHandle, } from '@bendyline/docblocks/filesystem';
14
- import { isElectronHost, getDocBlocksHost } from '@bendyline/docblocks/host';
15
- import { ensureDefaultWorkspace, getWorkspace, listWorkspaces, removeWorkspace, saveWorkspace, touchWorkspace, } from '@bendyline/docblocks/workspace';
16
- import { AppMenu } from '../AppMenu/AppMenu.js';
17
- import { FileExplorer } from '../FileExplorer/FileExplorer.js';
18
- import { WorkspacePicker } from '../WorkspacePicker/WorkspacePicker.js';
19
- import { WorkspaceSettingsButton } from '../WorkspacePicker/WorkspaceSettingsButton.js';
20
- import { WorkspaceSettingsDialog, } from '../WorkspacePicker/WorkspaceSettingsDialog.js';
21
- import { useAutoSave } from '../hooks/useAutoSave.js';
22
- import { ExportToolbarControls } from '../Export/ExportToolbarControls.js';
23
- import { loadVersioningPreference, resolveVersioningEnabled, saveVersioningPreference, } from '../preferences/versioning.js';
24
- function useOsTheme() {
25
- const [dark, setDark] = useState(() => typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches);
26
- useEffect(() => {
27
- const mq = window.matchMedia('(prefers-color-scheme: dark)');
28
- const handler = (e) => setDark(e.matches);
29
- mq.addEventListener('change', handler);
30
- return () => mq.removeEventListener('change', handler);
31
- }, []);
32
- return dark ? 'dark' : 'light';
33
- }
34
- /** Encode workspace + optional file path into a URL hash. */
35
- function buildHash(workspaceId, filePath) {
36
- const ws = encodeURIComponent(workspaceId);
37
- if (!filePath)
38
- return `#${ws}`;
39
- const fp = filePath.replace(/^\//, '');
40
- return `#${ws}/${encodeURIComponent(fp)}`;
41
- }
42
- /** Decode the current URL hash into workspace id + file path. */
43
- function parseHash() {
44
- const raw = window.location.hash.slice(1);
45
- if (!raw)
46
- return null;
47
- const slashIdx = raw.indexOf('/');
48
- if (slashIdx === -1) {
49
- return { workspaceId: decodeURIComponent(raw), filePath: null };
50
- }
51
- return {
52
- workspaceId: decodeURIComponent(raw.slice(0, slashIdx)),
53
- filePath: '/' + decodeURIComponent(raw.slice(slashIdx + 1)),
54
- };
55
- }
56
- const LAST_STATE_KEY = 'docblocks:lastState';
57
- function saveLastState(state) {
58
- try {
59
- localStorage.setItem(LAST_STATE_KEY, JSON.stringify(state));
60
- }
61
- catch {
62
- // ignore quota errors
63
- }
64
- }
65
- function loadLastState() {
66
- try {
67
- const raw = localStorage.getItem(LAST_STATE_KEY);
68
- if (!raw)
69
- return null;
70
- return JSON.parse(raw);
71
- }
72
- catch {
73
- return null;
74
- }
75
- }
76
- /** One-time first-run callout shown over the welcome doc's Play view.
77
- * Once the user starts writing, switches views themselves, or dismisses
78
- * it, it never comes back — on any workspace. */
79
- const WELCOME_GATEWAY_KEY = 'docblocks:welcomeGatewayDismissed';
80
- function isWelcomeGatewayDismissed() {
81
- try {
82
- return localStorage.getItem(WELCOME_GATEWAY_KEY) === '1';
83
- }
84
- catch {
85
- return false;
86
- }
87
- }
88
- function markWelcomeGatewayDismissed() {
89
- try {
90
- localStorage.setItem(WELCOME_GATEWAY_KEY, '1');
91
- }
92
- catch {
93
- // ignore quota errors
94
- }
95
- }
96
- const THEME_PREF_KEY = 'docblocks:themePreference';
97
- function loadThemePreference() {
98
- try {
99
- const raw = localStorage.getItem(THEME_PREF_KEY);
100
- if (raw === 'light' || raw === 'dark' || raw === 'auto')
101
- return raw;
102
- }
103
- catch {
104
- // ignore
105
- }
106
- return 'auto';
107
- }
108
- function saveThemePreference(pref) {
109
- try {
110
- localStorage.setItem(THEME_PREF_KEY, pref);
111
- }
112
- catch {
113
- // ignore quota errors
114
- }
115
- }
116
- const SIDEBAR_WIDTH_KEY = 'docblocks:sidebarWidth';
117
- const SIDEBAR_WIDTH_DEFAULT = 260;
118
- const SIDEBAR_WIDTH_MIN = 180;
119
- const SIDEBAR_WIDTH_MAX = 600;
120
- /** Drag below this many pixels and the sidebar collapses entirely —
121
- * the editor takes the full width and a back-arrow appears in the
122
- * toolbar so the user can pop the sidebar back open. Same UX as
123
- * the existing mobile narrow-viewport flow. */
124
- const SIDEBAR_COLLAPSE_THRESHOLD = 120;
125
- function loadSidebarWidth() {
126
- try {
127
- const raw = localStorage.getItem(SIDEBAR_WIDTH_KEY);
128
- if (raw === null)
129
- return SIDEBAR_WIDTH_DEFAULT;
130
- const n = Number(raw);
131
- if (!Number.isFinite(n))
132
- return SIDEBAR_WIDTH_DEFAULT;
133
- return Math.min(SIDEBAR_WIDTH_MAX, Math.max(SIDEBAR_WIDTH_MIN, n));
134
- }
135
- catch {
136
- return SIDEBAR_WIDTH_DEFAULT;
137
- }
138
- }
139
- function saveSidebarWidth(px) {
140
- try {
141
- localStorage.setItem(SIDEBAR_WIDTH_KEY, String(Math.round(px)));
142
- }
143
- catch {
144
- // ignore quota errors
145
- }
146
- }
147
- const VIEW_PREFS_KEY = 'docblocks:viewPreferences';
148
- const DEFAULT_VIEW_PREFS = {
149
- outline: false,
150
- inlinePreview: true,
151
- showStatusBar: true,
152
- };
153
- function loadViewPreferences() {
154
- try {
155
- const raw = localStorage.getItem(VIEW_PREFS_KEY);
156
- if (!raw)
157
- return DEFAULT_VIEW_PREFS;
158
- const parsed = JSON.parse(raw);
159
- return {
160
- outline: typeof parsed.outline === 'boolean' ? parsed.outline : DEFAULT_VIEW_PREFS.outline,
161
- inlinePreview: typeof parsed.inlinePreview === 'boolean'
162
- ? parsed.inlinePreview
163
- : DEFAULT_VIEW_PREFS.inlinePreview,
164
- showStatusBar: typeof parsed.showStatusBar === 'boolean'
165
- ? parsed.showStatusBar
166
- : DEFAULT_VIEW_PREFS.showStatusBar,
167
- };
168
- }
169
- catch {
170
- return DEFAULT_VIEW_PREFS;
171
- }
172
- }
173
- function saveViewPreferences(prefs) {
174
- try {
175
- localStorage.setItem(VIEW_PREFS_KEY, JSON.stringify(prefs));
176
- }
177
- catch {
178
- // ignore quota errors
179
- }
180
- }
181
- function dirnameOf(p) {
182
- const clean = p.replace(/^\/+/, '');
183
- const idx = clean.lastIndexOf('/');
184
- return idx === -1 ? '' : clean.slice(0, idx);
185
- }
186
- function basenameOf(p) {
187
- const clean = p.replace(/^\/+/, '');
188
- const idx = clean.lastIndexOf('/');
189
- return idx === -1 ? clean : clean.slice(idx + 1);
190
- }
191
- /** Strip the extension from a filename (`notes.md` -> `notes`). Matches
192
- * squisq's `getDocBasename` convention so version snapshot filenames
193
- * stay readable (`<basename>.<timestamp>.md`). */
194
- function stripExtension(name) {
195
- return name.replace(/\.[^.]+$/, '');
196
- }
197
- function normaliseProviderPath(p) {
198
- return '/' + p.replace(/^\/+/, '');
199
- }
200
- function sameProviderPath(a, b) {
201
- return normaliseProviderPath(a) === normaliseProviderPath(b);
202
- }
203
- /** Portable relative link from one workspace file to another. Walks up
204
- * from the source's directory and back down to the target so the link
205
- * survives folder reshuffles (`../sibling.md`, `subfolder/child.md`,
206
- * `resume.md` for siblings at the workspace root). */
207
- function relativeMarkdownLink(fromFile, toFile) {
208
- const fromParts = fromFile.replace(/^\/+/, '').split('/').filter(Boolean);
209
- const toParts = toFile.replace(/^\/+/, '').split('/').filter(Boolean);
210
- const fromDir = fromParts.slice(0, -1);
211
- const toDir = toParts.slice(0, -1);
212
- const toBase = toParts[toParts.length - 1] ?? '';
213
- let common = 0;
214
- while (common < fromDir.length && common < toDir.length && fromDir[common] === toDir[common]) {
215
- common++;
216
- }
217
- const ups = fromDir.length - common;
218
- const downs = [...toDir.slice(common), toBase];
219
- const parts = [...Array(ups).fill('..'), ...downs];
220
- return parts.length === 0 ? toBase : parts.join('/');
221
- }
222
- /** Recursively collect all `.md` files reachable from `root`. Skips
223
- * Word-style `*_files/` asset companions, dotfiles, and `node_modules`
224
- * so the candidate list stays workspace-meaningful. */
225
- async function collectMarkdownFiles(fs, root) {
226
- const out = [];
227
- const visited = new Set();
228
- async function walk(dir) {
229
- if (visited.has(dir))
230
- return;
231
- visited.add(dir);
232
- let entries;
233
- try {
234
- entries = await fs.readDirectory(dir);
235
- }
236
- catch {
237
- return;
238
- }
239
- for (const entry of entries) {
240
- if (entry.kind === 'directory') {
241
- const lower = entry.name.toLowerCase();
242
- if (lower.startsWith('.'))
243
- continue;
244
- if (lower === 'node_modules')
245
- continue;
246
- if (lower.endsWith('_files'))
247
- continue;
248
- await walk(entry.path);
249
- }
250
- else if (entry.kind === 'file') {
251
- if (entry.name.toLowerCase().endsWith('.md'))
252
- out.push(entry);
253
- }
254
- }
255
- }
256
- await walk(root);
257
- return out;
258
- }
259
- async function createElectronProviderFromWorkspace(ws) {
260
- if (!ws.rootPath)
261
- return null;
262
- try {
263
- await getDocBlocksHost().workspaces.register({
264
- id: ws.id,
265
- name: ws.name,
266
- rootPath: ws.rootPath,
267
- });
268
- }
269
- catch {
270
- return null;
271
- }
272
- return new ElectronFileSystemProvider(ws.id, ws.name, ws.rootPath);
273
- }
274
- function useIsMobile(breakpoint = 768) {
275
- const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.matchMedia(`(max-width: ${breakpoint}px)`).matches);
276
- useEffect(() => {
277
- const mq = window.matchMedia(`(max-width: ${breakpoint}px)`);
278
- const handler = (e) => setIsMobile(e.matches);
279
- mq.addEventListener('change', handler);
280
- return () => mq.removeEventListener('change', handler);
281
- }, [breakpoint]);
282
- return isMobile;
283
- }
284
- function FolderGlyph() {
285
- return (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "M3 7a1 1 0 0 1 1-1h5l2 2h9a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7z" }) }));
286
- }
287
- function FileGlyph() {
288
- return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M6 3h8l5 5v12a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z" }), _jsx("path", { d: "M14 3v5h5" })] }));
289
- }
290
- export function DocBlocksShell({ theme: _themeProp = 'auto', logoUrl, allowVersioning = true, versionBasename, versioningPrunePolicy, versioningAutoSaveIdleMs, onSaveVersion, versioningRef, }) {
291
- const osTheme = useOsTheme();
292
- const [themePreference, setThemePreference] = useState(loadThemePreference);
293
- // "System default" (auto) always follows the OS — the host's theme prop
294
- // is kept only for API back-compat and does not override OS detection.
295
- const resolvedTheme = themePreference === 'light' || themePreference === 'dark' ? themePreference : osTheme;
296
- const handleThemeChange = useCallback((pref) => {
297
- setThemePreference(pref);
298
- saveThemePreference(pref);
299
- }, []);
300
- const [viewPreferences, setViewPreferences] = useState(loadViewPreferences);
301
- const handleViewPreferencesChange = useCallback((prefs) => {
302
- setViewPreferences(prefs);
303
- saveViewPreferences(prefs);
304
- }, []);
305
- const isMobile = useIsMobile();
306
- const [mobileShowEditor, setMobileShowEditor] = useState(false);
307
- // Sidebar width — persisted across sessions, dragged via the resizer
308
- // between sidebar and editor area. We track the "live" width during a
309
- // drag in a ref so each mousemove doesn't trigger a state update; only
310
- // setState on commit so React doesn't churn through every pixel.
311
- const [sidebarWidth, setSidebarWidth] = useState(loadSidebarWidth);
312
- // When the user drags the resizer below SIDEBAR_COLLAPSE_THRESHOLD,
313
- // we switch the layout into single-pane "compact" mode — same UX as
314
- // the mobile narrow-viewport flow, where only the sidebar OR the
315
- // editor is visible at a time and a back-arrow in the toolbar pops
316
- // between them. A "Restore split view" button on the editor toolbar
317
- // exits compact mode; on real mobile that button is suppressed
318
- // because there's not enough viewport for side-by-side. Not
319
- // persisted across reloads. */
320
- const [compactLayout, setCompactLayout] = useState(false);
321
- const effectiveCompact = isMobile || compactLayout;
322
- const sidebarRef = useRef(null);
323
- const dragStateRef = useRef(null);
324
- const handleResizerPointerDown = useCallback((e) => {
325
- if (e.button !== 0)
326
- return;
327
- dragStateRef.current = { startX: e.clientX, startWidth: sidebarWidth };
328
- e.preventDefault();
329
- // Disable text selection + flip the body cursor for the duration
330
- // of the drag so the col-resize cursor stays visible even when the
331
- // pointer slips off the 7px hit area. The custom cursor lives in
332
- // the CSS class so Windows' white-cursor preference doesn't make
333
- // the dragging cursor invisible against the light chrome.
334
- document.body.style.userSelect = 'none';
335
- document.body.classList.add('db-resizing-sidebar');
336
- let lastRaw = sidebarWidth;
337
- const onMove = (ev) => {
338
- const drag = dragStateRef.current;
339
- if (!drag)
340
- return;
341
- lastRaw = drag.startWidth + (ev.clientX - drag.startX);
342
- if (lastRaw < SIDEBAR_COLLAPSE_THRESHOLD && sidebarRef.current) {
343
- // Below threshold — preview the collapse by snapping to the
344
- // minimum width and fading the sidebar, so the user can see
345
- // they've crossed into "release to collapse" territory.
346
- sidebarRef.current.style.width = `${SIDEBAR_WIDTH_MIN}px`;
347
- sidebarRef.current.style.opacity = '0.45';
348
- return;
349
- }
350
- const clamped = Math.min(SIDEBAR_WIDTH_MAX, Math.max(SIDEBAR_WIDTH_MIN, lastRaw));
351
- if (sidebarRef.current) {
352
- // Update the DOM directly during the drag for jank-free
353
- // dragging; React state syncs on release.
354
- sidebarRef.current.style.width = `${clamped}px`;
355
- sidebarRef.current.style.opacity = '';
356
- }
357
- };
358
- const onUp = () => {
359
- document.removeEventListener('pointermove', onMove);
360
- document.removeEventListener('pointerup', onUp);
361
- document.body.style.userSelect = '';
362
- document.body.classList.remove('db-resizing-sidebar');
363
- if (sidebarRef.current) {
364
- sidebarRef.current.style.opacity = '';
365
- }
366
- if (lastRaw < SIDEBAR_COLLAPSE_THRESHOLD) {
367
- // Released below threshold — switch to compact (single-pane)
368
- // layout focused on the editor. Keep the persisted
369
- // sidebarWidth so exiting compact mode restores it.
370
- setCompactLayout(true);
371
- setMobileShowEditor(true);
372
- }
373
- else {
374
- const finalWidth = sidebarRef.current?.getBoundingClientRect().width;
375
- if (finalWidth) {
376
- const clamped = Math.min(SIDEBAR_WIDTH_MAX, Math.max(SIDEBAR_WIDTH_MIN, Math.round(finalWidth)));
377
- setSidebarWidth(clamped);
378
- saveSidebarWidth(clamped);
379
- }
380
- }
381
- dragStateRef.current = null;
382
- };
383
- document.addEventListener('pointermove', onMove);
384
- document.addEventListener('pointerup', onUp);
385
- }, [sidebarWidth]);
386
- const [provider, setProvider] = useState(null);
387
- const [activeWorkspaceId, setActiveWorkspaceId] = useState(null);
388
- const [activeWorkspaceDescriptor, setActiveWorkspaceDescriptor] = useState(null);
389
- // Re-fetch the descriptor whenever the active id (or its versioning
390
- // override) changes. `descriptorRefreshKey` is bumped after writes so
391
- // the resolver picks up the updated override without remounting.
392
- const [descriptorRefreshKey, setDescriptorRefreshKey] = useState(0);
393
- useEffect(() => {
394
- let cancelled = false;
395
- if (!activeWorkspaceId) {
396
- setActiveWorkspaceDescriptor(null);
397
- return;
398
- }
399
- void getWorkspace(activeWorkspaceId).then((ws) => {
400
- if (!cancelled)
401
- setActiveWorkspaceDescriptor(ws);
402
- });
403
- return () => {
404
- cancelled = true;
405
- };
406
- }, [activeWorkspaceId, descriptorRefreshKey]);
407
- const [workspaceSettingsOpen, setWorkspaceSettingsOpen] = useState(false);
408
- const [versioningPreference, setVersioningPreference] = useState(loadVersioningPreference);
409
- const handleVersioningPreferenceChange = useCallback((pref) => {
410
- setVersioningPreference(pref);
411
- saveVersioningPreference(pref);
412
- }, []);
413
- const effectiveVersioning = allowVersioning && resolveVersioningEnabled(activeWorkspaceDescriptor, versioningPreference);
414
- const handleOpenWorkspaceSettings = useCallback(() => {
415
- if (!activeWorkspaceDescriptor)
416
- return;
417
- setWorkspaceSettingsOpen(true);
418
- }, [activeWorkspaceDescriptor]);
419
- const handleWorkspaceVersioningOverrideChange = useCallback(async (override) => {
420
- if (!activeWorkspaceDescriptor)
421
- return;
422
- await saveWorkspace({ ...activeWorkspaceDescriptor, versioningOverride: override });
423
- setDescriptorRefreshKey((k) => k + 1);
424
- setWorkspaceSettingsOpen(false);
425
- }, [activeWorkspaceDescriptor]);
426
- const [selectedFile, setSelectedFile] = useState(null);
427
- const [selectedFolder, setSelectedFolder] = useState(null);
428
- const [folderEntries, setFolderEntries] = useState([]);
429
- const [editorContent, setEditorContent] = useState('');
430
- const [editorKey, setEditorKey] = useState(0);
431
- const [explorerKey, setExplorerKey] = useState(0);
432
- const [initialView, setInitialView] = useState('wysiwyg');
433
- // First-run gateway over the welcome doc's Play view — see WELCOME_GATEWAY_KEY.
434
- const [showWelcomeGateway, setShowWelcomeGateway] = useState(false);
435
- /** Suppress popstate handling during programmatic navigation. */
436
- const skipPopState = useRef(false);
437
- const lastLocalSaveRef = useRef(null);
438
- /**
439
- * Per-file media container: for `notes.md`, images live in
440
- * `notes_files/` next to the markdown. Created lazily on first write,
441
- * picked up automatically if it already exists. Set up by the effect
442
- * below whenever the selected file (or backing provider) changes.
443
- */
444
- const mediaContainerRef = useRef(null);
445
- const [mediaProvider, setMediaProvider] = useState(null);
446
- /**
447
- * Per-document container scoped to `<basename>_files/`. This is what
448
- * the editor uses for version history (`.versions/` lives here) and
449
- * for audio mapping (MP3 / timing.json discovery). Distinct from
450
- * `mediaContainerRef` which is scoped to the parent directory so the
451
- * media provider can write `notes_files/image.png` paths that stay
452
- * portable in the markdown.
453
- */
454
- const versionsContainerRef = useRef(null);
455
- const [versionsContainer, setVersionsContainer] = useState(null);
456
- /** Cache of .md files in the active workspace, used to power the
457
- * squisq link-dialog's document picker. Lazily populated on first
458
- * provider call; cleared whenever the backing filesystem changes so
459
- * workspace switches don't surface stale neighbours. A pending Promise
460
- * during in-flight walks lets concurrent calls share the same scan. */
461
- const mdFileCacheRef = useRef(null);
462
- /** Push a new history entry with the given hash. */
463
- const pushHash = useCallback((wsId, filePath) => {
464
- const hash = buildHash(wsId, filePath);
465
- if (window.location.hash !== hash) {
466
- skipPopState.current = true;
467
- window.history.pushState(null, '', hash);
468
- }
469
- }, []);
470
- /** Open a workspace (and optionally a file) from identifiers.
471
- * Returns the FileSystemProvider on success, or null on failure. */
472
- const openFromIds = useCallback(async (wsId, filePath, push, view) => {
473
- const workspaces = await listWorkspaces();
474
- const ws = workspaces.find((w) => w.id === wsId);
475
- if (!ws)
476
- return null;
477
- let fsProvider = null;
478
- if (ws.type === 'electron-native') {
479
- fsProvider = await createElectronProviderFromWorkspace(ws);
480
- if (!fsProvider)
481
- return null;
482
- }
483
- else if (ws.type === 'native') {
484
- const restored = await restoreNativeFolder(ws.id);
485
- if (!restored)
486
- return null;
487
- fsProvider = restored;
488
- }
489
- else {
490
- fsProvider = new IndexedDBFileSystemProvider(ws.id, ws.name);
491
- }
492
- await touchWorkspace(ws.id);
493
- setProvider(fsProvider);
494
- setActiveWorkspaceId(ws.id);
495
- if (filePath) {
496
- const content = await fsProvider.readFile(filePath);
497
- if (content !== null) {
498
- setSelectedFile(filePath);
499
- setSelectedFolder(null);
500
- setFolderEntries([]);
501
- setEditorContent(content);
502
- const effectiveView = view ?? 'wysiwyg';
503
- setInitialView(effectiveView);
504
- setEditorKey((k) => k + 1);
505
- saveLastState({ workspaceId: wsId, filePath, view: effectiveView });
506
- }
507
- else {
508
- setSelectedFile(null);
509
- setSelectedFolder(null);
510
- setFolderEntries([]);
511
- setEditorContent('');
512
- setEditorKey((k) => k + 1);
513
- }
514
- }
515
- else {
516
- setSelectedFile(null);
517
- setSelectedFolder(null);
518
- setFolderEntries([]);
519
- setEditorContent('');
520
- setEditorKey((k) => k + 1);
521
- }
522
- if (push) {
523
- pushHash(ws.id, filePath);
524
- }
525
- return fsProvider;
526
- }, [pushHash]);
527
- const seedWelcomeFile = useCallback(async (fs) => {
528
- const entries = await fs.readDirectory('/');
529
- // If the only file is the welcome doc, auto-select it.
530
- // Match either casing so workspaces seeded before the rename
531
- // (aboutDocblocks.md) keep working alongside new ones (aboutDocBlocks.md).
532
- if (entries.length === 1 &&
533
- entries[0].kind === 'file' &&
534
- entries[0].path.replace(/^\//, '').toLowerCase() === 'aboutdocblocks.md') {
535
- const aboutPath = entries[0].path;
536
- const content = await fs.readFile(aboutPath);
537
- if (content !== null) {
538
- setSelectedFile(aboutPath);
539
- setEditorContent(content);
540
- setInitialView('preview');
541
- setEditorKey((k) => k + 1);
542
- setExplorerKey((k) => k + 1);
543
- pushHash(fs.id, aboutPath);
544
- saveLastState({ workspaceId: fs.id, filePath: aboutPath, view: 'preview' });
545
- if (!isWelcomeGatewayDismissed())
546
- setShowWelcomeGateway(true);
547
- }
548
- return;
549
- }
550
- if (entries.length > 0)
551
- return;
552
- const welcomePath = '/aboutDocBlocks.md';
553
- const welcomeContent = [
554
- '# Welcome to DocBlocks',
555
- '',
556
- 'DocBlocks is a free browser-based markdown document editor that lets you create, organize, and manage your documents right in the browser. What you write here can become a Word or PDF doc, a slide deck, an e-book, or a video.',
557
- '',
558
- 'Simple to write. Beautiful wherever it goes.',
559
- '',
560
- '## Features',
561
- '',
562
- '- **Rich Markdown Editing** — Write in a visual editor or switch to raw markdown anytime. Use section annotations to change the visualization for blocks of content.',
563
- '- **Workspaces** — Organize your documents into separate workspaces in the browser or on your device.',
564
- '- **Useful Everywhere** — Your content is usable across multiple formats — Microsoft Word .docx, PowerPoint, PDF, HTML, EPUB e-books, and Markdown.',
565
- '- **Playback & Video** — Preview your documents as rich visual presentations and export them as MP4 video',
566
- '- **No BS** — Free, no ads, no accounts, no tracking - everything runs locally in your browser',
567
- '',
568
- '## Getting Started',
569
- '',
570
- '1. Create a new file using the **New File** button in the sidebar',
571
- '2. Start writing in markdown — the editor supports headings, lists, links, images, and more',
572
- '3. Your work is saved automatically',
573
- '',
574
- 'Built with [Squiggly Square](https://github.com/bendyline/squisq) by [Bendyline](https://bendyline.com).',
575
- ].join('\n');
576
- await fs.writeFile(welcomePath, welcomeContent);
577
- setSelectedFile(welcomePath);
578
- setEditorContent(welcomeContent);
579
- setInitialView('preview');
580
- setEditorKey((k) => k + 1);
581
- setExplorerKey((k) => k + 1);
582
- pushHash(fs.id, welcomePath);
583
- saveLastState({ workspaceId: fs.id, filePath: welcomePath, view: 'preview' });
584
- if (!isWelcomeGatewayDismissed())
585
- setShowWelcomeGateway(true);
586
- }, [pushHash]);
587
- /** Hide the welcome gateway and never show it again. Safe to call from
588
- * paths where it may not be showing — only persists when it was. */
589
- const closeWelcomeGateway = useCallback(() => {
590
- setShowWelcomeGateway((showing) => {
591
- if (showing)
592
- markWelcomeGatewayDismissed();
593
- return false;
594
- });
595
- }, []);
596
- /** Gateway CTA — flip the welcome doc from Play into the editor. */
597
- const handleStartWriting = useCallback(() => {
598
- closeWelcomeGateway();
599
- setInitialView('wysiwyg');
600
- setEditorKey((k) => k + 1);
601
- if (activeWorkspaceId && selectedFile) {
602
- saveLastState({ workspaceId: activeWorkspaceId, filePath: selectedFile, view: 'wysiwyg' });
603
- }
604
- }, [closeWelcomeGateway, activeWorkspaceId, selectedFile]);
605
- // Initialise workspace on mount — restore from hash or last-used
606
- useEffect(() => {
607
- (async () => {
608
- // Try restoring from URL hash first
609
- const hashState = parseHash();
610
- if (hashState) {
611
- const restoredProvider = await openFromIds(hashState.workspaceId, hashState.filePath, false);
612
- if (restoredProvider) {
613
- // If the hash had no file, check whether we should auto-select the welcome doc
614
- if (!hashState.filePath) {
615
- await seedWelcomeFile(restoredProvider);
616
- }
617
- return;
618
- }
619
- }
620
- // Try restoring last viewed document from localStorage
621
- const lastState = loadLastState();
622
- if (lastState) {
623
- const restoredProvider = await openFromIds(lastState.workspaceId, lastState.filePath, true, lastState.view);
624
- if (restoredProvider)
625
- return;
626
- }
627
- let fsProvider = null;
628
- const electron = isElectronHost();
629
- const workspaces = await listWorkspaces();
630
- // On desktop, hide web-only workspaces (indexeddb/native) — only
631
- // folder-based workspaces are valid.
632
- const candidates = electron
633
- ? workspaces.filter((w) => w.type === 'electron-native')
634
- : workspaces.filter((w) => w.type !== 'electron-native');
635
- // Pick the most recently opened workspace
636
- const sorted = [...candidates].sort((a, b) => (b.lastOpened ?? '').localeCompare(a.lastOpened ?? ''));
637
- for (const ws of sorted) {
638
- if (ws.type === 'electron-native') {
639
- const p = await createElectronProviderFromWorkspace(ws);
640
- if (!p)
641
- continue;
642
- await touchWorkspace(ws.id);
643
- fsProvider = p;
644
- setProvider(p);
645
- setActiveWorkspaceId(ws.id);
646
- break;
647
- }
648
- else if (ws.type === 'native') {
649
- const restored = await restoreNativeFolder(ws.id);
650
- if (restored) {
651
- await touchWorkspace(ws.id);
652
- fsProvider = restored;
653
- setProvider(restored);
654
- setActiveWorkspaceId(ws.id);
655
- break;
656
- }
657
- }
658
- else {
659
- const p = new IndexedDBFileSystemProvider(ws.id, ws.name);
660
- await touchWorkspace(ws.id);
661
- fsProvider = p;
662
- setProvider(p);
663
- setActiveWorkspaceId(ws.id);
664
- break;
665
- }
666
- }
667
- if (!fsProvider) {
668
- if (electron) {
669
- // Desktop: ask the host for the default folder workspace
670
- // (creates ~/Documents/DocBlocks on first launch).
671
- const info = await getDocBlocksHost().workspaces.getDefault();
672
- const descriptor = {
673
- id: info.id,
674
- name: info.name,
675
- type: 'electron-native',
676
- rootPath: info.rootPath,
677
- lastOpened: new Date().toISOString(),
678
- };
679
- await saveWorkspace(descriptor);
680
- const p = new ElectronFileSystemProvider(info.id, info.name, info.rootPath);
681
- fsProvider = p;
682
- setProvider(p);
683
- setActiveWorkspaceId(info.id);
684
- }
685
- else {
686
- // Web: create the IndexedDB default workspace
687
- const defaultWs = await ensureDefaultWorkspace();
688
- const p = new IndexedDBFileSystemProvider(defaultWs.id, defaultWs.name);
689
- fsProvider = p;
690
- setProvider(p);
691
- setActiveWorkspaceId(defaultWs.id);
692
- }
693
- }
694
- // Set initial hash
695
- pushHash(fsProvider.id, null);
696
- // Seed welcome file if workspace is empty
697
- await seedWelcomeFile(fsProvider);
698
- })();
699
- }, [openFromIds, pushHash, seedWelcomeFile]);
700
- // Handle browser back/forward
701
- useEffect(() => {
702
- const onPopState = () => {
703
- if (skipPopState.current) {
704
- skipPopState.current = false;
705
- return;
706
- }
707
- const hashState = parseHash();
708
- if (hashState) {
709
- openFromIds(hashState.workspaceId, hashState.filePath, false);
710
- }
711
- };
712
- window.addEventListener('popstate', onPopState);
713
- return () => window.removeEventListener('popstate', onPopState);
714
- }, [openFromIds]);
715
- // Track view mode changes (Editor/Raw/Play tabs) and persist to localStorage
716
- useEffect(() => {
717
- const handler = (e) => {
718
- const target = e.target.closest?.('[data-view]');
719
- if (target) {
720
- const view = target.getAttribute('data-view');
721
- if (view) {
722
- // The user found the view tabs on their own — the gateway's job is done.
723
- closeWelcomeGateway();
724
- }
725
- if (view && activeWorkspaceId && selectedFile) {
726
- saveLastState({ workspaceId: activeWorkspaceId, filePath: selectedFile, view });
727
- }
728
- }
729
- };
730
- window.addEventListener('click', handler, true);
731
- return () => window.removeEventListener('click', handler, true);
732
- }, [activeWorkspaceId, selectedFile, closeWelcomeGateway]);
733
- const handleAutoSaved = useCallback((filePath, savedContent) => {
734
- lastLocalSaveRef.current = {
735
- filePath: normaliseProviderPath(filePath),
736
- content: savedContent,
737
- savedAt: Date.now(),
738
- };
739
- }, []);
740
- // Auto-save current file. The returned `flush` is called from the
741
- // Ctrl/Cmd+S handler below so the user gets immediate confirmation.
742
- const { flush: flushAutoSave } = useAutoSave(provider, selectedFile, editorContent, 500, handleAutoSaved);
743
- // Comfort-blanket Ctrl/Cmd+S: flushes any pending autosave and pops a
744
- // small "auto-save confirmed" toast. Files are already saved on every
745
- // keystroke (debounced) — this is purely UX reassurance for users who
746
- // muscle-memory hit Save.
747
- const [saveToastVisible, setSaveToastVisible] = useState(false);
748
- const saveToastTimerRef = useRef(null);
749
- useEffect(() => {
750
- const onKey = (e) => {
751
- const sKey = e.key === 's' || e.key === 'S';
752
- const accel = e.ctrlKey || e.metaKey;
753
- if (!sKey || !accel || e.altKey)
754
- return;
755
- e.preventDefault();
756
- e.stopPropagation();
757
- void flushAutoSave().catch(() => undefined);
758
- setSaveToastVisible(true);
759
- if (saveToastTimerRef.current)
760
- clearTimeout(saveToastTimerRef.current);
761
- saveToastTimerRef.current = setTimeout(() => setSaveToastVisible(false), 1800);
762
- };
763
- window.addEventListener('keydown', onKey, true);
764
- return () => window.removeEventListener('keydown', onKey, true);
765
- }, [flushAutoSave]);
766
- useEffect(() => {
767
- return () => {
768
- if (saveToastTimerRef.current)
769
- clearTimeout(saveToastTimerRef.current);
770
- };
771
- }, []);
772
- // Per-file media: for `notes.md` images live in `notes_files/` beside it.
773
- // Rebuilds whenever the provider or selected file changes.
774
- useEffect(() => {
775
- if (!provider || !selectedFile) {
776
- mediaContainerRef.current = null;
777
- versionsContainerRef.current = null;
778
- setMediaProvider(null);
779
- setVersionsContainer(null);
780
- return;
781
- }
782
- const parentDir = dirnameOf(selectedFile);
783
- const base = basenameOf(selectedFile);
784
- const baseNoExt = base.replace(/\.[^.]+$/, '');
785
- const container = new FileSystemContentContainer(provider, parentDir);
786
- const vPrefix = parentDir ? `${parentDir}/${baseNoExt}_files` : `${baseNoExt}_files`;
787
- const vContainer = new FileSystemContentContainer(provider, vPrefix);
788
- const mp = createFileMediaProvider(container, base);
789
- mediaContainerRef.current = container;
790
- versionsContainerRef.current = vContainer;
791
- setMediaProvider(mp);
792
- setVersionsContainer(vContainer);
793
- return () => {
794
- mp.dispose();
795
- };
796
- }, [provider, selectedFile]);
797
- // Invalidate the document-link candidate cache when the backing
798
- // workspace changes — otherwise the link dialog would surface
799
- // neighbours from a previously-open workspace.
800
- useEffect(() => {
801
- mdFileCacheRef.current = null;
802
- }, [provider]);
803
- /** Powers the squisq link dialog's "Browse documents" picker. Returns
804
- * workspace `.md` neighbours filtered by `query`, with paths expressed
805
- * relative to the currently-open document so the link survives folder
806
- * moves. The first call seeds an in-memory cache; subsequent calls
807
- * filter against it. */
808
- const documentLinkProvider = useCallback(async (query) => {
809
- if (!provider || !selectedFile)
810
- return [];
811
- if (!mdFileCacheRef.current) {
812
- mdFileCacheRef.current = collectMarkdownFiles(provider, '').catch(() => []);
813
- }
814
- const entries = await mdFileCacheRef.current;
815
- const q = query.trim().toLowerCase();
816
- const candidates = [];
817
- for (const entry of entries) {
818
- if (entry.kind !== 'file')
819
- continue;
820
- if (sameProviderPath(entry.path, selectedFile))
821
- continue;
822
- const label = entry.name.replace(/\.md$/i, '');
823
- const path = relativeMarkdownLink(selectedFile, entry.path);
824
- if (q && !label.toLowerCase().includes(q) && !path.toLowerCase().includes(q))
825
- continue;
826
- const dir = dirnameOf(entry.path);
827
- candidates.push(dir ? { path, label, description: dir } : { path, label });
828
- }
829
- // Stable alphabetical order keeps the picker predictable across
830
- // re-opens; the dialog can re-sort or rank on its own if needed.
831
- candidates.sort((a, b) => a.label.localeCompare(b.label));
832
- return candidates;
833
- }, [provider, selectedFile]);
834
- // Expose a DocumentVersionManager via versioningRef when requested.
835
- useEffect(() => {
836
- const ref = versioningRef;
837
- if (!ref)
838
- return;
839
- const assign = (mgr) => {
840
- if (typeof ref === 'function')
841
- ref(mgr);
842
- else
843
- ref.current = mgr;
844
- };
845
- if (!effectiveVersioning || !versionsContainer || !selectedFile) {
846
- assign(null);
847
- return;
848
- }
849
- const base = stripExtension(basenameOf(selectedFile));
850
- const mgr = new DocumentVersionManager(versionsContainer, {
851
- basename: versionBasename ?? base,
852
- });
853
- assign(mgr);
854
- return () => assign(null);
855
- }, [versioningRef, effectiveVersioning, versionBasename, selectedFile, versionsContainer]);
856
- // React to external file changes watched by the Electron host (chokidar).
857
- useEffect(() => {
858
- if (!isElectronHost())
859
- return;
860
- if (!provider || !(provider instanceof ElectronFileSystemProvider))
861
- return;
862
- const unwatch = provider.watch((changedPath) => {
863
- setExplorerKey((k) => k + 1);
864
- if (!selectedFile || !sameProviderPath(changedPath, selectedFile))
865
- return;
866
- // If the open file's contents changed on disk, reload it (best-effort).
867
- (async () => {
868
- const content = await provider.readFile(selectedFile);
869
- if (content === null || content === editorContent)
870
- return;
871
- const localSave = lastLocalSaveRef.current;
872
- if (localSave &&
873
- sameProviderPath(localSave.filePath, selectedFile) &&
874
- localSave.content === content &&
875
- Date.now() - localSave.savedAt < 5000) {
876
- return;
877
- }
878
- setEditorContent(content);
879
- setEditorKey((k) => k + 1);
880
- })();
881
- });
882
- return unwatch;
883
- }, [provider, selectedFile, editorContent]);
884
- const handleWorkspaceSelect = useCallback(async (ws) => {
885
- await touchWorkspace(ws.id);
886
- let nextProvider = null;
887
- if (ws.type === 'electron-native') {
888
- nextProvider = await createElectronProviderFromWorkspace(ws);
889
- if (!nextProvider)
890
- return;
891
- }
892
- else if (ws.type === 'native') {
893
- const restored = await restoreNativeFolder(ws.id);
894
- if (!restored) {
895
- // Permission denied or handle lost — fall through without changing provider
896
- return;
897
- }
898
- nextProvider = restored;
899
- }
900
- else {
901
- nextProvider = new IndexedDBFileSystemProvider(ws.id, ws.name);
902
- }
903
- setProvider(nextProvider);
904
- setActiveWorkspaceId(ws.id);
905
- setSelectedFile(null);
906
- setSelectedFolder(null);
907
- setFolderEntries([]);
908
- setEditorContent('');
909
- setEditorKey((k) => k + 1);
910
- pushHash(ws.id, null);
911
- }, [pushHash]);
912
- const handleOpenFolder = useCallback(async () => {
913
- try {
914
- if (isElectronHost()) {
915
- const info = await getDocBlocksHost().workspaces.pickFolder();
916
- if (!info)
917
- return; // user cancelled
918
- const descriptor = {
919
- id: info.id,
920
- name: info.name,
921
- type: 'electron-native',
922
- rootPath: info.rootPath,
923
- lastOpened: new Date().toISOString(),
924
- };
925
- await saveWorkspace(descriptor);
926
- const provider = new ElectronFileSystemProvider(info.id, info.name, info.rootPath);
927
- setProvider(provider);
928
- setActiveWorkspaceId(descriptor.id);
929
- setSelectedFile(null);
930
- setSelectedFolder(null);
931
- setFolderEntries([]);
932
- setEditorContent('');
933
- setEditorKey((k) => k + 1);
934
- pushHash(descriptor.id, null);
935
- return;
936
- }
937
- const nativeProvider = await openNativeFolder();
938
- const descriptor = {
939
- id: nativeProvider.id,
940
- name: nativeProvider.label,
941
- type: 'native',
942
- lastOpened: new Date().toISOString(),
943
- };
944
- await saveWorkspace(descriptor);
945
- setProvider(nativeProvider);
946
- setActiveWorkspaceId(descriptor.id);
947
- setSelectedFile(null);
948
- setSelectedFolder(null);
949
- setFolderEntries([]);
950
- setEditorContent('');
951
- setEditorKey((k) => k + 1);
952
- pushHash(descriptor.id, null);
953
- }
954
- catch {
955
- // User cancelled or API not supported
956
- }
957
- }, [pushHash]);
958
- const handleNewFile = useCallback(async () => {
959
- if (!provider)
960
- return;
961
- const name = prompt('New document name:', 'Untitled.md');
962
- if (!name)
963
- return;
964
- const filename = /\.[a-zA-Z0-9]+$/.test(name) ? name : `${name}.md`;
965
- await provider.writeFile(filename, '# ' + filename.replace(/\.[^.]+$/, '') + '\n');
966
- setSelectedFile('/' + filename);
967
- setEditorContent('# ' + filename.replace(/\.[^.]+$/, '') + '\n');
968
- setInitialView('wysiwyg');
969
- setEditorKey((k) => k + 1);
970
- setExplorerKey((k) => k + 1);
971
- closeWelcomeGateway();
972
- if (activeWorkspaceId) {
973
- pushHash(activeWorkspaceId, '/' + filename);
974
- }
975
- }, [provider, activeWorkspaceId, pushHash, closeWelcomeGateway]);
976
- const handleRevealWorkspace = useCallback(async () => {
977
- if (!isElectronHost() || !activeWorkspaceId)
978
- return;
979
- const ws = await getWorkspace(activeWorkspaceId);
980
- if (ws?.type === 'electron-native' && ws.rootPath) {
981
- await getDocBlocksHost().shell.revealInFolder(ws.rootPath);
982
- }
983
- }, [activeWorkspaceId]);
984
- // Subscribe to native menu commands (Electron host).
985
- useEffect(() => {
986
- if (!isElectronHost())
987
- return;
988
- const host = getDocBlocksHost();
989
- return host.onMenuCommand((cmd) => {
990
- switch (cmd) {
991
- case 'file:new':
992
- handleNewFile();
993
- break;
994
- case 'file:openFolder':
995
- handleOpenFolder();
996
- break;
997
- case 'file:revealWorkspace':
998
- handleRevealWorkspace();
999
- break;
1000
- case 'help:viewOnGitHub':
1001
- host.shell.openExternal('https://github.com/bendyline/docblocks');
1002
- break;
1003
- case 'help:checkForUpdates':
1004
- host.updater.checkForUpdates().catch(() => {
1005
- // errors surface via updater.onStatus
1006
- });
1007
- break;
1008
- case 'help:about':
1009
- (async () => {
1010
- const version = await host.updater.getVersion();
1011
- alert(`DocBlocks ${version}`);
1012
- })();
1013
- break;
1014
- default:
1015
- break;
1016
- }
1017
- });
1018
- }, [handleNewFile, handleOpenFolder, handleRevealWorkspace]);
1019
- // Subscribe to open-file / deep-link requests from the OS.
1020
- useEffect(() => {
1021
- if (!isElectronHost())
1022
- return;
1023
- const host = getDocBlocksHost();
1024
- return host.onOpenRequest(async (req) => {
1025
- await openFromIds(req.workspaceId, req.path, true);
1026
- });
1027
- }, [openFromIds]);
1028
- const handleSelect = useCallback(async (path, kind) => {
1029
- if (!provider || !activeWorkspaceId)
1030
- return;
1031
- if (kind === 'directory') {
1032
- setSelectedFile(null);
1033
- setSelectedFolder(path);
1034
- const entries = await provider.readDirectory(path);
1035
- setFolderEntries(entries);
1036
- setEditorContent('');
1037
- setEditorKey((k) => k + 1);
1038
- pushHash(activeWorkspaceId, null);
1039
- }
1040
- else {
1041
- const content = await provider.readFile(path);
1042
- setSelectedFile(path);
1043
- setSelectedFolder(null);
1044
- setFolderEntries([]);
1045
- setEditorContent(content ?? '');
1046
- setInitialView('wysiwyg');
1047
- setEditorKey((k) => k + 1);
1048
- closeWelcomeGateway();
1049
- pushHash(activeWorkspaceId, path);
1050
- saveLastState({ workspaceId: activeWorkspaceId, filePath: path, view: 'wysiwyg' });
1051
- if (effectiveCompact)
1052
- setMobileShowEditor(true);
1053
- }
1054
- }, [provider, activeWorkspaceId, pushHash, effectiveCompact, closeWelcomeGateway]);
1055
- const handleTreeChange = useCallback(async () => {
1056
- if (!provider)
1057
- return;
1058
- // If the open file was deleted, clear the editor
1059
- if (selectedFile) {
1060
- const exists = await provider.exists(selectedFile);
1061
- if (!exists) {
1062
- setSelectedFile(null);
1063
- setEditorContent('');
1064
- setEditorKey((k) => k + 1);
1065
- if (activeWorkspaceId)
1066
- pushHash(activeWorkspaceId, null);
1067
- }
1068
- }
1069
- if (selectedFolder) {
1070
- const entries = await provider.readDirectory(selectedFolder);
1071
- setFolderEntries(entries);
1072
- }
1073
- }, [provider, selectedFile, selectedFolder, activeWorkspaceId, pushHash]);
1074
- /**
1075
- * Copy non-markdown files from an imported container into the target
1076
- * markdown file's `_files/` sibling folder, preserving the source's
1077
- * relative structure.
1078
- */
1079
- const persistImportedMedia = useCallback(async (source, target, importedMarkdownPath) => {
1080
- const parentDir = dirnameOf(importedMarkdownPath);
1081
- const folder = basenameOf(importedMarkdownPath).replace(/\.[^.]+$/, '') + '_files';
1082
- const mediaRoot = parentDir ? `${parentDir}/${folder}` : folder;
1083
- const entries = await source.listFiles();
1084
- for (const entry of entries) {
1085
- if (entry.path.endsWith('.md'))
1086
- continue;
1087
- const data = await source.readFile(entry.path);
1088
- if (!data)
1089
- continue;
1090
- const cleanPath = entry.path.replace(/^\/+/, '');
1091
- await target.writeBinary(`${mediaRoot}/${cleanPath}`, new Uint8Array(data));
1092
- }
1093
- }, []);
1094
- const handleImportFiles = useCallback(async (files) => {
1095
- if (!provider)
1096
- return;
1097
- for (const file of files) {
1098
- const ext = file.name.slice(file.name.lastIndexOf('.')).toLowerCase();
1099
- const baseName = file.name.replace(/\.[^.]+$/, '');
1100
- const destPath = `${baseName}.md`;
1101
- try {
1102
- let markdown;
1103
- if (ext === '.md' || ext === '.txt') {
1104
- markdown = await file.text();
1105
- }
1106
- else if (ext === '.docx') {
1107
- const { docxToContainer } = await import('@bendyline/squisq-formats/docx');
1108
- const container = await docxToContainer(await file.arrayBuffer());
1109
- markdown = (await container.readDocument()) ?? '';
1110
- await persistImportedMedia(container, provider, destPath);
1111
- }
1112
- else if (ext === '.pdf') {
1113
- const { pdfToContainer } = await import('@bendyline/squisq-formats/pdf');
1114
- const container = await pdfToContainer(await file.arrayBuffer());
1115
- markdown = (await container.readDocument()) ?? '';
1116
- await persistImportedMedia(container, provider, destPath);
1117
- }
1118
- else if (ext === '.dbk' || ext === '.zip') {
1119
- const { zipToContainer } = await import('@bendyline/squisq-formats/container');
1120
- const container = await zipToContainer(await file.arrayBuffer());
1121
- markdown = (await container.readDocument()) ?? '';
1122
- await persistImportedMedia(container, provider, destPath);
1123
- }
1124
- else {
1125
- continue;
1126
- }
1127
- await provider.writeFile(destPath, markdown);
1128
- }
1129
- catch (err) {
1130
- console.error(`Failed to import ${file.name}:`, err);
1131
- }
1132
- }
1133
- // Refresh file tree so the new markdown files + _files folders show up.
1134
- setExplorerKey((k) => k + 1);
1135
- }, [provider, persistImportedMedia]);
1136
- const handleEditorChange = useCallback((source) => {
1137
- setEditorContent(source);
1138
- }, []);
1139
- const handleRenameWorkspace = useCallback(async () => {
1140
- if (!activeWorkspaceId)
1141
- return;
1142
- const ws = await getWorkspace(activeWorkspaceId);
1143
- if (!ws)
1144
- return;
1145
- const newName = prompt('Rename workspace:', ws.name);
1146
- if (!newName || newName === ws.name)
1147
- return;
1148
- await saveWorkspace({ ...ws, name: newName });
1149
- // Bump key to trigger re-render — workspace name is read from the descriptor, not the provider
1150
- setEditorKey((k) => k + 1);
1151
- }, [activeWorkspaceId]);
1152
- /**
1153
- * Walk a FileSystemProvider and copy every file into `container` under
1154
- * `pathPrefix` (no leading slash; empty string for the root). Used by
1155
- * both single- and all-workspace downloads.
1156
- */
1157
- const copyProviderToContainer = useCallback(async (src, container, pathPrefix) => {
1158
- const encoder = new TextEncoder();
1159
- const stack = ['/'];
1160
- while (stack.length > 0) {
1161
- const dir = stack.pop();
1162
- const entries = await src.readDirectory(dir);
1163
- for (const entry of entries) {
1164
- if (entry.kind === 'directory') {
1165
- stack.push(entry.path);
1166
- continue;
1167
- }
1168
- const rel = entry.path.replace(/^\/+/, '');
1169
- const zipPath = pathPrefix ? `${pathPrefix}/${rel}` : rel;
1170
- // Files may be stored as text (writeFile) or binary (writeBinary);
1171
- // try binary first, fall back to text and encode as UTF-8.
1172
- const binary = await src.readBinary(entry.path);
1173
- if (binary) {
1174
- await container.writeFile(zipPath, binary);
1175
- continue;
1176
- }
1177
- const text = await src.readFile(entry.path);
1178
- if (text !== null) {
1179
- await container.writeFile(zipPath, encoder.encode(text));
1180
- }
1181
- }
1182
- }
1183
- }, []);
1184
- const handleDownloadWorkspace = useCallback(async () => {
1185
- if (!provider)
1186
- return;
1187
- try {
1188
- const [{ MemoryContentContainer }, { containerToZip }] = await Promise.all([
1189
- import('@bendyline/squisq/storage'),
1190
- import('@bendyline/squisq-formats/container'),
1191
- ]);
1192
- const container = new MemoryContentContainer();
1193
- await copyProviderToContainer(provider, container, '');
1194
- const blob = await containerToZip(container);
1195
- const url = URL.createObjectURL(blob);
1196
- const a = document.createElement('a');
1197
- a.href = url;
1198
- const safeName = (provider.label || 'workspace').replace(/[^a-z0-9_\- ]/gi, '_').trim() || 'workspace';
1199
- a.download = `${safeName}.zip`;
1200
- a.click();
1201
- URL.revokeObjectURL(url);
1202
- }
1203
- catch (err) {
1204
- console.error('Failed to download workspace', err);
1205
- alert('Failed to download workspace. See console for details.');
1206
- }
1207
- }, [provider, copyProviderToContainer]);
1208
- /**
1209
- * Bundle every workspace the host can open without further prompting
1210
- * into a single zip, with each workspace nested under its own folder.
1211
- * Native (browser-picked) workspaces whose handle hasn't been re-granted
1212
- * for this session are skipped — restoring them would require a user
1213
- * gesture per workspace.
1214
- */
1215
- const handleDownloadAllWorkspaces = useCallback(async () => {
1216
- try {
1217
- const [{ MemoryContentContainer }, { containerToZip }] = await Promise.all([
1218
- import('@bendyline/squisq/storage'),
1219
- import('@bendyline/squisq-formats/container'),
1220
- ]);
1221
- const electron = isElectronHost();
1222
- const all = await listWorkspaces();
1223
- const candidates = all.filter((w) => electron ? w.type === 'electron-native' : w.type !== 'electron-native');
1224
- if (candidates.length === 0) {
1225
- alert('No workspaces to download.');
1226
- return;
1227
- }
1228
- const container = new MemoryContentContainer();
1229
- const usedFolders = new Set();
1230
- const skipped = [];
1231
- for (const ws of candidates) {
1232
- let p = null;
1233
- try {
1234
- if (ws.type === 'electron-native') {
1235
- p = await createElectronProviderFromWorkspace(ws);
1236
- }
1237
- else if (ws.type === 'native') {
1238
- // Restore without prompting — only succeeds when the browser
1239
- // still remembers the granted handle for this origin/session.
1240
- p = await restoreNativeFolder(ws.id);
1241
- }
1242
- else {
1243
- p = new IndexedDBFileSystemProvider(ws.id, ws.name);
1244
- }
1245
- }
1246
- catch (err) {
1247
- console.warn(`Skipping workspace ${ws.name}:`, err);
1248
- }
1249
- if (!p) {
1250
- skipped.push(ws.name);
1251
- continue;
1252
- }
1253
- // Pick a unique, filesystem-safe folder name per workspace.
1254
- const base = (ws.name || 'workspace').replace(/[^a-z0-9_\- ]/gi, '_').trim() || 'workspace';
1255
- let folder = base;
1256
- let suffix = 2;
1257
- while (usedFolders.has(folder)) {
1258
- folder = `${base} (${suffix++})`;
1259
- }
1260
- usedFolders.add(folder);
1261
- await copyProviderToContainer(p, container, folder);
1262
- }
1263
- if (usedFolders.size === 0) {
1264
- alert('No workspaces could be opened for download.');
1265
- return;
1266
- }
1267
- const blob = await containerToZip(container);
1268
- const url = URL.createObjectURL(blob);
1269
- const a = document.createElement('a');
1270
- a.href = url;
1271
- const stamp = new Date().toISOString().slice(0, 10);
1272
- a.download = `docblocks-workspaces-${stamp}.zip`;
1273
- a.click();
1274
- URL.revokeObjectURL(url);
1275
- if (skipped.length > 0) {
1276
- alert(`Downloaded ${usedFolders.size} workspace(s). Skipped ${skipped.length} that require re-granting access: ${skipped.join(', ')}.`);
1277
- }
1278
- }
1279
- catch (err) {
1280
- console.error('Failed to download all workspaces', err);
1281
- alert('Failed to download all workspaces. See console for details.');
1282
- }
1283
- }, [copyProviderToContainer]);
1284
- const handleRemoveWorkspace = useCallback(async () => {
1285
- if (!activeWorkspaceId)
1286
- return;
1287
- const confirmMsg = isElectronHost()
1288
- ? 'Remove this workspace from DocBlocks? The files on disk will not be deleted.'
1289
- : 'Remove this workspace? This cannot be undone.';
1290
- if (!confirm(confirmMsg))
1291
- return;
1292
- const ws = await getWorkspace(activeWorkspaceId);
1293
- if (ws?.type === 'electron-native') {
1294
- try {
1295
- await getDocBlocksHost().workspaces.unregister(activeWorkspaceId);
1296
- }
1297
- catch {
1298
- // ignore — host cleanup is best-effort
1299
- }
1300
- }
1301
- else {
1302
- await removeDirectoryHandle(activeWorkspaceId);
1303
- }
1304
- await removeWorkspace(activeWorkspaceId);
1305
- const electron = isElectronHost();
1306
- // Switch to most recent remaining workspace or create default
1307
- const remaining = (await listWorkspaces()).filter((w) => electron ? w.type === 'electron-native' : w.type !== 'electron-native');
1308
- if (remaining.length > 0) {
1309
- const next = remaining[0];
1310
- await handleWorkspaceSelect(next);
1311
- }
1312
- else if (electron) {
1313
- const info = await getDocBlocksHost().workspaces.getDefault();
1314
- const descriptor = {
1315
- id: info.id,
1316
- name: info.name,
1317
- type: 'electron-native',
1318
- rootPath: info.rootPath,
1319
- lastOpened: new Date().toISOString(),
1320
- };
1321
- await saveWorkspace(descriptor);
1322
- const p = new ElectronFileSystemProvider(info.id, info.name, info.rootPath);
1323
- setProvider(p);
1324
- setActiveWorkspaceId(info.id);
1325
- setSelectedFile(null);
1326
- setSelectedFolder(null);
1327
- setFolderEntries([]);
1328
- setEditorContent('');
1329
- setEditorKey((k) => k + 1);
1330
- }
1331
- else {
1332
- const defaultWs = await ensureDefaultWorkspace();
1333
- const fsProvider = new IndexedDBFileSystemProvider(defaultWs.id, defaultWs.name);
1334
- setProvider(fsProvider);
1335
- setActiveWorkspaceId(defaultWs.id);
1336
- setSelectedFile(null);
1337
- setSelectedFolder(null);
1338
- setFolderEntries([]);
1339
- setEditorContent('');
1340
- setEditorKey((k) => k + 1);
1341
- }
1342
- }, [activeWorkspaceId, handleWorkspaceSelect]);
1343
- return (_jsxs("div", { className: `db-shell${effectiveCompact ? ' db-shell--mobile' : ''}`, "data-theme": resolvedTheme, children: [saveToastVisible && (_jsx("div", { className: "db-save-toast", role: "status", "aria-live": "polite", children: "Autosaved. You're all set." })), workspaceSettingsOpen && activeWorkspaceDescriptor && (_jsx(WorkspaceSettingsDialog, { workspace: activeWorkspaceDescriptor, globalVersioningPreference: versioningPreference, onChange: handleWorkspaceVersioningOverrideChange, onClose: () => setWorkspaceSettingsOpen(false) })), _jsxs("div", { style: { display: 'flex', flex: 1, overflow: 'hidden' }, children: [(!effectiveCompact || !mobileShowEditor) && (_jsxs("div", { ref: sidebarRef, className: "db-shell-sidebar", style: effectiveCompact ? undefined : { width: `${sidebarWidth}px` }, children: [_jsxs("div", { className: "db-shell-sidebar-header", children: [_jsx(AppMenu, { logoUrl: logoUrl, themePreference: themePreference, onThemeChange: handleThemeChange, versioningPreference: versioningPreference, onVersioningPreferenceChange: handleVersioningPreferenceChange, onDownloadAllWorkspaces: handleDownloadAllWorkspaces }), _jsx(WorkspacePicker, { activeWorkspaceId: activeWorkspaceId, onSelect: handleWorkspaceSelect, onOpenFolder: handleOpenFolder }), _jsx(WorkspaceSettingsButton, { onSettings: handleOpenWorkspaceSettings, onRename: handleRenameWorkspace, onDownload: handleDownloadWorkspace, onRemove: handleRemoveWorkspace })] }), _jsx(FileExplorer, { provider: provider, onSelect: handleSelect, onTreeChange: handleTreeChange, onImportFiles: handleImportFiles }, explorerKey), _jsx("div", { className: "db-shell-sidebar-footer", children: _jsx("a", { href: "https://github.com/bendyline/docblocks/blob/main/LICENSE", target: "_blank", rel: "noopener noreferrer", children: "Terms of Use" }) })] })), !effectiveCompact && (_jsx("div", { className: "db-shell-sidebar-resizer", role: "separator", "aria-orientation": "vertical", "aria-label": "Resize sidebar", onPointerDown: handleResizerPointerDown })), (!effectiveCompact || mobileShowEditor) && (_jsx("div", { style: {
1344
- flex: 1,
1345
- overflow: 'hidden',
1346
- display: 'flex',
1347
- flexDirection: 'column',
1348
- position: 'relative',
1349
- }, children: selectedFile && mediaProvider ? (_jsxs(MediaContext.Provider, { value: mediaProvider, children: [_jsx(EditorShell, { initialMarkdown: editorContent, initialView: initialView, articleId: selectedFile, fileName: selectedFile, onChange: handleEditorChange, colorScheme: resolvedTheme, height: "100%", outlineWidth: 280, mediaProvider: mediaProvider, documentLinkProvider: documentLinkProvider, container: versionsContainer ?? undefined, allowVersioning: effectiveVersioning, viewPreferences: viewPreferences, onViewPreferencesChange: handleViewPreferencesChange, versionBasename: versionBasename ?? stripExtension(basenameOf(selectedFile)), versioningPrunePolicy: versioningPrunePolicy, versioningAutoSaveIdleMs: versioningAutoSaveIdleMs, onSaveVersion: onSaveVersion, toolbarSlotLeft: effectiveCompact ? (_jsx("button", { className: "db-mobile-back", onClick: () => setMobileShowEditor(false), "aria-label": "Show file list", children: _jsx("span", { className: "db-mobile-back-arrow", children: "\u2190" }) })) : undefined, toolbarSlotRight: _jsxs(_Fragment, { children: [compactLayout && !isMobile && (_jsx("button", { className: "db-restore-split", onClick: () => setCompactLayout(false), "aria-label": "Restore split view", title: "Restore split view", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("rect", { x: "1.5", y: "2.5", width: "13", height: "11", rx: "1" }), _jsx("line", { x1: "6", y1: "2.5", x2: "6", y2: "13.5" })] }) })), _jsx(ExportToolbarControls, { selectedFile: selectedFile, mediaContainer: mediaContainerRef.current })] }) }, `${selectedFile}-${editorKey}`), showWelcomeGateway && (_jsxs("div", { className: "db-welcome-gateway", role: "note", "aria-label": "Welcome tip", children: [_jsxs("span", { className: "db-welcome-gateway-text", children: ["You\u2019re watching this welcome doc in ", _jsx("strong", { children: "Play" }), " view \u2014 it\u2019s a regular markdown file, and so is everything you\u2019ll write."] }), _jsx("button", { className: "db-welcome-gateway-cta", onClick: handleStartWriting, children: "Start writing" }), _jsx("button", { className: "db-welcome-gateway-dismiss", onClick: closeWelcomeGateway, "aria-label": "Dismiss welcome tip", title: "Dismiss", children: "\u00D7" })] }))] })) : selectedFolder ? (_jsxs("div", { className: "db-folder-view", children: [effectiveCompact && (_jsxs("button", { className: "db-mobile-back", onClick: () => setMobileShowEditor(false), children: [_jsx("span", { className: "db-mobile-back-arrow", children: "\u2190" }), "Back to files"] })), _jsxs("div", { className: "db-folder-view-header", children: [_jsx("span", { className: "db-folder-view-icon", children: _jsx(FolderGlyph, {}) }), _jsx("span", { className: "db-folder-view-path", children: selectedFolder })] }), folderEntries.length === 0 ? (_jsx("p", { className: "db-folder-view-empty", children: "This folder is empty." })) : (_jsx("ul", { className: "db-folder-view-list", children: folderEntries.map((entry) => (_jsxs("li", { className: "db-folder-view-item", onClick: () => handleSelect(entry.path, entry.kind), children: [_jsx("span", { className: "db-folder-view-item-icon", children: entry.kind === 'directory' ? _jsx(FolderGlyph, {}) : _jsx(FileGlyph, {}) }), entry.name] }, entry.path))) }))] })) : (_jsxs("div", { className: "db-shell-empty", children: [effectiveCompact && (_jsxs("button", { className: "db-mobile-back", onClick: () => setMobileShowEditor(false), children: [_jsx("span", { className: "db-mobile-back-arrow", children: "\u2190" }), "Back to files"] })), _jsx("p", { children: "Select a file to start editing, or create a new one." })] })) }))] })] }));
1350
- }
1351
- //# sourceMappingURL=DocBlocksShell.js.map