@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,612 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useEffect, useState, useCallback } from 'react';
3
+ import { TextSelection } from 'prosemirror-state';
4
+ import { useDocument } from '../context/DocumentContext';
5
+ import { useTranslation } from '../i18n/useTranslation';
6
+ import { buildSceneList } from './scene-list';
7
+ import { getEditorView, getSourceView } from '../editor/editor-bus';
8
+ import { schema } from '../editor/schema';
9
+ import { EditorView as CmView } from '@codemirror/view';
10
+ import { StatisticsDialog } from './StatisticsDialog';
11
+ import { EyeIcon, EyeOffIcon, LockIcon, StatsIcon, SparklesIcon, CloseIcon } from '../shell/icons';
12
+ import { getAIConfig, callAIStructured, hasConfiguredApiKey } from '../ai/ai-client';
13
+ import { SCENE_METADATA_PROMPT, SCENE_METADATA_SCHEMA } from '../ai/prompts';
14
+ import { prosemirrorToSutra } from '../editor/prosemirror-to-sutra';
15
+ /** Update the scene synopsis in the raw Sutra text. */
16
+ function updateSynopsisInText(text, sceneOffset, newSynopsis) {
17
+ const lines = text.split('\n');
18
+ let currentOffset = 0;
19
+ let targetLineIdx = -1;
20
+ for (let i = 0; i < lines.length; i++) {
21
+ if (currentOffset === sceneOffset) {
22
+ targetLineIdx = i;
23
+ break;
24
+ }
25
+ currentOffset += (lines[i]?.length ?? 0) + 1;
26
+ }
27
+ if (targetLineIdx === -1)
28
+ return text;
29
+ let synopsisLineIdx = -1;
30
+ let insertIdx = targetLineIdx + 1;
31
+ for (let j = targetLineIdx + 1; j < lines.length; j++) {
32
+ const line = (lines[j] ?? '').trim();
33
+ if (line.startsWith('##') || line.startsWith('#'))
34
+ break;
35
+ if (line.startsWith('& synopsis:')) {
36
+ synopsisLineIdx = j;
37
+ break;
38
+ }
39
+ if (line.startsWith('&')) {
40
+ insertIdx = j + 1;
41
+ }
42
+ else if (line !== '') {
43
+ break;
44
+ }
45
+ }
46
+ const newLines = [...lines];
47
+ if (synopsisLineIdx !== -1) {
48
+ if (newSynopsis.trim() === '') {
49
+ newLines.splice(synopsisLineIdx, 1);
50
+ }
51
+ else {
52
+ newLines[synopsisLineIdx] = `& synopsis: ${newSynopsis}`;
53
+ }
54
+ }
55
+ else if (newSynopsis.trim() !== '') {
56
+ newLines.splice(insertIdx, 0, `& synopsis: ${newSynopsis}`);
57
+ }
58
+ return newLines.join('\n');
59
+ }
60
+ /** Update a scene metadata key/value in the raw Sutra text. */
61
+ function updateSceneMetadataInText(text, sceneOffset, key, val) {
62
+ const lines = text.split('\n');
63
+ let currentOffset = 0;
64
+ let targetLineIdx = -1;
65
+ for (let i = 0; i < lines.length; i++) {
66
+ if (currentOffset === sceneOffset) {
67
+ targetLineIdx = i;
68
+ break;
69
+ }
70
+ currentOffset += (lines[i]?.length ?? 0) + 1;
71
+ }
72
+ if (targetLineIdx === -1)
73
+ return text;
74
+ let metadataLineIdx = -1;
75
+ let insertIdx = targetLineIdx + 1;
76
+ for (let j = targetLineIdx + 1; j < lines.length; j++) {
77
+ const line = (lines[j] ?? '').trim();
78
+ if (line.startsWith('##') || line.startsWith('#'))
79
+ break;
80
+ if (line.startsWith(`& ${key}:`)) {
81
+ metadataLineIdx = j;
82
+ break;
83
+ }
84
+ if (line.startsWith('&')) {
85
+ insertIdx = j + 1;
86
+ }
87
+ else if (line !== '') {
88
+ break;
89
+ }
90
+ }
91
+ const newLines = [...lines];
92
+ if (metadataLineIdx !== -1) {
93
+ if (val.trim() === '') {
94
+ newLines.splice(metadataLineIdx, 1);
95
+ }
96
+ else {
97
+ newLines[metadataLineIdx] = `& ${key}: ${val}`;
98
+ }
99
+ }
100
+ else if (val.trim() !== '') {
101
+ newLines.splice(insertIdx, 0, `& ${key}: ${val}`);
102
+ }
103
+ return newLines.join('\n');
104
+ }
105
+ /** Update a scene metadata key/value in the ProseMirror editor document. */
106
+ function updateSceneMetadataInProseMirror(sceneIndex, key, val) {
107
+ const view = getEditorView();
108
+ if (!view)
109
+ return;
110
+ let headingPmPos = null;
111
+ let headingCount = 0;
112
+ view.state.doc.forEach((node, offset) => {
113
+ if (headingPmPos !== null)
114
+ return;
115
+ if (node.type.name === 'scene_heading') {
116
+ headingCount++;
117
+ if (headingCount === sceneIndex)
118
+ headingPmPos = offset;
119
+ }
120
+ });
121
+ if (headingPmPos !== null) {
122
+ let tr = view.state.tr;
123
+ let existingPos = null;
124
+ let index = view.state.doc.resolve(headingPmPos).index(0) + 1;
125
+ let pos = headingPmPos + view.state.doc.nodeAt(headingPmPos).nodeSize;
126
+ while (index < view.state.doc.childCount) {
127
+ const sibling = view.state.doc.child(index);
128
+ if (sibling.type.name === 'scene_metadata') {
129
+ if (sibling.attrs['metaKey'] === key) {
130
+ existingPos = pos;
131
+ break;
132
+ }
133
+ }
134
+ else {
135
+ break;
136
+ }
137
+ pos += sibling.nodeSize;
138
+ index++;
139
+ }
140
+ if (existingPos !== null) {
141
+ const node = view.state.doc.nodeAt(existingPos);
142
+ if (val === '') {
143
+ tr = tr.delete(existingPos, existingPos + node.nodeSize);
144
+ }
145
+ else {
146
+ tr = tr.replaceWith(existingPos + 1, existingPos + node.nodeSize - 1, schema.text(val));
147
+ }
148
+ }
149
+ else if (val !== '') {
150
+ let insertPos = headingPmPos + view.state.doc.nodeAt(headingPmPos).nodeSize;
151
+ let siblingIdx = view.state.doc.resolve(headingPmPos).index(0) + 1;
152
+ while (siblingIdx < view.state.doc.childCount) {
153
+ const sibling = view.state.doc.child(siblingIdx);
154
+ if (sibling.type.name === 'scene_metadata') {
155
+ insertPos += sibling.nodeSize;
156
+ siblingIdx++;
157
+ }
158
+ else {
159
+ break;
160
+ }
161
+ }
162
+ const metaNode = schema.nodes['scene_metadata'].create({ metaKey: key }, schema.text(val));
163
+ tr = tr.insert(insertPos, metaNode);
164
+ }
165
+ view.dispatch(tr);
166
+ }
167
+ }
168
+ /** Lock scene numbers in raw Sutra text. */
169
+ function lockNumbersInText(text) {
170
+ const lines = text.split('\n');
171
+ const result = [];
172
+ let sceneCount = 0;
173
+ for (let i = 0; i < lines.length; i++) {
174
+ const line = lines[i] ?? '';
175
+ const trimmed = line.trim();
176
+ result.push(line);
177
+ if (trimmed.startsWith('## ') || trimmed.startsWith('##\t')) {
178
+ sceneCount++;
179
+ let hasNumber = false;
180
+ let j = i + 1;
181
+ while (j < lines.length) {
182
+ const nextLine = (lines[j] ?? '').trim();
183
+ if (nextLine.startsWith('##') || nextLine.startsWith('#'))
184
+ break;
185
+ if (nextLine.startsWith('& number:')) {
186
+ hasNumber = true;
187
+ break;
188
+ }
189
+ j++;
190
+ }
191
+ if (!hasNumber) {
192
+ result.push(`& number: ${sceneCount}`);
193
+ }
194
+ }
195
+ }
196
+ return result.join('\n');
197
+ }
198
+ export function SceneNavigator() {
199
+ const { text, setText, setAIOnboardingVisible, showToast, setNavVisible } = useDocument();
200
+ const { t } = useTranslation();
201
+ const [scenes, setScenes] = useState([]);
202
+ const [dragIndex, setDragIndex] = useState(null);
203
+ const [dropIndex, setDropIndex] = useState(null);
204
+ // New States
205
+ const [showNavSynopsis, setShowNavSynopsis] = useState(true);
206
+ const [statsOpen, setStatsOpen] = useState(false);
207
+ const [editingSceneIdx, setEditingSceneIdx] = useState(null);
208
+ const [editVal, setEditVal] = useState('');
209
+ // AI Generation States
210
+ const [isProcessing, setIsProcessing] = useState(false);
211
+ const [progressText, setProgressText] = useState('');
212
+ const textRef = React.useRef(text);
213
+ textRef.current = text;
214
+ // Rebuild scene list whenever text changes
215
+ useEffect(() => {
216
+ setScenes(buildSceneList(text));
217
+ }, [text]);
218
+ const handleSceneMetadata = useCallback(async (scene) => {
219
+ setIsProcessing(true);
220
+ setProgressText('Generating synopsis & estimating duration...');
221
+ try {
222
+ const aiConfig = getAIConfig();
223
+ if (!(await hasConfiguredApiKey(aiConfig))) {
224
+ showToast('AI keys are required to generate synopsis & duration. Opening Setup...', 'info');
225
+ setAIOnboardingVisible(true);
226
+ return;
227
+ }
228
+ const idx = scenes.findIndex(s => s.index === scene.index);
229
+ if (idx === -1)
230
+ return;
231
+ const start = scene.textOffset;
232
+ const end = idx + 1 < scenes.length ? scenes[idx + 1].textOffset : text.length;
233
+ const sceneText = text.substring(start, end);
234
+ const { synopsis, duration } = await callAIStructured(sceneText, SCENE_METADATA_PROMPT, aiConfig, SCENE_METADATA_SCHEMA);
235
+ const view = getEditorView();
236
+ if (view) {
237
+ updateSceneMetadataInProseMirror(scene.index, 'synopsis', synopsis.trim());
238
+ updateSceneMetadataInProseMirror(scene.index, 'est-duration', duration.trim());
239
+ }
240
+ else {
241
+ let nextText = updateSceneMetadataInText(text, scene.textOffset, 'synopsis', synopsis.trim());
242
+ nextText = updateSceneMetadataInText(nextText, scene.textOffset, 'est-duration', duration.trim());
243
+ setText(nextText);
244
+ }
245
+ showToast(`Generated synopsis & duration for scene ${scene.id || scene.index}.`, 'success');
246
+ }
247
+ catch (e) {
248
+ showToast(`Scene metadata generation failed: ${e.message || String(e)}`, 'error');
249
+ }
250
+ finally {
251
+ setIsProcessing(false);
252
+ setProgressText('');
253
+ }
254
+ }, [scenes, text, setText, setAIOnboardingVisible, showToast]);
255
+ const handleGenerateAllSceneMetadata = useCallback(async () => {
256
+ setIsProcessing(true);
257
+ setProgressText('Starting batch synopsis & duration generation...');
258
+ try {
259
+ const aiConfig = getAIConfig();
260
+ if (!(await hasConfiguredApiKey(aiConfig))) {
261
+ showToast('AI keys are required to generate synopsis & duration. Opening Setup...', 'info');
262
+ setAIOnboardingVisible(true);
263
+ return;
264
+ }
265
+ const pmView = getEditorView();
266
+ const cmView = getSourceView();
267
+ let currentText = pmView ? prosemirrorToSutra(pmView.state.doc) : (cmView ? cmView.state.doc.toString() : textRef.current);
268
+ let currentScenes = buildSceneList(currentText);
269
+ const missing = currentScenes.filter(s => !s.synopsis || !s.estDuration);
270
+ if (missing.length === 0) {
271
+ showToast('All scenes already have synopsis and duration.', 'info');
272
+ return;
273
+ }
274
+ let current = 0;
275
+ for (const scene of missing) {
276
+ current++;
277
+ setProgressText(`Generating synopsis & duration (${current}/${missing.length})...`);
278
+ currentText = pmView ? prosemirrorToSutra(pmView.state.doc) : (cmView ? cmView.state.doc.toString() : textRef.current);
279
+ currentScenes = buildSceneList(currentText);
280
+ const latestScene = currentScenes.find(s => s.index === scene.index);
281
+ if (!latestScene)
282
+ continue;
283
+ const start = latestScene.textOffset;
284
+ const idx = currentScenes.findIndex(s => s.index === scene.index);
285
+ const end = idx + 1 < currentScenes.length ? currentScenes[idx + 1].textOffset : currentText.length;
286
+ const sceneText = currentText.substring(start, end);
287
+ try {
288
+ const { synopsis, duration } = await callAIStructured(sceneText, SCENE_METADATA_PROMPT, aiConfig, SCENE_METADATA_SCHEMA);
289
+ if (pmView) {
290
+ if (!latestScene.synopsis)
291
+ updateSceneMetadataInProseMirror(scene.index, 'synopsis', synopsis.trim());
292
+ if (!latestScene.estDuration)
293
+ updateSceneMetadataInProseMirror(scene.index, 'est-duration', duration.trim());
294
+ }
295
+ else {
296
+ let nextText = currentText;
297
+ if (!latestScene.synopsis)
298
+ nextText = updateSceneMetadataInText(nextText, latestScene.textOffset, 'synopsis', synopsis.trim());
299
+ if (!latestScene.estDuration)
300
+ nextText = updateSceneMetadataInText(nextText, latestScene.textOffset, 'est-duration', duration.trim());
301
+ if (cmView) {
302
+ cmView.dispatch({
303
+ changes: { from: 0, to: cmView.state.doc.length, insert: nextText }
304
+ });
305
+ }
306
+ else {
307
+ setText(nextText);
308
+ }
309
+ }
310
+ }
311
+ catch (err) {
312
+ console.warn(`Failed scene metadata for scene ${scene.index}:`, err);
313
+ }
314
+ }
315
+ showToast(`Generated synopsis & duration for ${missing.length} scenes.`, 'success');
316
+ }
317
+ catch (e) {
318
+ showToast(`Batch generation failed: ${e.message || String(e)}`, 'error');
319
+ }
320
+ finally {
321
+ setIsProcessing(false);
322
+ setProgressText('');
323
+ }
324
+ }, [setText, setAIOnboardingVisible, showToast]);
325
+ const handleFrontmatter = useCallback(() => {
326
+ const view = getEditorView();
327
+ if (!view) {
328
+ const cmView = getSourceView();
329
+ if (cmView) {
330
+ cmView.dispatch({
331
+ selection: { anchor: 0 },
332
+ effects: CmView.scrollIntoView(0, { y: 'start' }),
333
+ });
334
+ cmView.focus();
335
+ }
336
+ return;
337
+ }
338
+ let tr = view.state.tr;
339
+ const first = view.state.doc.firstChild;
340
+ if (!first || first.type.name !== 'title_page') {
341
+ const field = schema.nodes['frontmatter_field'].create({ fmKey: 'title' });
342
+ tr = tr.insert(0, schema.nodes['title_page'].create({}, [field]));
343
+ }
344
+ tr = tr.setSelection(TextSelection.near(tr.doc.resolve(2)));
345
+ view.dispatch(tr);
346
+ view.focus();
347
+ view.dom.closest('.cs-main')?.scrollTo({ top: 0, behavior: 'smooth' });
348
+ if (typeof window !== 'undefined' && window.innerWidth <= 640) {
349
+ setNavVisible(false);
350
+ }
351
+ }, [setNavVisible]);
352
+ const handleJump = useCallback((scene) => {
353
+ const view = getEditorView();
354
+ if (!view) {
355
+ const cmView = getSourceView();
356
+ if (cmView) {
357
+ cmView.dispatch({
358
+ selection: { anchor: scene.textOffset },
359
+ effects: CmView.scrollIntoView(scene.textOffset, { y: 'start' }),
360
+ });
361
+ cmView.focus();
362
+ if (typeof window !== 'undefined' && window.innerWidth <= 640) {
363
+ setNavVisible(false);
364
+ }
365
+ }
366
+ return;
367
+ }
368
+ try {
369
+ let headingPmPos = null;
370
+ let headingCount = 0;
371
+ view.state.doc.forEach((node, offset) => {
372
+ if (headingPmPos !== null)
373
+ return;
374
+ if (node.type.name === 'scene_heading') {
375
+ headingCount++;
376
+ if (headingCount === scene.index)
377
+ headingPmPos = offset;
378
+ }
379
+ });
380
+ if (headingPmPos === null)
381
+ return;
382
+ const resolved = view.state.doc.resolve(headingPmPos + 1);
383
+ const sel = TextSelection.near(resolved);
384
+ view.dispatch(view.state.tr.setSelection(sel));
385
+ view.focus();
386
+ const domNode = view.nodeDOM(headingPmPos);
387
+ const el = domNode instanceof Element ? domNode : domNode?.parentElement;
388
+ el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
389
+ if (typeof window !== 'undefined' && window.innerWidth <= 640) {
390
+ setNavVisible(false);
391
+ }
392
+ }
393
+ catch {
394
+ /* at doc boundary — ignore */
395
+ }
396
+ }, [setNavVisible]);
397
+ const handleDragStart = useCallback((idx) => {
398
+ setDragIndex(idx);
399
+ }, []);
400
+ const handleDragOver = useCallback((e, idx) => {
401
+ e.preventDefault();
402
+ setDropIndex(idx);
403
+ }, []);
404
+ const handleDrop = useCallback((e, targetIdx) => {
405
+ e.preventDefault();
406
+ if (dragIndex === null || dragIndex === targetIdx) {
407
+ setDragIndex(null);
408
+ setDropIndex(null);
409
+ return;
410
+ }
411
+ const newText = reorderScenes(text, scenes, dragIndex, targetIdx);
412
+ setText(newText);
413
+ setDragIndex(null);
414
+ setDropIndex(null);
415
+ }, [dragIndex, text, scenes, setText]);
416
+ const commitSynopsis = useCallback((idx, scene) => {
417
+ setEditingSceneIdx(null);
418
+ const newSynopsis = editVal.trim();
419
+ if (newSynopsis === scene.synopsis)
420
+ return;
421
+ const view = getEditorView();
422
+ if (view) {
423
+ let headingPmPos = null;
424
+ let headingCount = 0;
425
+ view.state.doc.forEach((node, offset) => {
426
+ if (headingPmPos !== null)
427
+ return;
428
+ if (node.type.name === 'scene_heading') {
429
+ headingCount++;
430
+ if (headingCount === scene.index)
431
+ headingPmPos = offset;
432
+ }
433
+ });
434
+ if (headingPmPos !== null) {
435
+ let tr = view.state.tr;
436
+ let existingPos = null;
437
+ let index = view.state.doc.resolve(headingPmPos).index(0) + 1;
438
+ let pos = headingPmPos + view.state.doc.nodeAt(headingPmPos).nodeSize;
439
+ while (index < view.state.doc.childCount) {
440
+ const sibling = view.state.doc.child(index);
441
+ if (sibling.type.name === 'scene_metadata') {
442
+ if (sibling.attrs['metaKey'] === 'synopsis') {
443
+ existingPos = pos;
444
+ break;
445
+ }
446
+ }
447
+ else {
448
+ break;
449
+ }
450
+ pos += sibling.nodeSize;
451
+ index++;
452
+ }
453
+ if (existingPos !== null) {
454
+ const node = view.state.doc.nodeAt(existingPos);
455
+ if (newSynopsis === '') {
456
+ tr = tr.delete(existingPos, existingPos + node.nodeSize);
457
+ }
458
+ else {
459
+ tr = tr.replaceWith(existingPos + 1, existingPos + node.nodeSize - 1, schema.text(newSynopsis));
460
+ }
461
+ }
462
+ else if (newSynopsis !== '') {
463
+ const insertPos = headingPmPos + view.state.doc.nodeAt(headingPmPos).nodeSize;
464
+ const metaNode = schema.nodes['scene_metadata'].create({ metaKey: 'synopsis' }, schema.text(newSynopsis));
465
+ tr = tr.insert(insertPos, metaNode);
466
+ }
467
+ view.dispatch(tr);
468
+ }
469
+ }
470
+ else {
471
+ const nextText = updateSynopsisInText(text, scene.textOffset, newSynopsis);
472
+ setText(nextText);
473
+ }
474
+ }, [editVal, text, setText]);
475
+ const handleLockNumbers = useCallback(() => {
476
+ const view = getEditorView();
477
+ if (view) {
478
+ let tr = view.state.tr;
479
+ let headingCount = 0;
480
+ let posMap = [];
481
+ view.state.doc.forEach((node, offset) => {
482
+ if (node.type.name === 'scene_heading') {
483
+ headingCount++;
484
+ let hasNumber = false;
485
+ let index = view.state.doc.resolve(offset).index(0) + 1;
486
+ while (index < view.state.doc.childCount) {
487
+ const sibling = view.state.doc.child(index);
488
+ if (sibling.type.name === 'scene_metadata') {
489
+ if (sibling.attrs['metaKey'] === 'number') {
490
+ hasNumber = true;
491
+ break;
492
+ }
493
+ }
494
+ else {
495
+ break;
496
+ }
497
+ index++;
498
+ }
499
+ if (!hasNumber) {
500
+ posMap.push({ headingPos: offset, currentNum: String(headingCount) });
501
+ }
502
+ }
503
+ });
504
+ for (let k = posMap.length - 1; k >= 0; k--) {
505
+ const { headingPos, currentNum } = posMap[k];
506
+ const headingNode = view.state.doc.nodeAt(headingPos);
507
+ const insertPos = headingPos + headingNode.nodeSize;
508
+ const metaNode = schema.nodes['scene_metadata'].create({ metaKey: 'number' }, schema.text(currentNum));
509
+ tr = tr.insert(insertPos, metaNode);
510
+ }
511
+ view.dispatch(tr);
512
+ }
513
+ else {
514
+ const nextText = lockNumbersInText(text);
515
+ setText(nextText);
516
+ }
517
+ }, [text, setText]);
518
+ return (_jsxs("div", { className: "cs-navigator", role: "navigation", "aria-label": "Scene Navigator", children: [_jsxs("div", { className: "cs-nav-mobile-bar", children: [_jsx("span", { className: "cs-nav-mobile-title", children: t('navigator.title') }), _jsx("button", { type: "button", className: "cs-nav-mobile-close", "aria-label": "Close navigator", onClick: () => setNavVisible(false), children: _jsx(CloseIcon, { size: 18 }) })] }), _jsxs("div", { className: "cs-nav-list", children: [_jsx("button", { type: "button", className: "cs-nav-section-header cs-nav-frontmatter-btn", onClick: handleFrontmatter, title: t('navigator.frontmatter'), children: _jsx("span", { className: "cs-nav-title", children: t('navigator.frontmatter') }) }), _jsxs("div", { className: "cs-nav-section-header cs-nav-scenes-header", style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx("span", { className: "cs-nav-title", children: t('navigator.title') }), _jsx("span", { className: "cs-nav-count", children: scenes.length })] }), _jsxs("div", { style: { display: 'flex', gap: '4px', alignItems: 'center' }, onClick: e => e.stopPropagation(), children: [_jsx("button", { type: "button", className: "cs-nav-action-btn", onClick: () => setShowNavSynopsis(!showNavSynopsis), title: showNavSynopsis ? 'Hide Synopsis & Duration' : 'Show Synopsis & Duration', "aria-label": showNavSynopsis ? 'Hide Synopsis & Duration' : 'Show Synopsis & Duration', children: showNavSynopsis ? _jsx(EyeIcon, { size: 14 }) : _jsx(EyeOffIcon, { size: 14 }) }), _jsx("button", { type: "button", className: "cs-nav-action-btn", onClick: handleGenerateAllSceneMetadata, disabled: isProcessing, title: "Generate synopsis & estimate duration (all scenes)", "aria-label": "Generate synopsis & estimate duration (all scenes)", style: { opacity: isProcessing ? 0.5 : 1 }, children: _jsx(SparklesIcon, { size: 14 }) }), _jsx("button", { type: "button", className: "cs-nav-action-btn", onClick: handleLockNumbers, title: "Lock Scene Numbers", "aria-label": "Lock Scene Numbers", children: _jsx(LockIcon, { size: 14 }) })] })] }), isProcessing && progressText && (_jsxs("div", { className: "cs-nav-progress", style: {
519
+ display: 'flex',
520
+ alignItems: 'center',
521
+ gap: '8px',
522
+ padding: '6px 12px',
523
+ fontSize: '11px',
524
+ color: 'var(--cs-ui-accent, #c4760a)',
525
+ background: 'var(--cs-ui-bg-panel-subtle, #f4f2ea)',
526
+ borderBottom: '1px solid var(--cs-ui-border-light, #e2e0d6)',
527
+ fontWeight: 500,
528
+ }, children: [_jsx("span", { className: "cs-spinner", style: { width: '10px', height: '10px', borderWidth: '1.5px' } }), _jsx("span", { children: progressText })] })), scenes.map((scene, idx) => (_jsxs("div", { role: "button", tabIndex: 0, "aria-label": `Jump to ${scene.heading}`, onKeyDown: (e) => {
529
+ if (e.key === 'Enter' || e.key === ' ') {
530
+ e.preventDefault();
531
+ handleJump(scene);
532
+ }
533
+ }, className: [
534
+ 'cs-nav-item',
535
+ dragIndex === idx ? 'cs-nav-dragging' : '',
536
+ dropIndex === idx ? 'cs-nav-drop-target' : '',
537
+ ]
538
+ .filter(Boolean)
539
+ .join(' '), draggable: true, onClick: () => handleJump(scene), onDragStart: () => handleDragStart(idx), onDragOver: (e) => handleDragOver(e, idx), onDrop: (e) => handleDrop(e, idx), onDragEnd: () => {
540
+ setDragIndex(null);
541
+ setDropIndex(null);
542
+ }, children: [_jsxs("div", { className: "cs-nav-scene-info", children: [_jsx("div", { className: "cs-nav-heading", children: scene.heading }), showNavSynopsis && (_jsxs("div", { className: "cs-nav-synopsis-row", style: { marginTop: '4px' }, children: [_jsx("div", { style: { flex: 1, minWidth: 0 }, children: editingSceneIdx === idx ? (_jsx("input", { type: "text", className: "cs-nav-synopsis-input", value: editVal, onChange: e => setEditVal(e.target.value), onBlur: () => commitSynopsis(idx, scene), onKeyDown: e => {
543
+ if (e.key === 'Enter')
544
+ commitSynopsis(idx, scene);
545
+ if (e.key === 'Escape')
546
+ setEditingSceneIdx(null);
547
+ }, autoFocus: true, onClick: e => e.stopPropagation() })) : scene.synopsis ? (_jsx("div", { className: "cs-nav-scene-synopsis", onClick: e => {
548
+ e.stopPropagation();
549
+ setEditingSceneIdx(idx);
550
+ setEditVal(scene.synopsis);
551
+ }, title: "Click to edit synopsis", children: scene.synopsis })) : (_jsx("div", { className: "cs-nav-scene-synopsis cs-nav-synopsis-placeholder", onClick: e => {
552
+ e.stopPropagation();
553
+ setEditingSceneIdx(idx);
554
+ setEditVal('');
555
+ }, role: "button", tabIndex: 0, "aria-label": "Add synopsis", onKeyDown: e => {
556
+ if (e.key === 'Enter' || e.key === ' ') {
557
+ e.stopPropagation();
558
+ e.preventDefault();
559
+ setEditingSceneIdx(idx);
560
+ setEditVal('');
561
+ }
562
+ }, children: "+ Synopsis" })) }), _jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); void handleSceneMetadata(scene); }, disabled: isProcessing, title: "Generate synopsis & estimate duration", "aria-label": "Generate synopsis & estimate duration", className: "cs-nav-action-btn cs-nav-synopsis-ai-btn", style: { color: 'var(--cs-ui-accent, #c4760a)', ...(isProcessing ? { opacity: 0.5 } : {}) }, children: _jsx(SparklesIcon, { size: 12 }) })] })), (scene.status || (showNavSynopsis && scene.estDuration)) && (_jsxs("div", { style: { display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center', marginTop: '6px' }, children: [scene.status && (_jsx("span", { className: `cs-nav-status cs-status-${scene.status
563
+ .toLowerCase()
564
+ .replace(/\s+/g, '-')}`, children: scene.status })), showNavSynopsis && scene.estDuration && (_jsxs("span", { className: "cs-nav-status", title: "Estimated Onscreen Duration", style: {
565
+ background: 'rgba(217, 119, 6, 0.1)',
566
+ color: 'var(--cs-ui-accent, #c4760a)',
567
+ border: '1px solid rgba(217, 119, 6, 0.25)',
568
+ fontWeight: 600,
569
+ }, children: ["\u23F1 ", scene.estDuration] }))] }))] }), scene.id && _jsx("div", { className: "cs-nav-scene-num", children: scene.id })] }, scene.id ?? `scene-${idx}`))), scenes.length === 0 && (_jsx("div", { className: "cs-nav-empty", children: t('navigator.empty') })), _jsx("div", { style: { marginTop: '20px', padding: '0 8px 10px', borderTop: '1px solid #eee', paddingTop: '15px' }, children: _jsxs("button", { type: "button", className: "cs-nav-section-header cs-nav-frontmatter-btn", onClick: () => setStatsOpen(true), style: { width: '100%', textAlign: 'left', display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }, children: [_jsx(StatsIcon, { size: 14 }), " ", _jsx("span", { className: "cs-nav-title", children: "Statistics" })] }) })] }), statsOpen && _jsx(StatisticsDialog, { onClose: () => setStatsOpen(false) })] }));
570
+ }
571
+ /**
572
+ * Reorder scene blocks in the source text.
573
+ * Moves the entire block (heading through next heading) without mutating IDs.
574
+ */
575
+ function reorderScenes(text, scenes, fromIdx, toIdx) {
576
+ const lines = text.split('\n');
577
+ const sceneLineRanges = scenes.map((scene, i) => {
578
+ const nextScene = scenes[i + 1];
579
+ const startLine = findLineAt(lines, scene.textOffset);
580
+ const endLine = nextScene
581
+ ? findLineAt(lines, nextScene.textOffset) - 1
582
+ : lines.length - 1;
583
+ return { startLine, endLine };
584
+ });
585
+ // Extract prefix before first scene
586
+ const firstSceneStart = sceneLineRanges[0]?.startLine ?? 0;
587
+ const prefixLines = lines.slice(0, firstSceneStart);
588
+ // Extract line blocks for each scene
589
+ const blocks = sceneLineRanges.map(r => lines.slice(r.startLine, r.endLine + 1));
590
+ const blockToMove = blocks[fromIdx];
591
+ if (!blockToMove)
592
+ return text;
593
+ // Remove the block from the list
594
+ const remainingBlocks = blocks.filter((_, i) => i !== fromIdx);
595
+ // Insert the block at the target index
596
+ remainingBlocks.splice(toIdx, 0, blockToMove);
597
+ // Rebuild the text
598
+ const finalLines = [...prefixLines];
599
+ for (const b of remainingBlocks) {
600
+ finalLines.push(...b);
601
+ }
602
+ return finalLines.join('\n');
603
+ }
604
+ function findLineAt(lines, offset) {
605
+ let pos = 0;
606
+ for (let i = 0; i < lines.length; i++) {
607
+ if (pos >= offset)
608
+ return i;
609
+ pos += (lines[i]?.length ?? 0) + 1;
610
+ }
611
+ return lines.length - 1;
612
+ }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface Props {
3
+ onClose: () => void;
4
+ }
5
+ export declare function StatisticsDialog({ onClose }: Props): React.JSX.Element;
6
+ export {};