@sutrata/editor 0.1.1 → 0.2.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 (268) hide show
  1. package/README.md +87 -0
  2. package/dist/ai/VoiceConfirmDialog.d.ts +1 -0
  3. package/dist/ai/VoiceConfirmDialog.js +6 -6
  4. package/dist/ai/VoiceToolbar.d.ts +1 -0
  5. package/dist/ai/VoiceToolbar.js +22 -16
  6. package/dist/ai/clean-response.d.ts +7 -0
  7. package/dist/ai/clean-response.js +58 -0
  8. package/dist/ai/format-transcript.d.ts +3 -0
  9. package/dist/ai/format-transcript.js +7 -0
  10. package/dist/ai/prompts.d.ts +8 -1
  11. package/dist/ai/prompts.js +34 -7
  12. package/dist/autocomplete/suggestions.js +1 -1
  13. package/dist/context/DocumentContext.d.ts +28 -4
  14. package/dist/context/DocumentContext.js +67 -37
  15. package/dist/editor/EditorView.js +32 -4
  16. package/dist/editor/SelectionMenu.js +5 -3
  17. package/dist/editor/external-decorations-plugin.d.ts +23 -0
  18. package/dist/editor/external-decorations-plugin.js +151 -0
  19. package/dist/editor/omitted-scene-plugin.d.ts +9 -0
  20. package/dist/editor/omitted-scene-plugin.js +44 -0
  21. package/dist/editor/plugins.js +4 -0
  22. package/dist/editor/private-metadata-plugin.d.ts +10 -0
  23. package/dist/editor/private-metadata-plugin.js +60 -0
  24. package/dist/editor/prosemirror-to-sutra.js +179 -74
  25. package/dist/editor/scene-heading-view.js +2 -0
  26. package/dist/editor/schema.js +69 -12
  27. package/dist/editor/sutra-to-prosemirror.d.ts +2 -0
  28. package/dist/editor/sutra-to-prosemirror.js +32 -34
  29. package/dist/extensions/ai-provider.d.ts +44 -0
  30. package/dist/extensions/ai-provider.js +27 -0
  31. package/dist/extensions/collab.d.ts +21 -0
  32. package/dist/extensions/collab.js +6 -0
  33. package/dist/extensions/command-registry.d.ts +49 -0
  34. package/dist/extensions/command-registry.js +28 -0
  35. package/dist/extensions/decorations.d.ts +48 -0
  36. package/dist/extensions/decorations.js +6 -0
  37. package/dist/extensions/export-registry.d.ts +59 -0
  38. package/dist/extensions/export-registry.js +10 -0
  39. package/dist/extensions/panel-registry.d.ts +30 -0
  40. package/dist/extensions/panel-registry.js +16 -0
  41. package/dist/extensions/registries.d.ts +15 -0
  42. package/dist/extensions/registries.js +12 -0
  43. package/dist/extensions/registry-store.d.ts +26 -0
  44. package/dist/extensions/registry-store.js +35 -0
  45. package/dist/extensions/session.d.ts +29 -0
  46. package/dist/extensions/session.js +20 -0
  47. package/dist/extensions/speech-provider.d.ts +35 -0
  48. package/dist/extensions/speech-provider.js +17 -0
  49. package/dist/file/builtin-exporters.d.ts +9 -0
  50. package/dist/file/builtin-exporters.js +72 -0
  51. package/dist/file/docx-exporter.js +7 -4
  52. package/dist/file/docx-importer.d.ts +9 -0
  53. package/dist/file/docx-importer.js +47 -19
  54. package/dist/file/scene-number.d.ts +5 -0
  55. package/dist/file/scene-number.js +8 -0
  56. package/dist/file/workflow-reports.js +13 -7
  57. package/dist/findreplace/FindReplace.js +5 -2
  58. package/dist/import/AIImportDialog.d.ts +11 -0
  59. package/dist/import/AIImportDialog.js +112 -0
  60. package/dist/import/ai-import.d.ts +51 -0
  61. package/dist/import/ai-import.js +200 -0
  62. package/dist/import/apply-import.d.ts +7 -0
  63. package/dist/import/apply-import.js +35 -0
  64. package/dist/import/extract.d.ts +17 -0
  65. package/dist/import/extract.js +18 -0
  66. package/dist/index.d.ts +17 -2
  67. package/dist/index.js +7 -0
  68. package/dist/locales/bn.json +8 -0
  69. package/dist/locales/en.json +8 -0
  70. package/dist/locales/gu.json +8 -0
  71. package/dist/locales/hi.json +8 -0
  72. package/dist/locales/kn.json +8 -0
  73. package/dist/locales/ml.json +8 -0
  74. package/dist/locales/mr.json +8 -0
  75. package/dist/locales/or.json +8 -0
  76. package/dist/locales/pa.json +8 -0
  77. package/dist/locales/si.json +8 -0
  78. package/dist/locales/ta.json +8 -0
  79. package/dist/locales/te.json +8 -0
  80. package/dist/navigator/SceneNavigator.js +125 -131
  81. package/dist/navigator/StatisticsDialog.js +5 -1
  82. package/dist/navigator/scene-edits.d.ts +18 -0
  83. package/dist/navigator/scene-edits.js +143 -0
  84. package/dist/navigator/scene-list.js +4 -4
  85. package/dist/navigator/scene-numbering.d.ts +37 -0
  86. package/dist/navigator/scene-numbering.js +88 -0
  87. package/dist/settings/SettingsDialog.js +8 -120
  88. package/dist/shell/AppBar.js +21 -2
  89. package/dist/shell/AppShell.js +18 -4
  90. package/dist/shell/ElementToolbar.d.ts +5 -0
  91. package/dist/shell/ElementToolbar.js +35 -25
  92. package/dist/shell/LanguageStatus.js +6 -0
  93. package/dist/shell/StatusBar.js +11 -6
  94. package/dist/shell/builtin-commands.d.ts +10 -0
  95. package/dist/shell/builtin-commands.js +43 -0
  96. package/dist/shell/icons.d.ts +6 -0
  97. package/dist/shell/icons.js +6 -0
  98. package/dist/shell/use-commands.d.ts +7 -0
  99. package/dist/shell/use-commands.js +24 -0
  100. package/dist/shell/use-panel-context.d.ts +3 -0
  101. package/dist/shell/use-panel-context.js +39 -0
  102. package/dist/source/SourceView.js +18 -1
  103. package/dist/styles/screenplay.css +113 -1
  104. package/dist/titlepage/ExportDialog.d.ts +5 -0
  105. package/dist/titlepage/ExportDialog.js +59 -96
  106. package/package.json +32 -5
  107. package/dist/ai/OnboardingDialog.d.ts +0 -6
  108. package/dist/ai/OnboardingDialog.js +0 -115
  109. package/dist/ai/ai-client.d.ts +0 -71
  110. package/dist/ai/ai-client.js +0 -494
  111. package/dist/ai/generated-providers.json +0 -3696
  112. package/dist/ai/voice-service.d.ts +0 -15
  113. package/dist/ai/voice-service.js +0 -81
  114. package/dist/editor/character-view.d.ts +0 -35
  115. package/dist/editor/character-view.js +0 -112
  116. package/dist/editor/transliterate-input.d.ts +0 -10
  117. package/dist/editor/transliterate-input.js +0 -88
  118. package/dist/extensions/types.d.ts +0 -73
  119. package/dist/extensions/types.js +0 -12
  120. package/scripts/copy-assets.mjs +0 -16
  121. package/scripts/generate-providers.mjs +0 -127
  122. package/src/ai/OnboardingDialog.tsx +0 -390
  123. package/src/ai/VoiceConfirmDialog.tsx +0 -137
  124. package/src/ai/VoiceToolbar.tsx +0 -321
  125. package/src/ai/ai-client.ts +0 -596
  126. package/src/ai/commands.ts +0 -164
  127. package/src/ai/generated-providers.json +0 -3696
  128. package/src/ai/prompts.ts +0 -62
  129. package/src/ai/voice-service.ts +0 -96
  130. package/src/autocomplete/AutocompletePlugin.ts +0 -21
  131. package/src/autocomplete/suggestions.ts +0 -147
  132. package/src/context/DocumentContext.tsx +0 -648
  133. package/src/desktop-api.d.ts +0 -40
  134. package/src/editor/EditorView.tsx +0 -82
  135. package/src/editor/SelectionMenu.tsx +0 -152
  136. package/src/editor/attr-pencil-plugin.ts +0 -172
  137. package/src/editor/character-decoration-plugin.ts +0 -104
  138. package/src/editor/editor-bus.ts +0 -51
  139. package/src/editor/find-highlight-plugin.ts +0 -68
  140. package/src/editor/frontmatter-field-view.ts +0 -52
  141. package/src/editor/frontmatter-field.ts +0 -115
  142. package/src/editor/grapheme-cursor.ts +0 -99
  143. package/src/editor/ime-plugin.ts +0 -85
  144. package/src/editor/insert-helper.ts +0 -43
  145. package/src/editor/keymap.ts +0 -124
  146. package/src/editor/language-class-plugin.ts +0 -91
  147. package/src/editor/plugins.ts +0 -26
  148. package/src/editor/prosemirror-to-sutra.ts +0 -192
  149. package/src/editor/scene-heading-view.ts +0 -147
  150. package/src/editor/schema.ts +0 -156
  151. package/src/editor/sutra-to-prosemirror.ts +0 -156
  152. package/src/extensions/storage-adapter.ts +0 -65
  153. package/src/extensions/types.ts +0 -60
  154. package/src/file/docx-exporter.ts +0 -585
  155. package/src/file/docx-importer.ts +0 -313
  156. package/src/file/print-paginator.ts +0 -301
  157. package/src/file/templates.ts +0 -73
  158. package/src/file/workflow-reports.ts +0 -820
  159. package/src/findreplace/FindReplace.tsx +0 -242
  160. package/src/findreplace/find-engine.ts +0 -97
  161. package/src/findreplace/scroll-to-match.ts +0 -89
  162. package/src/fonts/tofu-detect.ts +0 -53
  163. package/src/i18n/LanguageContext.tsx +0 -207
  164. package/src/i18n/language-detect.ts +0 -73
  165. package/src/i18n/useTranslation.tsx +0 -74
  166. package/src/index.ts +0 -27
  167. package/src/locales/bn.json +0 -143
  168. package/src/locales/en.json +0 -143
  169. package/src/locales/gu.json +0 -143
  170. package/src/locales/hi.json +0 -143
  171. package/src/locales/kn.json +0 -143
  172. package/src/locales/ml.json +0 -143
  173. package/src/locales/mr.json +0 -143
  174. package/src/locales/or.json +0 -143
  175. package/src/locales/pa.json +0 -143
  176. package/src/locales/si.json +0 -143
  177. package/src/locales/ta.json +0 -143
  178. package/src/locales/te.json +0 -143
  179. package/src/navigator/SceneNavigator.tsx +0 -844
  180. package/src/navigator/StatisticsDialog.tsx +0 -249
  181. package/src/navigator/scene-list.ts +0 -84
  182. package/src/settings/SettingsDialog.tsx +0 -303
  183. package/src/shell/AppBar.tsx +0 -354
  184. package/src/shell/AppShell.tsx +0 -71
  185. package/src/shell/ConfirmDialog.tsx +0 -71
  186. package/src/shell/ElementToolbar.tsx +0 -175
  187. package/src/shell/LanguageStatus.tsx +0 -101
  188. package/src/shell/StatusBar.tsx +0 -135
  189. package/src/shell/SutrataLogo.tsx +0 -89
  190. package/src/shell/Toast.tsx +0 -46
  191. package/src/shell/ToolbarButton.tsx +0 -30
  192. package/src/shell/VersionHistory.tsx +0 -212
  193. package/src/shell/Watermark.tsx +0 -9
  194. package/src/shell/fonts/fonts.css +0 -1133
  195. package/src/shell/icons.tsx +0 -335
  196. package/src/shell/toolbar-actions.ts +0 -59
  197. package/src/source/SourceView.tsx +0 -111
  198. package/src/source/find-highlight-source.ts +0 -57
  199. package/src/source/sutra-lang.ts +0 -121
  200. package/src/spellcheck/dictionaries.ts +0 -21
  201. package/src/spellcheck/hunspell.ts +0 -110
  202. package/src/spellcheck/spellcheck-plugin.ts +0 -83
  203. package/src/styles/StyleDialog.tsx +0 -148
  204. package/src/styles/builtin-styles.ts +0 -139
  205. package/src/styles/css-adapter.ts +0 -76
  206. package/src/styles/docx-adapter.ts +0 -109
  207. package/src/styles/embed-fonts.ts +0 -81
  208. package/src/styles/pagination.ts +0 -27
  209. package/src/styles/print-adapter.ts +0 -288
  210. package/src/styles/registry.ts +0 -20
  211. package/src/styles/screenplay.css +0 -3006
  212. package/src/styles/script-ranges.ts +0 -17
  213. package/src/styles/types.ts +0 -104
  214. package/src/styles/validate.ts +0 -114
  215. package/src/templates/templates.ts +0 -194
  216. package/src/titlepage/ExportDialog.tsx +0 -494
  217. package/src/titlepage/TitlePageDialog.tsx +0 -312
  218. package/src/titlepage/frontmatter-form.ts +0 -280
  219. package/src/titlepage/languages.ts +0 -37
  220. package/src/types.ts +0 -7
  221. package/tests/AppBar.test.tsx +0 -35
  222. package/tests/AppShell.test.tsx +0 -22
  223. package/tests/ElementToolbar.test.tsx +0 -36
  224. package/tests/LanguageStatus.test.tsx +0 -129
  225. package/tests/SelectionMenu.test.tsx +0 -71
  226. package/tests/ToolbarButton.test.tsx +0 -24
  227. package/tests/ai-onboarding.test.ts +0 -102
  228. package/tests/ai-status.test.tsx +0 -72
  229. package/tests/ai-structured.test.ts +0 -94
  230. package/tests/autocomplete/AutocompletePlugin.test.ts +0 -20
  231. package/tests/autocomplete/suggestions.test.ts +0 -106
  232. package/tests/document-context.test.tsx +0 -565
  233. package/tests/docx-importer.test.ts +0 -160
  234. package/tests/editor/character-decoration-plugin.test.ts +0 -101
  235. package/tests/editor/character-extension.test.ts +0 -44
  236. package/tests/editor/grapheme-cursor.test.ts +0 -118
  237. package/tests/editor/ime-plugin.test.ts +0 -72
  238. package/tests/editor/inline-note-bridge.test.ts +0 -32
  239. package/tests/editor/language-class-plugin.test.ts +0 -265
  240. package/tests/editor/title-page.test.ts +0 -124
  241. package/tests/embed-fonts.test.ts +0 -17
  242. package/tests/exporters.test.ts +0 -365
  243. package/tests/findreplace/FindReplace.test.tsx +0 -391
  244. package/tests/findreplace/find-engine.test.ts +0 -86
  245. package/tests/findreplace/scroll-to-match.test.ts +0 -50
  246. package/tests/fonts/tofu-detect.test.ts +0 -122
  247. package/tests/i18n/LanguageContext.test.tsx +0 -146
  248. package/tests/i18n/language-detect.test.ts +0 -54
  249. package/tests/i18n/locale-parity.test.ts +0 -25
  250. package/tests/i18n/useTranslation.test.tsx +0 -45
  251. package/tests/keymap.test.ts +0 -18
  252. package/tests/navigator/SceneNavigator.test.tsx +0 -177
  253. package/tests/navigator/StatisticsDialog.test.tsx +0 -63
  254. package/tests/navigator/scene-list.test.ts +0 -71
  255. package/tests/print-paginator.test.ts +0 -82
  256. package/tests/shell/VersionHistory.test.tsx +0 -129
  257. package/tests/spellcheck/hunspell.test.ts +0 -82
  258. package/tests/spellcheck/spellcheck-plugin.test.ts +0 -56
  259. package/tests/styles.test.ts +0 -315
  260. package/tests/templates/templates.test.ts +0 -83
  261. package/tests/titlepage/TitlePageDialog.test.tsx +0 -90
  262. package/tests/titlepage/apply-title-page.test.ts +0 -61
  263. package/tests/titlepage/frontmatter-form.test.ts +0 -115
  264. package/tests/toolbar-actions.test.ts +0 -93
  265. package/tests/voice-commands.test.ts +0 -58
  266. package/tsconfig.json +0 -17
  267. package/vitest.config.ts +0 -11
  268. package/vitest.setup.ts +0 -65
@@ -1,101 +0,0 @@
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
- }
@@ -1,135 +0,0 @@
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
- }
@@ -1,89 +0,0 @@
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
- }
@@ -1,46 +0,0 @@
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
- }
@@ -1,30 +0,0 @@
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
- }
@@ -1,212 +0,0 @@
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
- }
@@ -1,9 +0,0 @@
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
- }