@sutrata/editor 0.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 (337) hide show
  1. package/LICENSE +674 -0
  2. package/dist/ai/OnboardingDialog.d.ts +6 -0
  3. package/dist/ai/OnboardingDialog.js +115 -0
  4. package/dist/ai/VoiceConfirmDialog.d.ts +9 -0
  5. package/dist/ai/VoiceConfirmDialog.js +37 -0
  6. package/dist/ai/VoiceToolbar.d.ts +2 -0
  7. package/dist/ai/VoiceToolbar.js +177 -0
  8. package/dist/ai/ai-client.d.ts +71 -0
  9. package/dist/ai/ai-client.js +494 -0
  10. package/dist/ai/commands.d.ts +12 -0
  11. package/dist/ai/commands.js +140 -0
  12. package/dist/ai/generated-providers.json +3696 -0
  13. package/dist/ai/prompts.d.ts +25 -0
  14. package/dist/ai/prompts.js +59 -0
  15. package/dist/ai/voice-service.d.ts +15 -0
  16. package/dist/ai/voice-service.js +81 -0
  17. package/dist/autocomplete/AutocompletePlugin.d.ts +2 -0
  18. package/dist/autocomplete/AutocompletePlugin.js +19 -0
  19. package/dist/autocomplete/suggestions.d.ts +25 -0
  20. package/dist/autocomplete/suggestions.js +126 -0
  21. package/dist/context/DocumentContext.d.ts +68 -0
  22. package/dist/context/DocumentContext.js +605 -0
  23. package/dist/desktop-api.d.ts +40 -0
  24. package/dist/editor/EditorView.d.ts +2 -0
  25. package/dist/editor/EditorView.js +78 -0
  26. package/dist/editor/SelectionMenu.d.ts +2 -0
  27. package/dist/editor/SelectionMenu.js +102 -0
  28. package/dist/editor/attr-pencil-plugin.d.ts +17 -0
  29. package/dist/editor/attr-pencil-plugin.js +158 -0
  30. package/dist/editor/character-decoration-plugin.d.ts +26 -0
  31. package/dist/editor/character-decoration-plugin.js +77 -0
  32. package/dist/editor/character-view.d.ts +35 -0
  33. package/dist/editor/character-view.js +112 -0
  34. package/dist/editor/editor-bus.d.ts +14 -0
  35. package/dist/editor/editor-bus.js +39 -0
  36. package/dist/editor/find-highlight-plugin.d.ts +7 -0
  37. package/dist/editor/find-highlight-plugin.js +57 -0
  38. package/dist/editor/frontmatter-field-view.d.ts +10 -0
  39. package/dist/editor/frontmatter-field-view.js +48 -0
  40. package/dist/editor/frontmatter-field.d.ts +21 -0
  41. package/dist/editor/frontmatter-field.js +116 -0
  42. package/dist/editor/grapheme-cursor.d.ts +10 -0
  43. package/dist/editor/grapheme-cursor.js +104 -0
  44. package/dist/editor/ime-plugin.d.ts +18 -0
  45. package/dist/editor/ime-plugin.js +71 -0
  46. package/dist/editor/insert-helper.d.ts +1 -0
  47. package/dist/editor/insert-helper.js +42 -0
  48. package/dist/editor/keymap.d.ts +3 -0
  49. package/dist/editor/keymap.js +131 -0
  50. package/dist/editor/language-class-plugin.d.ts +7 -0
  51. package/dist/editor/language-class-plugin.js +75 -0
  52. package/dist/editor/plugins.d.ts +1 -0
  53. package/dist/editor/plugins.js +25 -0
  54. package/dist/editor/prosemirror-to-sutra.d.ts +2 -0
  55. package/dist/editor/prosemirror-to-sutra.js +208 -0
  56. package/dist/editor/scene-heading-view.d.ts +27 -0
  57. package/dist/editor/scene-heading-view.js +136 -0
  58. package/dist/editor/schema.d.ts +3 -0
  59. package/dist/editor/schema.js +135 -0
  60. package/dist/editor/sutra-to-prosemirror.d.ts +3 -0
  61. package/dist/editor/sutra-to-prosemirror.js +154 -0
  62. package/dist/editor/transliterate-input.d.ts +10 -0
  63. package/dist/editor/transliterate-input.js +88 -0
  64. package/dist/extensions/storage-adapter.d.ts +59 -0
  65. package/dist/extensions/storage-adapter.js +9 -0
  66. package/dist/extensions/types.d.ts +73 -0
  67. package/dist/extensions/types.js +12 -0
  68. package/dist/file/docx-exporter.d.ts +8 -0
  69. package/dist/file/docx-exporter.js +494 -0
  70. package/dist/file/docx-importer.d.ts +10 -0
  71. package/dist/file/docx-importer.js +283 -0
  72. package/dist/file/print-paginator.d.ts +29 -0
  73. package/dist/file/print-paginator.js +289 -0
  74. package/dist/file/templates.d.ts +2 -0
  75. package/dist/file/templates.js +66 -0
  76. package/dist/file/workflow-reports.d.ts +34 -0
  77. package/dist/file/workflow-reports.js +719 -0
  78. package/dist/findreplace/FindReplace.d.ts +2 -0
  79. package/dist/findreplace/FindReplace.js +164 -0
  80. package/dist/findreplace/find-engine.d.ts +29 -0
  81. package/dist/findreplace/find-engine.js +69 -0
  82. package/dist/findreplace/scroll-to-match.d.ts +29 -0
  83. package/dist/findreplace/scroll-to-match.js +74 -0
  84. package/dist/fonts/tofu-detect.d.ts +1 -0
  85. package/dist/fonts/tofu-detect.js +48 -0
  86. package/dist/i18n/LanguageContext.d.ts +10 -0
  87. package/dist/i18n/LanguageContext.js +174 -0
  88. package/dist/i18n/language-detect.d.ts +1 -0
  89. package/dist/i18n/language-detect.js +58 -0
  90. package/dist/i18n/useTranslation.d.ts +11 -0
  91. package/dist/i18n/useTranslation.js +60 -0
  92. package/dist/index.d.ts +11 -0
  93. package/dist/index.js +5 -0
  94. package/dist/locales/bn.json +143 -0
  95. package/dist/locales/en.json +143 -0
  96. package/dist/locales/gu.json +143 -0
  97. package/dist/locales/hi.json +143 -0
  98. package/dist/locales/kn.json +143 -0
  99. package/dist/locales/ml.json +143 -0
  100. package/dist/locales/mr.json +143 -0
  101. package/dist/locales/or.json +143 -0
  102. package/dist/locales/pa.json +143 -0
  103. package/dist/locales/si.json +143 -0
  104. package/dist/locales/ta.json +143 -0
  105. package/dist/locales/te.json +143 -0
  106. package/dist/navigator/SceneNavigator.d.ts +2 -0
  107. package/dist/navigator/SceneNavigator.js +612 -0
  108. package/dist/navigator/StatisticsDialog.d.ts +6 -0
  109. package/dist/navigator/StatisticsDialog.js +146 -0
  110. package/dist/navigator/scene-list.d.ts +16 -0
  111. package/dist/navigator/scene-list.js +70 -0
  112. package/dist/settings/SettingsDialog.d.ts +6 -0
  113. package/dist/settings/SettingsDialog.js +147 -0
  114. package/dist/shell/AppBar.d.ts +2 -0
  115. package/dist/shell/AppBar.js +34 -0
  116. package/dist/shell/AppShell.d.ts +2 -0
  117. package/dist/shell/AppShell.js +30 -0
  118. package/dist/shell/ConfirmDialog.d.ts +16 -0
  119. package/dist/shell/ConfirmDialog.js +23 -0
  120. package/dist/shell/ElementToolbar.d.ts +2 -0
  121. package/dist/shell/ElementToolbar.js +30 -0
  122. package/dist/shell/LanguageStatus.d.ts +2 -0
  123. package/dist/shell/LanguageStatus.js +65 -0
  124. package/dist/shell/StatusBar.d.ts +2 -0
  125. package/dist/shell/StatusBar.js +54 -0
  126. package/dist/shell/SutrataLogo.d.ts +8 -0
  127. package/dist/shell/SutrataLogo.js +7 -0
  128. package/dist/shell/Toast.d.ts +12 -0
  129. package/dist/shell/Toast.js +21 -0
  130. package/dist/shell/ToolbarButton.d.ts +11 -0
  131. package/dist/shell/ToolbarButton.js +4 -0
  132. package/dist/shell/VersionHistory.d.ts +12 -0
  133. package/dist/shell/VersionHistory.js +89 -0
  134. package/dist/shell/Watermark.d.ts +3 -0
  135. package/dist/shell/Watermark.js +10 -0
  136. package/dist/shell/fonts/fonts.css +1133 -0
  137. package/dist/shell/icons.d.ts +45 -0
  138. package/dist/shell/icons.js +51 -0
  139. package/dist/shell/toolbar-actions.d.ts +6 -0
  140. package/dist/shell/toolbar-actions.js +58 -0
  141. package/dist/source/SourceView.d.ts +1 -0
  142. package/dist/source/SourceView.js +106 -0
  143. package/dist/source/find-highlight-source.d.ts +14 -0
  144. package/dist/source/find-highlight-source.js +47 -0
  145. package/dist/source/sutra-lang.d.ts +2 -0
  146. package/dist/source/sutra-lang.js +97 -0
  147. package/dist/spellcheck/dictionaries.d.ts +9 -0
  148. package/dist/spellcheck/dictionaries.js +16 -0
  149. package/dist/spellcheck/hunspell.d.ts +23 -0
  150. package/dist/spellcheck/hunspell.js +94 -0
  151. package/dist/spellcheck/spellcheck-plugin.d.ts +18 -0
  152. package/dist/spellcheck/spellcheck-plugin.js +75 -0
  153. package/dist/styles/StyleDialog.d.ts +6 -0
  154. package/dist/styles/StyleDialog.js +63 -0
  155. package/dist/styles/builtin-styles.d.ts +43 -0
  156. package/dist/styles/builtin-styles.js +131 -0
  157. package/dist/styles/css-adapter.d.ts +32 -0
  158. package/dist/styles/css-adapter.js +71 -0
  159. package/dist/styles/docx-adapter.d.ts +7 -0
  160. package/dist/styles/docx-adapter.js +84 -0
  161. package/dist/styles/embed-fonts.d.ts +20 -0
  162. package/dist/styles/embed-fonts.js +81 -0
  163. package/dist/styles/pagination.d.ts +19 -0
  164. package/dist/styles/pagination.js +22 -0
  165. package/dist/styles/print-adapter.d.ts +6 -0
  166. package/dist/styles/print-adapter.js +288 -0
  167. package/dist/styles/registry.d.ts +6 -0
  168. package/dist/styles/registry.js +14 -0
  169. package/dist/styles/screenplay.css +3006 -0
  170. package/dist/styles/script-ranges.d.ts +6 -0
  171. package/dist/styles/script-ranges.js +17 -0
  172. package/dist/styles/types.d.ts +93 -0
  173. package/dist/styles/types.js +18 -0
  174. package/dist/styles/validate.d.ts +12 -0
  175. package/dist/styles/validate.js +105 -0
  176. package/dist/templates/templates.d.ts +10 -0
  177. package/dist/templates/templates.js +181 -0
  178. package/dist/titlepage/ExportDialog.d.ts +1 -0
  179. package/dist/titlepage/ExportDialog.js +152 -0
  180. package/dist/titlepage/TitlePageDialog.d.ts +6 -0
  181. package/dist/titlepage/TitlePageDialog.js +96 -0
  182. package/dist/titlepage/frontmatter-form.d.ts +56 -0
  183. package/dist/titlepage/frontmatter-form.js +245 -0
  184. package/dist/titlepage/languages.d.ts +10 -0
  185. package/dist/titlepage/languages.js +31 -0
  186. package/dist/types.d.ts +6 -0
  187. package/dist/types.js +1 -0
  188. package/package.json +47 -0
  189. package/scripts/copy-assets.mjs +16 -0
  190. package/scripts/generate-providers.mjs +127 -0
  191. package/src/ai/OnboardingDialog.tsx +390 -0
  192. package/src/ai/VoiceConfirmDialog.tsx +137 -0
  193. package/src/ai/VoiceToolbar.tsx +321 -0
  194. package/src/ai/ai-client.ts +596 -0
  195. package/src/ai/commands.ts +164 -0
  196. package/src/ai/generated-providers.json +3696 -0
  197. package/src/ai/prompts.ts +62 -0
  198. package/src/ai/voice-service.ts +96 -0
  199. package/src/autocomplete/AutocompletePlugin.ts +21 -0
  200. package/src/autocomplete/suggestions.ts +147 -0
  201. package/src/context/DocumentContext.tsx +648 -0
  202. package/src/desktop-api.d.ts +40 -0
  203. package/src/editor/EditorView.tsx +82 -0
  204. package/src/editor/SelectionMenu.tsx +152 -0
  205. package/src/editor/attr-pencil-plugin.ts +172 -0
  206. package/src/editor/character-decoration-plugin.ts +104 -0
  207. package/src/editor/editor-bus.ts +51 -0
  208. package/src/editor/find-highlight-plugin.ts +68 -0
  209. package/src/editor/frontmatter-field-view.ts +52 -0
  210. package/src/editor/frontmatter-field.ts +115 -0
  211. package/src/editor/grapheme-cursor.ts +99 -0
  212. package/src/editor/ime-plugin.ts +85 -0
  213. package/src/editor/insert-helper.ts +43 -0
  214. package/src/editor/keymap.ts +124 -0
  215. package/src/editor/language-class-plugin.ts +91 -0
  216. package/src/editor/plugins.ts +26 -0
  217. package/src/editor/prosemirror-to-sutra.ts +192 -0
  218. package/src/editor/scene-heading-view.ts +147 -0
  219. package/src/editor/schema.ts +156 -0
  220. package/src/editor/sutra-to-prosemirror.ts +156 -0
  221. package/src/extensions/storage-adapter.ts +65 -0
  222. package/src/extensions/types.ts +60 -0
  223. package/src/file/docx-exporter.ts +585 -0
  224. package/src/file/docx-importer.ts +313 -0
  225. package/src/file/print-paginator.ts +301 -0
  226. package/src/file/templates.ts +73 -0
  227. package/src/file/workflow-reports.ts +820 -0
  228. package/src/findreplace/FindReplace.tsx +242 -0
  229. package/src/findreplace/find-engine.ts +97 -0
  230. package/src/findreplace/scroll-to-match.ts +89 -0
  231. package/src/fonts/tofu-detect.ts +53 -0
  232. package/src/i18n/LanguageContext.tsx +207 -0
  233. package/src/i18n/language-detect.ts +73 -0
  234. package/src/i18n/useTranslation.tsx +74 -0
  235. package/src/index.ts +27 -0
  236. package/src/locales/bn.json +143 -0
  237. package/src/locales/en.json +143 -0
  238. package/src/locales/gu.json +143 -0
  239. package/src/locales/hi.json +143 -0
  240. package/src/locales/kn.json +143 -0
  241. package/src/locales/ml.json +143 -0
  242. package/src/locales/mr.json +143 -0
  243. package/src/locales/or.json +143 -0
  244. package/src/locales/pa.json +143 -0
  245. package/src/locales/si.json +143 -0
  246. package/src/locales/ta.json +143 -0
  247. package/src/locales/te.json +143 -0
  248. package/src/navigator/SceneNavigator.tsx +844 -0
  249. package/src/navigator/StatisticsDialog.tsx +249 -0
  250. package/src/navigator/scene-list.ts +84 -0
  251. package/src/settings/SettingsDialog.tsx +303 -0
  252. package/src/shell/AppBar.tsx +354 -0
  253. package/src/shell/AppShell.tsx +71 -0
  254. package/src/shell/ConfirmDialog.tsx +71 -0
  255. package/src/shell/ElementToolbar.tsx +175 -0
  256. package/src/shell/LanguageStatus.tsx +101 -0
  257. package/src/shell/StatusBar.tsx +135 -0
  258. package/src/shell/SutrataLogo.tsx +89 -0
  259. package/src/shell/Toast.tsx +46 -0
  260. package/src/shell/ToolbarButton.tsx +30 -0
  261. package/src/shell/VersionHistory.tsx +212 -0
  262. package/src/shell/Watermark.tsx +9 -0
  263. package/src/shell/fonts/fonts.css +1133 -0
  264. package/src/shell/icons.tsx +335 -0
  265. package/src/shell/toolbar-actions.ts +59 -0
  266. package/src/source/SourceView.tsx +111 -0
  267. package/src/source/find-highlight-source.ts +57 -0
  268. package/src/source/sutra-lang.ts +121 -0
  269. package/src/spellcheck/dictionaries.ts +21 -0
  270. package/src/spellcheck/hunspell.ts +110 -0
  271. package/src/spellcheck/spellcheck-plugin.ts +83 -0
  272. package/src/styles/StyleDialog.tsx +148 -0
  273. package/src/styles/builtin-styles.ts +139 -0
  274. package/src/styles/css-adapter.ts +76 -0
  275. package/src/styles/docx-adapter.ts +109 -0
  276. package/src/styles/embed-fonts.ts +81 -0
  277. package/src/styles/pagination.ts +27 -0
  278. package/src/styles/print-adapter.ts +288 -0
  279. package/src/styles/registry.ts +20 -0
  280. package/src/styles/screenplay.css +3006 -0
  281. package/src/styles/script-ranges.ts +17 -0
  282. package/src/styles/types.ts +104 -0
  283. package/src/styles/validate.ts +114 -0
  284. package/src/templates/templates.ts +194 -0
  285. package/src/titlepage/ExportDialog.tsx +494 -0
  286. package/src/titlepage/TitlePageDialog.tsx +312 -0
  287. package/src/titlepage/frontmatter-form.ts +280 -0
  288. package/src/titlepage/languages.ts +37 -0
  289. package/src/types.ts +7 -0
  290. package/tests/AppBar.test.tsx +35 -0
  291. package/tests/AppShell.test.tsx +22 -0
  292. package/tests/ElementToolbar.test.tsx +36 -0
  293. package/tests/LanguageStatus.test.tsx +129 -0
  294. package/tests/SelectionMenu.test.tsx +71 -0
  295. package/tests/ToolbarButton.test.tsx +24 -0
  296. package/tests/ai-onboarding.test.ts +102 -0
  297. package/tests/ai-status.test.tsx +72 -0
  298. package/tests/ai-structured.test.ts +94 -0
  299. package/tests/autocomplete/AutocompletePlugin.test.ts +20 -0
  300. package/tests/autocomplete/suggestions.test.ts +106 -0
  301. package/tests/document-context.test.tsx +565 -0
  302. package/tests/docx-importer.test.ts +160 -0
  303. package/tests/editor/character-decoration-plugin.test.ts +101 -0
  304. package/tests/editor/character-extension.test.ts +44 -0
  305. package/tests/editor/grapheme-cursor.test.ts +118 -0
  306. package/tests/editor/ime-plugin.test.ts +72 -0
  307. package/tests/editor/inline-note-bridge.test.ts +32 -0
  308. package/tests/editor/language-class-plugin.test.ts +265 -0
  309. package/tests/editor/title-page.test.ts +124 -0
  310. package/tests/embed-fonts.test.ts +17 -0
  311. package/tests/exporters.test.ts +365 -0
  312. package/tests/findreplace/FindReplace.test.tsx +391 -0
  313. package/tests/findreplace/find-engine.test.ts +86 -0
  314. package/tests/findreplace/scroll-to-match.test.ts +50 -0
  315. package/tests/fonts/tofu-detect.test.ts +122 -0
  316. package/tests/i18n/LanguageContext.test.tsx +146 -0
  317. package/tests/i18n/language-detect.test.ts +54 -0
  318. package/tests/i18n/locale-parity.test.ts +25 -0
  319. package/tests/i18n/useTranslation.test.tsx +45 -0
  320. package/tests/keymap.test.ts +18 -0
  321. package/tests/navigator/SceneNavigator.test.tsx +177 -0
  322. package/tests/navigator/StatisticsDialog.test.tsx +63 -0
  323. package/tests/navigator/scene-list.test.ts +71 -0
  324. package/tests/print-paginator.test.ts +82 -0
  325. package/tests/shell/VersionHistory.test.tsx +129 -0
  326. package/tests/spellcheck/hunspell.test.ts +82 -0
  327. package/tests/spellcheck/spellcheck-plugin.test.ts +56 -0
  328. package/tests/styles.test.ts +315 -0
  329. package/tests/templates/templates.test.ts +83 -0
  330. package/tests/titlepage/TitlePageDialog.test.tsx +90 -0
  331. package/tests/titlepage/apply-title-page.test.ts +61 -0
  332. package/tests/titlepage/frontmatter-form.test.ts +115 -0
  333. package/tests/toolbar-actions.test.ts +93 -0
  334. package/tests/voice-commands.test.ts +58 -0
  335. package/tsconfig.json +17 -0
  336. package/vitest.config.ts +11 -0
  337. package/vitest.setup.ts +65 -0
@@ -0,0 +1,605 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useState, useCallback, useEffect, useMemo, useRef } from 'react';
3
+ import { parse, importFountain, fountainDocToSutra } from '@sutrata/parser';
4
+ import { StorageAdapterProvider } from '../extensions/storage-adapter';
5
+ import { importDocx as importDocxToSutra } from '../file/docx-importer';
6
+ import { runCommand, getEditorView } from '../editor/editor-bus';
7
+ import { setFlatFrontmatterField } from '../editor/frontmatter-field';
8
+ import { makeSetBlock, makeSetOrToggleNote } from '../shell/toolbar-actions';
9
+ import { resolveStyle } from '../styles/registry';
10
+ const DocumentContext = createContext(null);
11
+ const EMPTY_DOC = '## Scene 1\n\nAction.\n';
12
+ // Points at whichever autosave key (a real filePath, or '__autosave__' for an
13
+ // untitled doc) currently holds unsaved-but-not-yet-file-saved content, so a
14
+ // reload can find it. Cleared once the content is genuinely saved to a file.
15
+ const RECOVERY_POINTER_KEY = 'sutrata:recovery-pointer';
16
+ // A save fires shortly after the user pauses...
17
+ const AUTOSAVE_PAUSE_MS = 3_000;
18
+ // ...and unconditionally at this interval even if they never pause (previously
19
+ // this was a pure 30s debounce that never fired while the user kept typing).
20
+ const AUTOSAVE_MAX_WAIT_MS = 30_000;
21
+ function readRecoveryPointer() {
22
+ try {
23
+ const raw = localStorage.getItem(RECOVERY_POINTER_KEY);
24
+ if (!raw)
25
+ return null;
26
+ const parsed = JSON.parse(raw);
27
+ if (typeof parsed.path !== 'string' || typeof parsed.updatedAt !== 'number')
28
+ return null;
29
+ return parsed;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ function writeRecoveryPointer(path) {
36
+ try {
37
+ localStorage.setItem(RECOVERY_POINTER_KEY, JSON.stringify({ path, updatedAt: Date.now() }));
38
+ }
39
+ catch {
40
+ // localStorage unavailable (private browsing, quota) — recovery just won't work this session.
41
+ }
42
+ }
43
+ function clearRecoveryPointer() {
44
+ try {
45
+ localStorage.removeItem(RECOVERY_POINTER_KEY);
46
+ }
47
+ catch {
48
+ // ignore
49
+ }
50
+ }
51
+ export function DocumentProvider({ children, storageAdapter }) {
52
+ const [text, setTextState] = useState(EMPTY_DOC);
53
+ const [ast, setAst] = useState(() => parse(EMPTY_DOC));
54
+ const [mode, setMode] = useState('formatted');
55
+ const [filePath, setFilePath] = useState(null);
56
+ const [isDirty, setIsDirty] = useState(false);
57
+ // Where the most recent save attempt went. isDirty tracks "does this differ
58
+ // from the real file on disk" (VS Code-style) — a local autosave backs the
59
+ // content up but does NOT clear isDirty, only saveFile()/saveFileAs() does.
60
+ // lastSaveTarget just tells the UI which kind of save last happened, so it
61
+ // can show e.g. "backed up locally, not yet saved" while still dirty.
62
+ const [lastSaveTarget, setLastSaveTarget] = useState(null);
63
+ const [ribbonVisible, setRibbonVisible] = useState(true);
64
+ const [navVisible, setNavVisible] = useState(() => window.innerWidth > 640);
65
+ const [exportVisible, setExportVisible] = useState(false);
66
+ const [settingsVisible, setSettingsVisible] = useState(false);
67
+ const [aiOnboardingVisible, setAIOnboardingVisible] = useState(false);
68
+ const [voiceActive, setVoiceActive] = useState(false);
69
+ const [metadataVisible, setMetadataVisible] = useState(true);
70
+ const [findReplaceVisible, setFindReplaceVisible] = useState(false);
71
+ const [findMode, setFindMode] = useState('find');
72
+ const [styleVisible, setStyleVisible] = useState(false);
73
+ const [customStyles, setCustomStyles] = useState([]);
74
+ const [confirmModal, setConfirmModal] = useState(null);
75
+ const [toasts, setToasts] = useState([]);
76
+ const [versions, setVersions] = useState([]);
77
+ const [versionHistoryVisible, setVersionHistoryVisible] = useState(false);
78
+ const showConfirm = useCallback((options) => {
79
+ setConfirmModal(options);
80
+ }, []);
81
+ const closeConfirm = useCallback(() => {
82
+ setConfirmModal(null);
83
+ }, []);
84
+ const showToast = useCallback((message, type = 'info') => {
85
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
86
+ setToasts((prev) => [...prev, { id, message, type }]);
87
+ }, []);
88
+ const dismissToast = useCallback((id) => {
89
+ setToasts((prev) => prev.filter((t) => t.id !== id));
90
+ }, []);
91
+ // Opaque handle from the StorageAdapter's File System Access-style open/save
92
+ // (unknown on purpose — see extensions/storage-adapter.ts) letting saveFile()
93
+ // write back in place instead of prompting Save As every time.
94
+ const fileHandleRef = useRef(null);
95
+ // isDirtyRef is updated synchronously by setDirty() below, everywhere isDirty
96
+ // changes — not via a useEffect syncing off the `isDirty` state, which would lag
97
+ // a render+commit behind and race anything (like the close-guard) that checks
98
+ // "did that save actually succeed?" right after calling setDirty in the same tick.
99
+ const isDirtyRef = useRef(false);
100
+ const setDirty = useCallback((value) => {
101
+ isDirtyRef.current = value;
102
+ setIsDirty(value);
103
+ }, []);
104
+ const reloadCustomStyles = useCallback(async () => {
105
+ try {
106
+ setCustomStyles(await storageAdapter.loadAllStyles());
107
+ }
108
+ catch {
109
+ // IndexedDB unavailable (private browsing, test environment, etc.) — fall
110
+ // back to built-in styles only rather than crashing the app.
111
+ setCustomStyles([]);
112
+ }
113
+ }, [storageAdapter]);
114
+ useEffect(() => { void reloadCustomStyles(); }, [reloadCustomStyles]);
115
+ // Recover autosaved-but-unsaved content on boot (Ctrl-R, a crash, or the tab closing
116
+ // without an explicit save) instead of always starting from a blank document.
117
+ useEffect(() => {
118
+ const pointer = readRecoveryPointer();
119
+ if (!pointer)
120
+ return;
121
+ let cancelled = false;
122
+ void (async () => {
123
+ try {
124
+ const content = await storageAdapter.loadDocument(pointer.path);
125
+ if (cancelled || !content)
126
+ return;
127
+ setTextState(content);
128
+ setAst(parse(content));
129
+ setFilePath(pointer.path === '__autosave__' ? null : pointer.path);
130
+ setDirty(true);
131
+ // Still marked dirty above regardless — a restored draft isn't confirmed
132
+ // written back until the user actually saves it again.
133
+ if (pointer.path !== '__autosave__') {
134
+ const handle = await storageAdapter.restoreFileHandle?.(pointer.path);
135
+ if (!cancelled && handle)
136
+ fileHandleRef.current = handle;
137
+ }
138
+ showToast('Recovered unsaved changes from your last session.', 'info');
139
+ }
140
+ catch {
141
+ // Best-effort recovery; fall back to the empty template on failure.
142
+ }
143
+ })();
144
+ return () => { cancelled = true; };
145
+ }, [storageAdapter, showToast]);
146
+ const setText = useCallback((next) => {
147
+ setTextState(next);
148
+ setAst(parse(next));
149
+ setDirty(true);
150
+ }, []);
151
+ const frontmatterData = ast.frontmatter?.data;
152
+ const documentStyleId = frontmatterData?.['style'];
153
+ const resolvedStyle = useMemo(() => resolveStyle(documentStyleId, customStyles), [documentStyleId, customStyles]);
154
+ const watermarkRaw = frontmatterData?.['watermark'];
155
+ const watermarkText = typeof watermarkRaw === 'string' && watermarkRaw.trim() ? watermarkRaw.trim() : null;
156
+ const setDocumentStyle = useCallback((id) => {
157
+ setFlatFrontmatterField(getEditorView(), text, setText, 'style', id);
158
+ }, [text, setText]);
159
+ const markClean = useCallback(() => setDirty(false), []);
160
+ // Refs so the autosave path always writes the latest text/filePath without
161
+ // needing them in a dependency array that would tear down the timers below
162
+ // on every keystroke.
163
+ const textRef = useRef(text);
164
+ useEffect(() => { textRef.current = text; }, [text]);
165
+ const filePathRef = useRef(filePath);
166
+ useEffect(() => { filePathRef.current = filePath; }, [filePath]);
167
+ const autosaveFailedRef = useRef(false);
168
+ const save = useCallback(async () => {
169
+ const path = filePathRef.current ?? '__autosave__';
170
+ try {
171
+ await storageAdapter.saveDocument(path, textRef.current);
172
+ writeRecoveryPointer(path);
173
+ setLastSaveTarget('local');
174
+ autosaveFailedRef.current = false;
175
+ // Deliberately NOT setDirty(false) here — this is a local backup, not a
176
+ // save to the real file, so the document must keep showing as unsaved
177
+ // (VS Code-style) until saveFile()/saveFileAs() succeeds. Still reset the
178
+ // ceiling/ref below so a marathon typing session keeps getting backed up
179
+ // roughly every AUTOSAVE_MAX_WAIT_MS instead of only once.
180
+ if (maxWaitTimerRef.current) {
181
+ clearTimeout(maxWaitTimerRef.current);
182
+ maxWaitTimerRef.current = null;
183
+ }
184
+ dirtySinceRef.current = null;
185
+ }
186
+ catch {
187
+ if (!autosaveFailedRef.current) {
188
+ autosaveFailedRef.current = true;
189
+ showToast('Autosave failed — your browser storage may be full or unavailable.', 'warn');
190
+ }
191
+ }
192
+ }, [storageAdapter, showToast]);
193
+ const newDocument = useCallback((initialContent) => {
194
+ const doNew = () => {
195
+ if (pauseTimerRef.current) {
196
+ clearTimeout(pauseTimerRef.current);
197
+ pauseTimerRef.current = null;
198
+ }
199
+ if (maxWaitTimerRef.current) {
200
+ clearTimeout(maxWaitTimerRef.current);
201
+ maxWaitTimerRef.current = null;
202
+ }
203
+ dirtySinceRef.current = null;
204
+ fileHandleRef.current = null;
205
+ clearRecoveryPointer();
206
+ const content = initialContent ?? EMPTY_DOC;
207
+ setTextState(content);
208
+ setAst(parse(content));
209
+ setFilePath(null);
210
+ setDirty(false);
211
+ setLastSaveTarget(null);
212
+ setVersions([]);
213
+ };
214
+ if (!isDirty) {
215
+ doNew();
216
+ return;
217
+ }
218
+ showConfirm({
219
+ title: 'Discard Unsaved Changes?',
220
+ message: 'Starting a new screenplay will discard all unsaved edits to your current document.',
221
+ confirmLabel: 'Discard & Create New',
222
+ destructive: true,
223
+ onConfirm: doNew,
224
+ });
225
+ }, [isDirty, showConfirm]);
226
+ const openFile = useCallback(async () => {
227
+ const doOpen = async () => {
228
+ const result = await storageAdapter.openFile();
229
+ if (!result)
230
+ return;
231
+ if (pauseTimerRef.current) {
232
+ clearTimeout(pauseTimerRef.current);
233
+ pauseTimerRef.current = null;
234
+ }
235
+ if (maxWaitTimerRef.current) {
236
+ clearTimeout(maxWaitTimerRef.current);
237
+ maxWaitTimerRef.current = null;
238
+ }
239
+ dirtySinceRef.current = null;
240
+ clearRecoveryPointer();
241
+ let content = result.content;
242
+ const isFountain = result.name.endsWith('.fountain');
243
+ if (isFountain) {
244
+ const { document } = importFountain(content);
245
+ content = fountainDocToSutra(document);
246
+ }
247
+ const parsed = parse(content);
248
+ if (!parsed.frontmatter) {
249
+ const baseName = result.name.replace(/\.[^.]+$/, '');
250
+ content = `---\ntitle: ${baseName}\n---\n\n${content}`;
251
+ }
252
+ // Fountain files get no handle (we save as .sutra); .sutra files keep their handle
253
+ fileHandleRef.current = isFountain ? null : (result.handle ?? null);
254
+ const newPath = result.name.replace(/\.fountain$/i, '.sutra');
255
+ setTextState(content);
256
+ setAst(parse(content));
257
+ setFilePath(newPath);
258
+ // Opening a file is a clean state; fountain conversion counts as dirty since format changed
259
+ setDirty(isFountain);
260
+ setLastSaveTarget(isFountain ? null : 'file');
261
+ if (!isFountain && fileHandleRef.current) {
262
+ void storageAdapter.persistFileHandle?.(newPath, fileHandleRef.current);
263
+ }
264
+ };
265
+ if (!isDirty) {
266
+ await doOpen();
267
+ return;
268
+ }
269
+ await new Promise((resolve) => {
270
+ showConfirm({
271
+ title: 'Discard Unsaved Changes?',
272
+ message: 'Opening a different screenplay will discard any unsaved edits to your current document.',
273
+ confirmLabel: 'Discard & Open',
274
+ destructive: true,
275
+ onConfirm: () => { void doOpen().then(resolve); },
276
+ onCancel: () => resolve(),
277
+ });
278
+ });
279
+ }, [storageAdapter, isDirty, showConfirm]);
280
+ // Power-user feature: versions are captured only on explicit saveFile/saveFileAs
281
+ // (never on autosave — see save() above). Called both by the filePath-change
282
+ // effect below and directly after a save that reuses the same filePath (which
283
+ // wouldn't otherwise re-trigger that effect).
284
+ const refreshVersions = useCallback(async (path) => {
285
+ try {
286
+ const loaded = await storageAdapter.listVersions(path);
287
+ setVersions(loaded);
288
+ }
289
+ catch {
290
+ // silently fail if storage unavailable
291
+ }
292
+ }, [storageAdapter]);
293
+ const saveFile = useCallback(async () => {
294
+ const name = filePath ?? 'untitled.sutra';
295
+ const result = await storageAdapter.saveFile(name, text, fileHandleRef.current);
296
+ if (result) {
297
+ fileHandleRef.current = result.handle;
298
+ setFilePath(result.savedName);
299
+ setDirty(false);
300
+ setLastSaveTarget('file');
301
+ clearRecoveryPointer();
302
+ if (result.handle)
303
+ void storageAdapter.persistFileHandle?.(result.savedName, result.handle);
304
+ void refreshVersions(result.savedName);
305
+ showToast(`Saved ${result.savedName}`, 'success');
306
+ }
307
+ }, [filePath, text, showToast, storageAdapter, refreshVersions]);
308
+ const saveFileAs = useCallback(async () => {
309
+ const name = filePath ?? 'untitled.sutra';
310
+ const result = await storageAdapter.saveFile(name, text, null);
311
+ if (result) {
312
+ fileHandleRef.current = result.handle;
313
+ setFilePath(result.savedName);
314
+ setDirty(false);
315
+ setLastSaveTarget('file');
316
+ clearRecoveryPointer();
317
+ if (result.handle)
318
+ void storageAdapter.persistFileHandle?.(result.savedName, result.handle);
319
+ void refreshVersions(result.savedName);
320
+ showToast(`Saved as ${result.savedName}`, 'success');
321
+ }
322
+ }, [filePath, text, showToast, storageAdapter, refreshVersions]);
323
+ // Reimports a .docx previously exported via exportToDocx (optionally corrected/proofread
324
+ // in Word — the exporter's CineXxx paragraph styles are what makes the round trip possible;
325
+ // font-only changes don't affect them). Returns null if the user cancelled the file picker
326
+ // or declined the replace-content confirmation; otherwise the import warnings (possibly empty).
327
+ const importDocx = useCallback(async () => {
328
+ const picked = await storageAdapter.openDocx();
329
+ if (!picked)
330
+ return null;
331
+ const { bodyText, warnings } = await importDocxToSutra(picked.buffer);
332
+ if (bodyText === null)
333
+ return warnings;
334
+ return new Promise((resolve) => {
335
+ showConfirm({
336
+ title: 'Import Word Document',
337
+ message: 'Replace the current screenplay content with the imported Word document? This cannot be undone.',
338
+ confirmLabel: 'Replace Screenplay',
339
+ destructive: true,
340
+ onConfirm: () => {
341
+ const frontmatterRaw = ast.frontmatter?.raw;
342
+ const newText = frontmatterRaw ? `${frontmatterRaw}\n${bodyText}\n` : `${bodyText}\n`;
343
+ setText(newText);
344
+ showToast('Word document imported successfully', 'success');
345
+ resolve(warnings);
346
+ },
347
+ onCancel: () => resolve(null),
348
+ });
349
+ });
350
+ }, [ast, setText, showConfirm, showToast]);
351
+ // Power-user feature: versions are captured only on explicit saveFile/saveFileAs
352
+ // (never on autosave — see save() above), so this needs an explicit refresh
353
+ // after each of those succeed, not just when filePath changes.
354
+ useEffect(() => {
355
+ if (!filePath) {
356
+ setVersions([]);
357
+ return;
358
+ }
359
+ void refreshVersions(filePath);
360
+ }, [filePath, refreshVersions]);
361
+ const restoreVersion = useCallback((entry) => {
362
+ setText(entry.content); // setText already marks the document dirty
363
+ setVersionHistoryVisible(false);
364
+ showToast('Version restored — remember to save.', 'info');
365
+ }, [setText, showToast]);
366
+ const clearVersionHistory = useCallback(async () => {
367
+ if (!filePath)
368
+ return;
369
+ return new Promise((resolve) => {
370
+ showConfirm({
371
+ title: 'Delete Version History?',
372
+ message: `Delete all ${versions.length} saved versions of ${filePath}? This cannot be undone.`,
373
+ confirmLabel: 'Delete Version History',
374
+ destructive: true,
375
+ onConfirm: async () => {
376
+ try {
377
+ await storageAdapter.deleteAllVersions?.(filePath);
378
+ setVersions([]);
379
+ showToast('Version history cleared', 'success');
380
+ }
381
+ catch {
382
+ showToast('Failed to clear version history', 'error');
383
+ }
384
+ resolve();
385
+ },
386
+ onCancel: () => resolve(),
387
+ });
388
+ });
389
+ }, [filePath, versions.length, showConfirm, showToast, storageAdapter]);
390
+ // Autosave: a short pause after the last keystroke triggers a save, and a hard
391
+ // ceiling (AUTOSAVE_MAX_WAIT_MS) forces one regardless, so a long uninterrupted
392
+ // typing session is never more than ~30s away from a recoverable backup.
393
+ const pauseTimerRef = useRef(null);
394
+ const maxWaitTimerRef = useRef(null);
395
+ const dirtySinceRef = useRef(null);
396
+ useEffect(() => {
397
+ if (!isDirty) {
398
+ if (pauseTimerRef.current) {
399
+ clearTimeout(pauseTimerRef.current);
400
+ pauseTimerRef.current = null;
401
+ }
402
+ if (maxWaitTimerRef.current) {
403
+ clearTimeout(maxWaitTimerRef.current);
404
+ maxWaitTimerRef.current = null;
405
+ }
406
+ dirtySinceRef.current = null;
407
+ return;
408
+ }
409
+ if (pauseTimerRef.current)
410
+ clearTimeout(pauseTimerRef.current);
411
+ pauseTimerRef.current = setTimeout(() => { void save(); }, AUTOSAVE_PAUSE_MS);
412
+ // Only armed once per dirty streak — NOT reset on every keystroke — so it
413
+ // actually acts as a ceiling instead of an ever-postponed debounce.
414
+ if (dirtySinceRef.current === null) {
415
+ dirtySinceRef.current = Date.now();
416
+ maxWaitTimerRef.current = setTimeout(() => { void save(); }, AUTOSAVE_MAX_WAIT_MS);
417
+ }
418
+ return () => {
419
+ if (pauseTimerRef.current) {
420
+ clearTimeout(pauseTimerRef.current);
421
+ pauseTimerRef.current = null;
422
+ }
423
+ };
424
+ }, [text, isDirty, save]);
425
+ useEffect(() => {
426
+ return () => {
427
+ if (pauseTimerRef.current)
428
+ clearTimeout(pauseTimerRef.current);
429
+ if (maxWaitTimerRef.current)
430
+ clearTimeout(maxWaitTimerRef.current);
431
+ };
432
+ }, []);
433
+ // beforeunload guard — fires on page reload, window close, Electron reload
434
+ useEffect(() => {
435
+ const onBeforeUnload = (e) => {
436
+ if (!isDirty)
437
+ return;
438
+ e.preventDefault();
439
+ e.returnValue = 'You have unsaved changes.';
440
+ };
441
+ window.addEventListener('beforeunload', onBeforeUnload);
442
+ return () => window.removeEventListener('beforeunload', onBeforeUnload);
443
+ }, [isDirty]);
444
+ useEffect(() => {
445
+ const onKeyDown = (e) => {
446
+ const mod = e.ctrlKey || e.metaKey;
447
+ const cs = mod && e.shiftKey;
448
+ // File operations — use e.code for layout-independence
449
+ if (mod && !e.shiftKey && !e.altKey && e.code === 'KeyS') {
450
+ e.preventDefault();
451
+ void saveFile();
452
+ return;
453
+ }
454
+ if (mod && !e.shiftKey && !e.altKey && e.code === 'KeyO') {
455
+ e.preventDefault();
456
+ void openFile();
457
+ return;
458
+ }
459
+ if (mod && !e.shiftKey && !e.altKey && e.code === 'KeyF') {
460
+ e.preventDefault();
461
+ setFindMode('find');
462
+ setFindReplaceVisible(v => !v);
463
+ return;
464
+ }
465
+ if (mod && !e.shiftKey && !e.altKey && e.code === 'KeyH') {
466
+ e.preventDefault();
467
+ setFindMode('replace');
468
+ setFindReplaceVisible(true);
469
+ return;
470
+ }
471
+ // Block type shortcuts Alt+Shift+letter
472
+ if (e.altKey && e.shiftKey && !mod) {
473
+ if (e.code === 'KeyN') {
474
+ e.preventDefault();
475
+ runCommand(makeSetOrToggleNote());
476
+ return;
477
+ }
478
+ const blockMap = {
479
+ 'KeyH': ['scene_heading', { id: null }],
480
+ 'KeyA': ['action'],
481
+ 'KeyC': ['character', { extension: null, isDual: false }],
482
+ 'KeyD': ['dialogue'],
483
+ 'KeyP': ['parenthetical'],
484
+ 'KeyX': ['transition'],
485
+ };
486
+ if (e.code in blockMap) {
487
+ e.preventDefault();
488
+ const [type, attrs] = blockMap[e.code];
489
+ runCommand(makeSetBlock(type, attrs ?? {}));
490
+ return;
491
+ }
492
+ }
493
+ // UI toggles Ctrl+Shift+letter
494
+ if (cs && !e.altKey) {
495
+ if (e.code === 'KeyE') {
496
+ e.preventDefault();
497
+ setMode(m => m === 'formatted' ? 'source' : 'formatted');
498
+ return;
499
+ }
500
+ if (e.code === 'KeyH') {
501
+ e.preventDefault();
502
+ setRibbonVisible(v => !v);
503
+ return;
504
+ } // was KeyR
505
+ if (e.code === 'KeyB') {
506
+ e.preventDefault();
507
+ setNavVisible(v => !v);
508
+ return;
509
+ }
510
+ }
511
+ };
512
+ window.addEventListener('keydown', onKeyDown);
513
+ return () => window.removeEventListener('keydown', onKeyDown);
514
+ }, [saveFile, openFile]);
515
+ // Native menu IPC (Desktop wrapper only) — receives commands from the main process menu
516
+ useEffect(() => {
517
+ const api = window.desktopAPI;
518
+ if (!api?.onMenuCommand)
519
+ return;
520
+ const off = api.onMenuCommand((cmd) => {
521
+ switch (cmd) {
522
+ case 'file:new':
523
+ newDocument(EMPTY_DOC);
524
+ break;
525
+ case 'file:open':
526
+ void openFile();
527
+ break;
528
+ case 'file:save':
529
+ void saveFile();
530
+ break;
531
+ case 'file:saveAs':
532
+ void saveFileAs();
533
+ break;
534
+ case 'edit:find':
535
+ setFindMode('find');
536
+ setFindReplaceVisible(true);
537
+ break;
538
+ case 'edit:replace':
539
+ setFindMode('replace');
540
+ setFindReplaceVisible(true);
541
+ break;
542
+ case 'edit:toggleSource':
543
+ setMode(m => m === 'formatted' ? 'source' : 'formatted');
544
+ break;
545
+ case 'edit:toggleRibbon':
546
+ setRibbonVisible(v => !v);
547
+ break;
548
+ case 'edit:toggleNav':
549
+ setNavVisible(v => !v);
550
+ break;
551
+ case 'edit:toggleMetadata':
552
+ setMetadataVisible(!metadataVisible);
553
+ break;
554
+ }
555
+ });
556
+ return off;
557
+ }, [isDirty, newDocument, openFile, saveFile, saveFileAs, setText, setMode, setRibbonVisible, setNavVisible, metadataVisible, showConfirm]);
558
+ // Native window-close guard (Desktop wrapper only). beforeunload isn't reliable for
559
+ // an OS-level close across Tauri's backing webviews, so the Rust side intercepts it
560
+ // and asks here instead; nothing to close until we say so via closeWindow().
561
+ useEffect(() => {
562
+ const api = window.desktopAPI;
563
+ if (!api?.onCloseRequested)
564
+ return;
565
+ const off = api.onCloseRequested(() => {
566
+ if (!isDirtyRef.current) {
567
+ void api.closeWindow?.();
568
+ return;
569
+ }
570
+ showConfirm({
571
+ title: 'Unsaved Changes',
572
+ message: 'You have unsaved changes. Save them before closing?',
573
+ confirmLabel: 'Save & Close',
574
+ cancelLabel: 'Cancel',
575
+ onConfirm: () => {
576
+ void saveFile().then(() => {
577
+ // saveFile only clears isDirty on success — if the user cancelled a
578
+ // Save As picker or it failed, stay open instead of closing anyway.
579
+ if (!isDirtyRef.current)
580
+ void api.closeWindow?.();
581
+ });
582
+ },
583
+ // onCancel omitted — dismissing just leaves the window open.
584
+ });
585
+ });
586
+ return off;
587
+ }, [saveFile, showConfirm]);
588
+ return (_jsx(StorageAdapterProvider, { value: storageAdapter, children: _jsx(DocumentContext.Provider, { value: {
589
+ text, ast, mode, filePath, isDirty, lastSaveTarget, ribbonVisible, navVisible, exportVisible, settingsVisible,
590
+ aiOnboardingVisible, voiceActive, metadataVisible,
591
+ findReplaceVisible, findMode, styleVisible, customStyles, resolvedStyle, watermarkText,
592
+ confirmModal, toasts, versions, versionHistoryVisible,
593
+ setText, setMode, setFilePath, setRibbonVisible, setNavVisible, setExportVisible, setSettingsVisible,
594
+ setAIOnboardingVisible, setVoiceActive, setMetadataVisible,
595
+ setFindReplaceVisible, setFindMode, setStyleVisible, setVersionHistoryVisible, setDocumentStyle, reloadCustomStyles,
596
+ markClean, save, newDocument, openFile, saveFile, saveFileAs, importDocx, restoreVersion, clearVersionHistory,
597
+ showConfirm, closeConfirm, showToast, dismissToast,
598
+ }, children: children }) }));
599
+ }
600
+ export function useDocument() {
601
+ const ctx = useContext(DocumentContext);
602
+ if (!ctx)
603
+ throw new Error('useDocument must be used inside DocumentProvider');
604
+ return ctx;
605
+ }
@@ -0,0 +1,40 @@
1
+ // Types for window.desktopAPI exposed by desktop container shells — keep in sync.
2
+ interface DesktopAPI {
3
+ readFile: (path: string) => Promise<string>
4
+ writeFile: (path: string, content: string) => Promise<void>
5
+ showOpenDialog: () => Promise<string | null>
6
+ showSaveDialog: (defaultName: string) => Promise<string | null>
7
+ // Keyring & AI API bridge for Tauri desktop
8
+ getApiKey?: (provider: string) => Promise<string | null>
9
+ setApiKey?: (provider: string, key: string) => Promise<void>
10
+ deleteApiKey?: (provider: string) => Promise<void>
11
+ callProviderApi?: (
12
+ url: string,
13
+ method: string,
14
+ headers: Record<string, string>,
15
+ body: any
16
+ ) => Promise<{ status: number; data: any }>
17
+ onMenuCommand: (callback: (command: string) => void) => () => void
18
+ // Native PDF export (Windows only — WebView2 PrintToPdf). Absent on other
19
+ // platforms; callers must feature-detect before use.
20
+ showSavePdfDialog?: (defaultName: string) => Promise<string | null>
21
+ exportScreenplayToPdf?: (html: string, path: string) => Promise<void>
22
+ // Disk-backed autosave (format spec §3.2: IndexedDB on web, disk on
23
+ // desktop). local-storage-adapter.ts prefers these over IndexedDB when present.
24
+ autosaveWrite?: (key: string, content: string) => Promise<void>
25
+ autosaveRead?: (key: string) => Promise<string | null>
26
+ // Native window-close guard: beforeunload isn't reliably fired by a native
27
+ // close (vs. an in-page reload) across WRY's backing webviews, so the Rust
28
+ // side intercepts CloseRequested and asks the frontend via this event
29
+ // instead of closing immediately.
30
+ onCloseRequested?: (callback: () => void) => () => void
31
+ closeWindow?: () => Promise<void>
32
+ }
33
+
34
+ declare global {
35
+ interface Window {
36
+ desktopAPI?: DesktopAPI
37
+ }
38
+ }
39
+
40
+ export {}
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare function EditorView(): React.JSX.Element;