@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,101 @@
1
+ import React, { useState, useEffect, useRef } from 'react'
2
+ import { useLanguage } from '../i18n/LanguageContext'
3
+ import { useDocument } from '../context/DocumentContext'
4
+ import { getEditorView } from '../editor/editor-bus'
5
+
6
+ const LANG_NAMES: Record<string, string> = {
7
+ en: 'English',
8
+ hi: 'Hindi (Devanagari)',
9
+ ta: 'Tamil',
10
+ te: 'Telugu',
11
+ kn: 'Kannada',
12
+ ml: 'Malayalam',
13
+ bn: 'Bengali',
14
+ gu: 'Gujarati',
15
+ pa: 'Gurmukhi',
16
+ or: 'Odia',
17
+ si: 'Sinhala',
18
+ }
19
+
20
+ export function LanguageStatus() {
21
+ const { currentLanguage } = useLanguage()
22
+ const { text, setText } = useDocument()
23
+ const [menuOpen, setMenuOpen] = useState(false)
24
+ const menuRef = useRef<HTMLDivElement>(null)
25
+
26
+ const langName = LANG_NAMES[currentLanguage] ?? currentLanguage
27
+
28
+ useEffect(() => {
29
+ if (!menuOpen) return
30
+ const handleClickOutside = (e: MouseEvent) => {
31
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
32
+ setMenuOpen(false)
33
+ }
34
+ }
35
+ const handleKeyDown = (e: KeyboardEvent) => {
36
+ if (e.key === 'Escape') setMenuOpen(false)
37
+ }
38
+ document.addEventListener('mousedown', handleClickOutside)
39
+ document.addEventListener('keydown', handleKeyDown)
40
+ return () => {
41
+ document.removeEventListener('mousedown', handleClickOutside)
42
+ document.removeEventListener('keydown', handleKeyDown)
43
+ }
44
+ }, [menuOpen])
45
+
46
+ const handleSelectLanguage = (code: string) => {
47
+ const view = getEditorView()
48
+ if (view) {
49
+ const { state, dispatch } = view
50
+ const { from, to } = state.selection
51
+ const tagText = `{lang=${code}}`
52
+ dispatch(state.tr.insertText(tagText, from, to))
53
+ view.focus()
54
+ } else {
55
+ setText(text + `\n{lang=${code}}`)
56
+ }
57
+ setMenuOpen(false)
58
+ }
59
+
60
+ return (
61
+ <div className="cs-lang-status" ref={menuRef}>
62
+ <button
63
+ type="button"
64
+ className="cs-lang-status-btn cs-lang-pill"
65
+ title={`Script language at caret: ${langName} (${currentLanguage.toUpperCase()})`}
66
+ aria-label={`Script language: ${langName}`}
67
+ aria-haspopup="menu"
68
+ aria-expanded={menuOpen}
69
+ onClick={() => setMenuOpen(!menuOpen)}
70
+ >
71
+ <span className="cs-lang-pill-script">अ/A</span>
72
+ <span className="cs-lang-code">{currentLanguage.toUpperCase()}</span>
73
+ </button>
74
+
75
+ {menuOpen && (
76
+ <div className="cs-lang-menu" role="menu" aria-label="Script languages">
77
+ {Object.entries(LANG_NAMES).map(([code, name]) => {
78
+ const isActive = currentLanguage === code
79
+ return (
80
+ <button
81
+ type="button"
82
+ key={code}
83
+ role="menuitem"
84
+ className={`cs-lang-option${isActive ? ' cs-lang-active' : ''}`}
85
+ onClick={(e) => {
86
+ e.stopPropagation()
87
+ handleSelectLanguage(code)
88
+ }}
89
+ >
90
+ <span>{name}</span>
91
+ <span className="cs-lang-code" style={{ fontSize: '10px', opacity: 0.7 }}>
92
+ {code.toUpperCase()}
93
+ </span>
94
+ </button>
95
+ )
96
+ })}
97
+ </div>
98
+ )}
99
+ </div>
100
+ )
101
+ }
@@ -0,0 +1,135 @@
1
+ import React, { useMemo, useState, useEffect } from 'react'
2
+ import { useDocument } from '../context/DocumentContext'
3
+ import { LanguageStatus } from './LanguageStatus'
4
+ import { buildSceneList } from '../navigator/scene-list'
5
+ import { StatisticsDialog } from '../navigator/StatisticsDialog'
6
+ import { VersionHistory } from './VersionHistory'
7
+ import { StatsIcon, HistoryIcon } from './icons'
8
+ import { subscribeToAICalls, formatActiveCalls } from '../ai/ai-client'
9
+ import { useTranslation } from '../i18n/useTranslation'
10
+
11
+ function AIStatusBarStatus() {
12
+ const [activeCalls, setActiveCalls] = useState<{ providerName: string; model: string }[]>([])
13
+
14
+ useEffect(() => {
15
+ return subscribeToAICalls(setActiveCalls)
16
+ }, [])
17
+
18
+ if (activeCalls.length === 0) return null
19
+
20
+ return (
21
+ <div
22
+ className="cs-ai-status-indicator"
23
+ style={{
24
+ display: 'inline-flex',
25
+ alignItems: 'center',
26
+ gap: '6px',
27
+ color: 'var(--cs-ui-accent, #c4760a)',
28
+ fontWeight: 600,
29
+ fontSize: '11px',
30
+ marginRight: '8px',
31
+ }}
32
+ >
33
+ <span className="cs-spinner" style={{ width: '10px', height: '10px', borderWidth: '1.5px' }} />
34
+ <span>{formatActiveCalls(activeCalls)}</span>
35
+ </div>
36
+ )
37
+ }
38
+
39
+ export function StatusBar() {
40
+ const { text, isDirty, lastSaveTarget, filePath, versions, versionHistoryVisible, setVersionHistoryVisible, restoreVersion, clearVersionHistory } = useDocument()
41
+ const { t } = useTranslation()
42
+ const [statsOpen, setStatsOpen] = useState(false)
43
+
44
+ // VS Code-style: a local autosave backs the content up but never counts as
45
+ // "saved" in the main label — only a real write to the file (saveFile/
46
+ // saveFileAs, which clears isDirty) does. While still dirty, the tooltip
47
+ // says whether that local backup has caught up with the latest edits yet.
48
+ const saveLabel = isDirty
49
+ ? t('statusbar.unsavedChanges')
50
+ : lastSaveTarget === 'file' ? t('statusbar.savedToFile')
51
+ : t('statusbar.allChangesSaved')
52
+ const saveTooltip = isDirty
53
+ ? lastSaveTarget === 'local' ? t('statusbar.savedLocallyTooltip') : undefined
54
+ : lastSaveTarget === 'file' ? t('statusbar.savedToFileTooltip')
55
+ : undefined
56
+
57
+ const scenes = useMemo(() => buildSceneList(text), [text])
58
+
59
+ const { wordCount, pageEstimate } = useMemo(() => {
60
+ const trimmed = text.trim()
61
+ const words = trimmed ? trimmed.split(/\s+/).length : 0
62
+ // Standard screenplay rule: ~54 lines or ~200-250 words per page
63
+ const lines = text.split('\n').length
64
+ const estPages = Math.max(1, Math.round(lines / 54))
65
+ return { wordCount: words, pageEstimate: estPages }
66
+ }, [text])
67
+
68
+ return (
69
+ <>
70
+ <footer className="cs-status-bar" role="contentinfo" aria-label="Status bar">
71
+ <div className="cs-sb-left">
72
+ <span className="cs-sb-metric cs-sb-metric-scenes" title={t('statusbar.scenesTooltip')}>
73
+ <strong>{scenes.length}</strong> <span className="cs-sb-metric-label">{scenes.length === 1 ? t('statusbar.scene') : t('statusbar.scenes')}</span>
74
+ </span>
75
+ <span className="cs-sb-sep">•</span>
76
+ <span className="cs-sb-metric cs-sb-metric-pages" title={t('statusbar.pagesTooltip')}>
77
+ <strong>~{pageEstimate}</strong> <span className="cs-sb-metric-label">{pageEstimate === 1 ? t('statusbar.page') : t('statusbar.pages')}</span>
78
+ </span>
79
+ <span className="cs-sb-sep cs-sb-words-sep">•</span>
80
+ <span className="cs-sb-metric cs-sb-metric-words" title={t('statusbar.wordsTooltip')}>
81
+ <strong>{wordCount.toLocaleString()}</strong> <span className="cs-sb-metric-label">{t('statusbar.words')}</span>
82
+ </span>
83
+ </div>
84
+
85
+ <div className="cs-sb-center">
86
+ <div className="cs-sb-save" role="status" aria-live="polite" title={saveTooltip}>
87
+ <span className={`cs-sb-dot ${isDirty ? 'cs-sb-dot-dirty' : 'cs-sb-dot-clean'}`} />
88
+ <span className={isDirty ? 'cs-sb-save-dirty' : 'cs-sb-save-clean'}>
89
+ {saveLabel}
90
+ </span>
91
+ </div>
92
+ </div>
93
+
94
+ <div className="cs-sb-right">
95
+ <AIStatusBarStatus />
96
+ {filePath && versions.length > 0 && (
97
+ <button
98
+ type="button"
99
+ className="cs-nav-action-btn"
100
+ onClick={() => setVersionHistoryVisible(true)}
101
+ title={`${versions.length} version${versions.length === 1 ? '' : 's'} saved`}
102
+ style={{ fontSize: '11px', gap: '4px', padding: '2px 6px' }}
103
+ >
104
+ <HistoryIcon size={13} />
105
+ <span>{versions.length}</span>
106
+ </button>
107
+ )}
108
+ <button
109
+ type="button"
110
+ className="cs-nav-action-btn"
111
+ onClick={() => setStatsOpen(true)}
112
+ title={t('statusbar.statsTooltip')}
113
+ style={{ fontSize: '11px', gap: '4px', padding: '2px 6px' }}
114
+ >
115
+ <StatsIcon size={13} />
116
+ <span>{t('statusbar.statsButton')}</span>
117
+ </button>
118
+ <LanguageStatus />
119
+ </div>
120
+ </footer>
121
+
122
+ {statsOpen && <StatisticsDialog onClose={() => setStatsOpen(false)} />}
123
+ {filePath && (
124
+ <VersionHistory
125
+ versions={versions}
126
+ filePath={filePath}
127
+ onRestore={restoreVersion}
128
+ onClearAll={clearVersionHistory}
129
+ visible={versionHistoryVisible}
130
+ onClose={() => setVersionHistoryVisible(false)}
131
+ />
132
+ )}
133
+ </>
134
+ )
135
+ }
@@ -0,0 +1,89 @@
1
+ import React from 'react'
2
+
3
+ // The Sutrata monogram: an S-shaped filmstrip ribbon with sprocket holes and a
4
+ // center reel, in the app's Deep Amber / Amber Glow palette. Source of truth for
5
+ // the shape and color values is packages/app/public/sutrata-monogram.svg — keep
6
+ // the two in sync if either changes.
7
+
8
+ interface LogoProps {
9
+ size?: number
10
+ className?: string
11
+ showText?: boolean
12
+ }
13
+
14
+ const MONOGRAM_ASPECT = 231.65818 / 324.60373
15
+
16
+ export function SutrataLogo({ size = 20, className = '', showText = true }: LogoProps) {
17
+ const height = size
18
+ const width = size * MONOGRAM_ASPECT
19
+
20
+ return (
21
+ <div className={`cs-brand-mark ${className}`} style={{ display: 'inline-flex', alignItems: 'center', gap: '7px', userSelect: 'none' }}>
22
+ <svg
23
+ width={width}
24
+ height={height}
25
+ viewBox="0 0 231.65818 324.60373"
26
+ xmlns="http://www.w3.org/2000/svg"
27
+ aria-hidden="true"
28
+ style={{ flexShrink: 0 }}
29
+ >
30
+ <g transform="translate(-3.0466903,-3.069361)">
31
+ <g>
32
+ <path
33
+ fill="#502308"
34
+ d="M 76.35291,222.53041 C 75.478969,209.22098 71.352034,194.29907 56.664655,177.24787 35.352192,152.22995 46.850499,139.85601 62.978003,130.13046 L 3.6899176,150.4351 c 5.0786411,20.05339 13.9650544,36.08746 28.6678214,45.98205 16.601069,11.28577 30.572578,19.06546 43.995171,26.11326 z" />
35
+ <path
36
+ fill="#92400e"
37
+ d="m 213.6921,3.069361 c 0,0 0.96049,32.353598 -0.14192,43.494212 -1.10241,11.140614 -14.06146,22.120287 -23.28179,26.493068 -26.78003,12.700542 -124.035721,55.394599 -124.035721,55.394599 0,0 -7.314493,3.8005 -14.852175,9.23243 -7.537682,5.43193 -23.670324,18.71663 -28.098707,33.71845 -1.670439,5.65887 -2.408461,14.04935 -2.408461,14.04935 C 8.2528446,173.99227 -4.2995876,149.42142 8.2289074,120.92479 18.129451,103.32842 31.24605,87.051456 55.796006,75.465101 101.11959,52.013746 150.99666,33.115884 198.69801,12.042303 Z" />
38
+ <g>
39
+ <path
40
+ fill="#fbbf24"
41
+ d="m 157.01648,44.42498 c 14.17698,-5.71484 11.14517,-6.35429 11.1893,10.23596 -0.40601,13.28313 2.27097,11.17087 -9.33278,16.05641 -13.29184,4.92069 -12.50077,7.73152 -12.14266,-9.03173 -0.29088,-15.61229 -1.75968,-11.39786 10.28614,-17.26064 z" />
42
+ <path
43
+ fill="#fbbf24"
44
+ d="m 118.47741,61.789741 c 14.17699,-5.71484 11.14518,-6.35429 11.18931,10.23596 -0.40601,13.28313 2.27097,11.17087 -9.33279,16.05641 -13.29183,4.92069 -12.50076,7.73152 -12.14265,-9.03173 -0.29089,-15.61229 -1.75968,-11.39786 10.28613,-17.26064 z" />
45
+ <path
46
+ fill="#fbbf24"
47
+ d="m 80.42697,79.119631 c 14.176988,-5.71484 11.145175,-6.35429 11.189307,10.23596 -0.406014,13.283129 2.27097,11.170869 -9.332786,16.056409 -13.291838,4.92069 -12.500765,7.73152 -12.142656,-9.031729 -0.290884,-15.61229 -1.759682,-11.39786 10.286135,-17.26064 z" />
48
+ </g>
49
+ </g>
50
+ <g transform="rotate(180,118.87578,165.37123)">
51
+ <path
52
+ fill="#502308"
53
+ d="M 76.35291,222.53041 C 75.478969,209.22098 71.352034,194.29907 56.664655,177.24787 35.352192,152.22995 46.850499,139.85601 62.978003,130.13046 L 3.6899176,150.4351 c 5.0786411,20.05339 13.9650544,36.08746 28.6678214,45.98205 16.601069,11.28577 30.572578,19.06546 43.995171,26.11326 z" />
54
+ <path
55
+ fill="#92400e"
56
+ d="m 213.6921,3.069361 c 0,0 0.96049,32.353598 -0.14192,43.494212 -1.10241,11.140614 -14.06146,22.120287 -23.28179,26.493068 -26.78003,12.700542 -124.035721,55.394599 -124.035721,55.394599 0,0 -7.314493,3.8005 -14.852175,9.23243 -7.537682,5.43193 -23.670324,18.71663 -28.098707,33.71845 -1.670439,5.65887 -2.408461,14.04935 -2.408461,14.04935 C 8.2528446,173.99227 -4.2995876,149.42142 8.2289074,120.92479 18.129451,103.32842 31.24605,87.051456 55.796006,75.465101 101.11959,52.013746 150.99666,33.115884 198.69801,12.042303 Z" />
57
+ <g>
58
+ <path
59
+ fill="#fbbf24"
60
+ d="m 157.01648,44.42498 c 14.17698,-5.71484 11.14517,-6.35429 11.1893,10.23596 -0.40601,13.28313 2.27097,11.17087 -9.33278,16.05641 -13.29184,4.92069 -12.50077,7.73152 -12.14266,-9.03173 -0.29088,-15.61229 -1.75968,-11.39786 10.28614,-17.26064 z" />
61
+ <path
62
+ fill="#fbbf24"
63
+ d="m 118.47741,61.789741 c 14.17699,-5.71484 11.14518,-6.35429 11.18931,10.23596 -0.40601,13.28313 2.27097,11.17087 -9.33279,16.05641 -13.29183,4.92069 -12.50076,7.73152 -12.14265,-9.03173 -0.29089,-15.61229 -1.75968,-11.39786 10.28613,-17.26064 z" />
64
+ <path
65
+ fill="#fbbf24"
66
+ d="m 80.42697,79.119631 c 14.176988,-5.71484 11.145175,-6.35429 11.189307,10.23596 -0.406014,13.283129 2.27097,11.170869 -9.332786,16.056409 -13.291838,4.92069 -12.500765,7.73152 -12.142656,-9.031729 -0.290884,-15.61229 -1.759682,-11.39786 10.286135,-17.26064 z" />
67
+ </g>
68
+ </g>
69
+ <g transform="matrix(1.1983554,-0.06280315,0.06280315,1.1983554,-34.773977,-29.239318)">
70
+ <path
71
+ fill="#fbbf24"
72
+ d="m 90.317275,164.77885 48.369915,-17.86274 c -6.75724,-6.27184 -19.73649,-10.33577 -32.1128,-4.41552 -11.797753,5.74748 -14.462218,13.89845 -16.257115,22.27826 z" />
73
+ <path
74
+ fill="#fbbf24"
75
+ d="M 148.18495,171.26956 99.815045,189.1323 c 6.757235,6.27184 19.736485,10.33577 32.112795,4.41552 11.79775,-5.74748 14.46221,-13.89845 16.25711,-22.27826 z" />
76
+ <path
77
+ fill="#fbbf24"
78
+ d="m 90.417628,169.79648 51.882262,-19.36804 c 3.81102,4.8999 5.68801,10.3205 6.02115,16.15676 l -52.032789,19.06698 c -3.269689,-4.15996 -5.408353,-9.28938 -5.870623,-15.8557 z" />
79
+ </g>
80
+ </g>
81
+ </svg>
82
+ {showText && (
83
+ <span className="cs-brand-text" style={{ fontWeight: 700, fontSize: '11px', letterSpacing: '1.2px', textTransform: 'uppercase', color: 'var(--cs-ui-accent-badge, #d97706)' }}>
84
+ Sutrata
85
+ </span>
86
+ )}
87
+ </div>
88
+ )
89
+ }
@@ -0,0 +1,46 @@
1
+ import React, { useEffect } from 'react'
2
+
3
+ export interface ToastItem {
4
+ id: string
5
+ message: string
6
+ type?: 'info' | 'success' | 'warn' | 'error'
7
+ }
8
+
9
+ interface ToastContainerProps {
10
+ toasts: ToastItem[]
11
+ onDismiss: (id: string) => void
12
+ }
13
+
14
+ export function ToastContainer({ toasts, onDismiss }: ToastContainerProps) {
15
+ return (
16
+ <div className="cs-toast-container" aria-live="polite" aria-atomic="true">
17
+ {toasts.map((toast) => (
18
+ <ToastMessage key={toast.id} toast={toast} onDismiss={onDismiss} />
19
+ ))}
20
+ </div>
21
+ )
22
+ }
23
+
24
+ function ToastMessage({ toast, onDismiss }: { toast: ToastItem; onDismiss: (id: string) => void }) {
25
+ useEffect(() => {
26
+ const timer = setTimeout(() => {
27
+ onDismiss(toast.id)
28
+ }, 3500)
29
+ return () => clearTimeout(timer)
30
+ }, [toast.id, onDismiss])
31
+
32
+ const typeClass =
33
+ toast.type === 'success'
34
+ ? 'cs-toast-success'
35
+ : toast.type === 'error'
36
+ ? 'cs-toast-error'
37
+ : toast.type === 'warn'
38
+ ? 'cs-toast-warn'
39
+ : 'cs-toast-info'
40
+
41
+ return (
42
+ <div className={`cs-toast ${typeClass}`} role="status">
43
+ <span>{toast.message}</span>
44
+ </div>
45
+ )
46
+ }
@@ -0,0 +1,30 @@
1
+ import React from 'react'
2
+
3
+ interface ToolbarButtonProps {
4
+ label: string
5
+ sigil?: string
6
+ shortcut?: string
7
+ active?: boolean
8
+ onClick: () => void
9
+ children: React.ReactNode
10
+ }
11
+
12
+ export function ToolbarButton({ label, sigil, shortcut, active, onClick, children }: ToolbarButtonProps) {
13
+ return (
14
+ <button
15
+ type="button"
16
+ aria-label={label}
17
+ className={`cs-tb-btn${active ? ' cs-tb-active' : ''}`}
18
+ onClick={onClick}
19
+ >
20
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
21
+ {children}
22
+ </div>
23
+ <span className="cs-tb-tip" role="tooltip">
24
+ <span>{label}</span>
25
+ {sigil ? <span className="cs-tb-sigil">{sigil}</span> : null}
26
+ {shortcut ? <span className="cs-tb-key">{shortcut}</span> : null}
27
+ </span>
28
+ </button>
29
+ )
30
+ }
@@ -0,0 +1,212 @@
1
+ import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'
2
+ import { diffWordsWithSpace, type Change } from 'diff'
3
+ import type { VersionEntry } from '../types'
4
+
5
+ interface VersionHistoryProps {
6
+ versions: VersionEntry[]
7
+ filePath: string | null
8
+ onRestore: (entry: VersionEntry) => void
9
+ onClearAll: () => Promise<void>
10
+ visible: boolean
11
+ onClose: () => void
12
+ }
13
+
14
+ interface DiffMarker {
15
+ type: 'added' | 'removed'
16
+ position: number // 0..1, fraction of the diff pane's scrollHeight
17
+ }
18
+
19
+ // Word-level diff over the full text (not line-bucketed): if only one word in
20
+ // a sentence changed, only that word is marked added/removed — the rest of
21
+ // the sentence comes back as an unchanged, unstyled Change chunk. Whitespace
22
+ // (including newlines) is treated as part of the diff so line breaks survive.
23
+ function computeDiff(oldText: string, newText: string): Change[] {
24
+ return diffWordsWithSpace(oldText, newText)
25
+ }
26
+
27
+ // Two markers within this many pixels of each other collapse into one, so a
28
+ // tight run of word edits doesn't paint a solid wall of ticks.
29
+ const CLUSTER_PX = 10
30
+
31
+ function formatTimestamp(ts: number): string {
32
+ return new Date(ts).toLocaleString(undefined, {
33
+ month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit',
34
+ })
35
+ }
36
+
37
+ export function VersionHistory({ versions, filePath, onRestore, onClearAll, visible, onClose }: VersionHistoryProps) {
38
+ const [selectedId, setSelectedId] = useState<string | null>(null)
39
+ const diffScrollRef = useRef<HTMLDivElement>(null)
40
+ // Maps a diff-part index (only added/removed parts) to its rendered <span>,
41
+ // so marker positions come from real layout instead of a text-based estimate —
42
+ // screenplay format has wildly uneven character density per line (short
43
+ // character cues and blank lines vs. long action paragraphs), which threw
44
+ // off an earlier character-offset heuristic by a page or more.
45
+ const spanRefMap = useRef(new Map<number, HTMLSpanElement>())
46
+ const [markers, setMarkers] = useState<DiffMarker[]>([])
47
+
48
+ const currentVersion = versions[0] ?? null
49
+ const selectedVersion = useMemo(
50
+ () => versions.find(v => v.id === selectedId) ?? null,
51
+ [versions, selectedId],
52
+ )
53
+
54
+ const diff = useMemo(() => {
55
+ if (!selectedVersion || !currentVersion || selectedVersion.id === currentVersion.id) return null
56
+ return computeDiff(selectedVersion.content, currentVersion.content)
57
+ }, [selectedVersion, currentVersion])
58
+
59
+ useLayoutEffect(() => {
60
+ const container = diffScrollRef.current
61
+ if (!container || !diff) { setMarkers([]); return }
62
+
63
+ const raw: { type: 'added' | 'removed'; top: number }[] = []
64
+ diff.forEach((part, i) => {
65
+ if (!part.added && !part.removed) return
66
+ const span = spanRefMap.current.get(i)
67
+ if (!span) return
68
+ raw.push({ type: part.added ? 'added' : 'removed', top: span.offsetTop })
69
+ })
70
+ raw.sort((a, b) => a.top - b.top)
71
+
72
+ const scrollHeight = container.scrollHeight || 1
73
+ const clustered: DiffMarker[] = []
74
+ let lastTop = -Infinity
75
+ let lastType: 'added' | 'removed' | null = null
76
+ for (const m of raw) {
77
+ if (lastType === m.type && m.top - lastTop < CLUSTER_PX) continue
78
+ clustered.push({ type: m.type, position: m.top / scrollHeight })
79
+ lastTop = m.top
80
+ lastType = m.type
81
+ }
82
+ setMarkers(clustered)
83
+ }, [diff])
84
+
85
+ const scrollToMarker = (position: number) => {
86
+ const el = diffScrollRef.current
87
+ if (!el) return
88
+ const target = Math.max(0, position * el.scrollHeight - el.clientHeight / 2)
89
+ if (typeof el.scrollTo === 'function') el.scrollTo({ top: target, behavior: 'smooth' })
90
+ else el.scrollTop = target
91
+ }
92
+
93
+ if (!visible || !filePath) return null
94
+
95
+ return (
96
+ <div
97
+ className="cs-dialog-overlay"
98
+ role="presentation"
99
+ onClick={e => { if (e.target === e.currentTarget) onClose() }}
100
+ >
101
+ <div
102
+ className="cs-dialog"
103
+ role="dialog"
104
+ aria-modal="true"
105
+ aria-labelledby="cs-version-dialog-title"
106
+ style={{ maxWidth: '620px', width: '92%', maxHeight: '80vh', display: 'flex', flexDirection: 'column' }}
107
+ >
108
+ <div className="cs-panel-header">
109
+ <span id="cs-version-dialog-title" className="cs-panel-title">Version History</span>
110
+ <button className="cs-panel-close" onClick={onClose} aria-label="Close">×</button>
111
+ </div>
112
+
113
+ <div className="cs-version-body">
114
+ <div className="cs-version-list">
115
+ {versions.length === 0 ? (
116
+ <div className="cs-version-empty">No versions saved yet</div>
117
+ ) : (
118
+ versions.map((v, idx) => (
119
+ <button
120
+ key={v.id}
121
+ type="button"
122
+ className={`cs-version-list-item${selectedId === v.id ? ' cs-version-list-item-active' : ''}`}
123
+ onClick={() => setSelectedId(v.id)}
124
+ >
125
+ <div className="cs-version-list-item-label">{idx === 0 ? 'Current' : formatTimestamp(v.timestamp)}</div>
126
+ {idx !== 0 && <div className="cs-version-list-item-date">{formatTimestamp(v.timestamp)}</div>}
127
+ </button>
128
+ ))
129
+ )}
130
+ </div>
131
+
132
+ <div className="cs-version-diff-wrap">
133
+ <div className="cs-version-diff" ref={diffScrollRef}>
134
+ {!selectedVersion ? (
135
+ <div className="cs-version-empty">Select a version to compare against the current document</div>
136
+ ) : selectedVersion.id === currentVersion?.id ? (
137
+ <div className="cs-version-empty">This is the current version</div>
138
+ ) : diff ? (
139
+ diff.every(part => !part.added && !part.removed) ? (
140
+ <div className="cs-version-empty">No differences from the current version</div>
141
+ ) : (
142
+ <div className="cs-version-diff-content">
143
+ {diff.map((part, i) => {
144
+ const isChange = part.added || part.removed
145
+ return (
146
+ <span
147
+ key={i}
148
+ ref={isChange ? (el) => {
149
+ if (el) spanRefMap.current.set(i, el)
150
+ else spanRefMap.current.delete(i)
151
+ } : undefined}
152
+ className={
153
+ part.added ? 'cs-version-diff-added'
154
+ : part.removed ? 'cs-version-diff-removed'
155
+ : undefined
156
+ }
157
+ >
158
+ {part.value}
159
+ </span>
160
+ )
161
+ })}
162
+ </div>
163
+ )
164
+ ) : null}
165
+ </div>
166
+
167
+ {/* Rendered unconditionally (even with zero markers) so its width is
168
+ reserved from the diff pane's very first layout pass — otherwise
169
+ measuring offsetTop before this strip exists, then having it pop
170
+ in afterward and narrow the text column, would make every
171
+ position stale by however much the text rewrapped. */}
172
+ <div className="cs-version-diff-minimap" aria-hidden="true">
173
+ {markers.map((m, i) => (
174
+ <button
175
+ key={i}
176
+ type="button"
177
+ className={`cs-version-diff-marker cs-version-diff-marker-${m.type}`}
178
+ style={{ top: `${m.position * 100}%` }}
179
+ title={m.type === 'added' ? 'Jump to added text' : 'Jump to removed text'}
180
+ onClick={() => scrollToMarker(m.position)}
181
+ />
182
+ ))}
183
+ </div>
184
+ </div>
185
+ </div>
186
+
187
+ <div className="cs-version-footer">
188
+ <button
189
+ type="button"
190
+ className="cs-confirm-btn-cancel cs-version-clear-btn"
191
+ onClick={() => void onClearAll()}
192
+ disabled={versions.length === 0}
193
+ >
194
+ Delete Version History
195
+ </button>
196
+ <div style={{ display: 'flex', gap: '10px' }}>
197
+ <button type="button" className="cs-confirm-btn-cancel" onClick={onClose}>Close</button>
198
+ {selectedVersion && selectedVersion.id !== currentVersion?.id && (
199
+ <button
200
+ type="button"
201
+ className="cs-confirm-btn-primary"
202
+ onClick={() => onRestore(selectedVersion)}
203
+ >
204
+ Restore This Version
205
+ </button>
206
+ )}
207
+ </div>
208
+ </div>
209
+ </div>
210
+ </div>
211
+ )
212
+ }
@@ -0,0 +1,9 @@
1
+ import { useDocument } from '../context/DocumentContext'
2
+
3
+ /** Renders the document's frontmatter `watermark:` text.
4
+ * Hidden in the screen editor UI, visible only in print output. */
5
+ export function Watermark() {
6
+ const { watermarkText } = useDocument()
7
+ if (!watermarkText) return null
8
+ return <div className="cs-watermark" aria-hidden="true">{watermarkText}</div>
9
+ }