@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,90 @@
1
+ import { useEffect } from 'react'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { DocumentProvider, useDocument } from '../../src/context/DocumentContext'
4
+ import { TranslationProvider } from '../../src/i18n/useTranslation'
5
+ import { TitlePageDialog } from '../../src/titlepage/TitlePageDialog'
6
+
7
+ const SRC = `---
8
+ title: The Last Train
9
+ date: June 2026
10
+ lang: hi
11
+ ---
12
+
13
+ ## INT. STATION - NIGHT
14
+ `
15
+
16
+ let latestText = ''
17
+
18
+ function Harness({ onClose }: { onClose: () => void }) {
19
+ const { text, setText, setMode } = useDocument()
20
+ useEffect(() => { setMode('source'); setText(SRC) }, []) // eslint-disable-line react-hooks/exhaustive-deps
21
+ latestText = text
22
+ return text === SRC || text.includes('title:') ? <TitlePageDialog onClose={onClose} /> : null
23
+ }
24
+
25
+ function renderDialog() {
26
+ const onClose = vi.fn()
27
+ render(
28
+ <TranslationProvider>
29
+ <DocumentProvider>
30
+ <Harness onClose={onClose} />
31
+ </DocumentProvider>
32
+ </TranslationProvider>,
33
+ )
34
+ return onClose
35
+ }
36
+
37
+ describe('TitlePageDialog', () => {
38
+ it('shows a tab per section and opens on Title & Story, pre-filled from frontmatter', () => {
39
+ renderDialog()
40
+ for (const name of ['Title & Story', 'Credits', 'Draft', 'Contact & Rights', 'Document Settings', 'Other Fields']) {
41
+ expect(screen.getByRole('tab', { name })).toBeInTheDocument()
42
+ }
43
+ expect(screen.getByRole('tab', { name: 'Title & Story' })).toHaveAttribute('aria-selected', 'true')
44
+ expect(screen.getByLabelText('Title')).toHaveValue('The Last Train')
45
+ })
46
+
47
+ it('switches panels on tab click, hiding fields from other tabs', () => {
48
+ // The inactive panel's fields stay in the DOM (form state must survive a tab
49
+ // switch), but the panel is hidden via the `hidden` attribute — jest-dom's
50
+ // toBeVisible() honors that (and ancestor `hidden`), so it's used instead of a
51
+ // presence check.
52
+ renderDialog()
53
+ expect(screen.getByLabelText('Primary language')).not.toBeVisible()
54
+ fireEvent.click(screen.getByRole('tab', { name: 'Document Settings' }))
55
+ expect(screen.getByRole('tab', { name: 'Document Settings' })).toHaveAttribute('aria-selected', 'true')
56
+ expect(screen.getByLabelText('Primary language')).toBeVisible()
57
+ expect(screen.getByLabelText('Primary language')).toHaveValue('hi')
58
+ expect(screen.getByLabelText('Title')).not.toBeVisible()
59
+ })
60
+
61
+ it('keeps a free-text date as text and offers the date picker', () => {
62
+ renderDialog()
63
+ fireEvent.click(screen.getByRole('tab', { name: 'Draft' }))
64
+ const date = screen.getByLabelText('Date')
65
+ expect(date).toHaveValue('June 2026')
66
+ expect(date).not.toHaveAttribute('type', 'date')
67
+ fireEvent.click(screen.getByRole('button', { name: 'Pick a date' }))
68
+ expect(screen.getByLabelText('Date')).toHaveAttribute('type', 'date')
69
+ })
70
+
71
+ it('offers style and language dropdowns and writes the choices to the frontmatter', () => {
72
+ const onClose = renderDialog()
73
+ fireEvent.click(screen.getByRole('tab', { name: 'Document Settings' }))
74
+ fireEvent.change(screen.getByLabelText('Screenplay style'), { target: { value: 'traditional' } })
75
+ fireEvent.change(screen.getByLabelText('Other languages'), { target: { value: 'en' } })
76
+ fireEvent.change(screen.getByLabelText('Page size'), { target: { value: 'A4' } })
77
+ fireEvent.click(screen.getByRole('button', { name: 'Save' }))
78
+ expect(onClose).toHaveBeenCalled()
79
+ expect(latestText).toContain('style: traditional')
80
+ expect(latestText).toContain('lang-secondary: [en]')
81
+ expect(latestText).toContain('page: A4')
82
+ expect(latestText).toContain('date: June 2026')
83
+ })
84
+
85
+ it('does not change the text when saved without edits', () => {
86
+ renderDialog()
87
+ fireEvent.click(screen.getByRole('button', { name: 'Save' }))
88
+ expect(latestText).toBe(SRC)
89
+ })
90
+ })
@@ -0,0 +1,61 @@
1
+ import { EditorState } from 'prosemirror-state'
2
+ import { EditorView } from 'prosemirror-view'
3
+ import { history, undo } from 'prosemirror-history'
4
+ import { parse } from '@sutrata/parser'
5
+ import { schema } from '../../src/editor/schema'
6
+ import { sutraToProsemirror } from '../../src/editor/sutra-to-prosemirror'
7
+ import { prosemirrorToSutra } from '../../src/editor/prosemirror-to-sutra'
8
+ import { applyTitlePageText } from '../../src/editor/frontmatter-field'
9
+ import { readTitlePage, writeTitlePage } from '../../src/titlepage/frontmatter-form'
10
+
11
+ const SRC = `---
12
+ title: The Last Train
13
+ author: Meera
14
+ ---
15
+
16
+ ## INT. STATION - NIGHT
17
+
18
+ Action line.
19
+ `
20
+
21
+ function mount(text: string) {
22
+ const state = EditorState.create({ doc: sutraToProsemirror(parse(text)), schema, plugins: [history()] })
23
+ return new EditorView(document.createElement('div'), { state })
24
+ }
25
+
26
+ describe('applyTitlePageText (formatted mode)', () => {
27
+ it('swaps the title page in one undoable transaction and leaves the body alone', () => {
28
+ const view = mount(SRC)
29
+ const f = readTitlePage(parse(SRC).frontmatter!.data)
30
+ const next = writeTitlePage(SRC, f, { ...f, author: 'Vikram', style: 'traditional' })
31
+ const setText = vi.fn()
32
+
33
+ applyTitlePageText(view, next, setText)
34
+
35
+ expect(setText).not.toHaveBeenCalled()
36
+ const out = prosemirrorToSutra(view.state.doc)
37
+ expect(out).toContain('author: Vikram')
38
+ expect(out).toContain('style: traditional')
39
+ expect(out).toContain('Action line.')
40
+
41
+ undo(view.state, view.dispatch)
42
+ expect(prosemirrorToSutra(view.state.doc)).toContain('author: Meera')
43
+ view.destroy()
44
+ })
45
+
46
+ it('inserts a title page when the document had none', () => {
47
+ const body = '## INT. ROOM - DAY\n\nAction.\n'
48
+ const view = mount(body)
49
+ const f = readTitlePage(undefined)
50
+ applyTitlePageText(view, writeTitlePage(body, f, { ...f, title: 'New Film' }), vi.fn())
51
+ expect(view.state.doc.firstChild!.type.name).toBe('title_page')
52
+ expect(prosemirrorToSutra(view.state.doc)).toContain('title: New Film')
53
+ view.destroy()
54
+ })
55
+
56
+ it('falls back to setting the text when no editor view is mounted', () => {
57
+ const setText = vi.fn()
58
+ applyTitlePageText(null, 'X', setText)
59
+ expect(setText).toHaveBeenCalledWith('X')
60
+ })
61
+ })
@@ -0,0 +1,115 @@
1
+ import { parse } from '@sutrata/parser'
2
+ import { readTitlePage, writeTitlePage, splitList } from '../../src/titlepage/frontmatter-form'
3
+
4
+ const SRC = `---
5
+ format: sutra/1.0
6
+ title:
7
+ hi: आख़िरी ट्रेन
8
+ en: The Last Train
9
+ author: रंगी पराता
10
+ date: June 2026
11
+ contact: |
12
+ rangi@example.com
13
+ +91 98xxx xxxxx
14
+ lang: hi
15
+ lang-secondary: [en]
16
+ producer: Big Films
17
+ characters:
18
+ JOHN: Actor One
19
+ ---
20
+
21
+ ## INT. STATION - NIGHT
22
+ `
23
+
24
+ const read = (text: string) => readTitlePage(parse(text).frontmatter?.data as Record<string, unknown>)
25
+
26
+ describe('readTitlePage', () => {
27
+ it('reads the multilingual title with the primary language first', () => {
28
+ const f = read(SRC)
29
+ expect(f.title).toBe('आख़िरी ट्रेन')
30
+ expect(f.altTitles).toEqual([{ lang: 'en', text: 'The Last Train' }])
31
+ expect(f.lang).toBe('hi')
32
+ expect(f.langSecondary).toEqual(['en'])
33
+ expect(f.contact).toBe('rangi@example.com\n+91 98xxx xxxxx')
34
+ })
35
+
36
+ it('lists simple unknown keys as custom rows, hides format and nested maps', () => {
37
+ const f = read(SRC)
38
+ expect(f.custom).toEqual([{ key: 'producer', value: 'Big Films' }])
39
+ })
40
+
41
+ it('splits list values in every shape', () => {
42
+ expect(splitList('[en, sa]')).toEqual(['en', 'sa'])
43
+ expect(splitList('en')).toEqual(['en'])
44
+ expect(splitList(['ta'])).toEqual(['ta'])
45
+ })
46
+ })
47
+
48
+ describe('writeTitlePage', () => {
49
+ it('leaves the text byte-identical when nothing changed', () => {
50
+ const f = read(SRC)
51
+ expect(writeTitlePage(SRC, f, { ...f })).toBe(SRC)
52
+ })
53
+
54
+ it('rewrites only the changed key and keeps everything else in place', () => {
55
+ const f = read(SRC)
56
+ const out = writeTitlePage(SRC, f, { ...f, author: 'Meera Nair' })
57
+ expect(out).toBe(SRC.replace('author: रंगी पराता', 'author: Meera Nair'))
58
+ })
59
+
60
+ it('appends new keys in canonical order before the closing fence', () => {
61
+ const f = read(SRC)
62
+ const out = writeTitlePage(SRC, f, { ...f, style: 'traditional', page: 'A4', draft: 'Second Draft' })
63
+ const fm = out.split('---')[1]!
64
+ expect(fm.trimEnd().split('\n').slice(-3)).toEqual(['draft: Second Draft', 'page: A4', 'style: traditional'])
65
+ expect(parse(out).frontmatter!.data['style']).toBe('traditional')
66
+ })
67
+
68
+ it('removes keys that were cleared', () => {
69
+ const f = read(SRC)
70
+ const out = writeTitlePage(SRC, f, { ...f, contact: '' })
71
+ expect(out).not.toContain('contact')
72
+ expect(out).not.toContain('rangi@example.com')
73
+ expect(out).toContain('author: रंगी पराता')
74
+ })
75
+
76
+ it('writes the multilingual title as a nested map and a plain title as a scalar', () => {
77
+ const f = read(SRC)
78
+ const both = writeTitlePage(SRC, f, { ...f, altTitles: [...f.altTitles, { lang: 'ta', text: 'கடைசி ரயில்' }] })
79
+ expect(parse(both).frontmatter!.data['title']).toEqual({ hi: 'आख़िरी ट्रेन', en: 'The Last Train', ta: 'கடைசி ரயில்' })
80
+ const single = writeTitlePage(SRC, f, { ...f, altTitles: [] })
81
+ expect(parse(single).frontmatter!.data['title']).toBe('आख़िरी ट्रेन')
82
+ })
83
+
84
+ it('writes secondary languages as a flow list and multi-line contact as a block', () => {
85
+ const f = read(SRC)
86
+ const out = writeTitlePage(SRC, f, { ...f, langSecondary: ['en', 'sa'], contact: 'a@b.c\nPhone' })
87
+ expect(out).toContain('lang-secondary: [en, sa]')
88
+ expect(out).toContain('contact: |\n a@b.c\n Phone')
89
+ expect(parse(out).frontmatter!.data['contact']).toBe('a@b.c\nPhone')
90
+ })
91
+
92
+ it('edits, removes and adds custom fields, never touching nested unknown maps', () => {
93
+ const f = read(SRC)
94
+ const out = writeTitlePage(SRC, f, { ...f, custom: [{ key: 'studio', value: 'Lotus' }] })
95
+ expect(out).not.toContain('producer:')
96
+ expect(out).toContain('studio: Lotus')
97
+ expect(out).toContain('characters:\n JOHN: Actor One')
98
+ expect(out).toContain('format: sutra/1.0')
99
+ })
100
+
101
+ it('creates a frontmatter block when the document has none', () => {
102
+ const body = '## INT. ROOM - DAY\n'
103
+ const f = read(body)
104
+ const out = writeTitlePage(body, f, { ...f, title: 'New Film', date: '2026-09-19' })
105
+ expect(out).toBe('---\ntitle: New Film\ndate: 2026-09-19\n---\n\n## INT. ROOM - DAY\n')
106
+ })
107
+
108
+ it('preserves CRLF line endings', () => {
109
+ const crlf = SRC.replace(/\n/g, '\r\n')
110
+ const f = read(crlf)
111
+ const out = writeTitlePage(crlf, f, { ...f, author: 'X' })
112
+ expect(out).toContain('author: X\r\n')
113
+ expect(out.replace(/\r\n/g, '')).not.toContain('\n')
114
+ })
115
+ })
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { EditorState, TextSelection } from 'prosemirror-state'
3
+ import { schema } from '../src/editor/schema'
4
+ import { makeSetBlock, makeSetOrToggleNote, makeToggleMark } from '../src/shell/toolbar-actions'
5
+
6
+ function stateWith(typeName: string, text: string) {
7
+ const node = schema.nodes[typeName]!.create(null, text ? schema.text(text) : undefined)
8
+ const doc = schema.nodes['doc']!.create(null, node)
9
+ let state = EditorState.create({ schema, doc })
10
+ state = state.apply(state.tr.setSelection(TextSelection.create(state.doc, 1)))
11
+ return state
12
+ }
13
+
14
+ describe('makeSetBlock', () => {
15
+ it('changes the current block type', () => {
16
+ const state = stateWith('action', 'hello')
17
+ let next = state
18
+ makeSetBlock('character')(state, (tr) => { next = state.apply(tr) })
19
+ expect(next.doc.firstChild!.type.name).toBe('character')
20
+ })
21
+ })
22
+
23
+ describe('makeToggleMark', () => {
24
+ it('returns false on empty selection (does nothing)', () => {
25
+ const state = stateWith('action', 'hello')
26
+ const result = makeToggleMark('bold')(state, () => {})
27
+ expect(result).toBe(false)
28
+ })
29
+
30
+ it('applies the note_mark to a selected range, so an inline note can be created from the WYSIWYG toolbar', () => {
31
+ const state = stateWith('action', 'Add the rain here please')
32
+ const withSelection = state.apply(
33
+ state.tr.setSelection(TextSelection.create(state.doc, 1, 1 + 'Add the rain'.length))
34
+ )
35
+ let next = withSelection
36
+ const applied = makeToggleMark('note_mark')(withSelection, (tr) => { next = withSelection.apply(tr) })
37
+ expect(applied).toBe(true)
38
+
39
+ const markType = schema.marks['note_mark']!
40
+ let hasMark = false
41
+ next.doc.firstChild!.descendants(node => {
42
+ if (node.isText && node.marks.some(m => m.type === markType)) hasMark = true
43
+ })
44
+ expect(hasMark).toBe(true)
45
+ })
46
+ })
47
+
48
+ function noteMarkedText(doc: EditorState['doc']): string {
49
+ const markType = schema.marks['note_mark']!
50
+ let text = ''
51
+ doc.firstChild!.descendants(node => {
52
+ if (node.isText && node.marks.some(m => m.type === markType)) text += node.text
53
+ })
54
+ return text
55
+ }
56
+
57
+ describe('makeSetOrToggleNote', () => {
58
+ it('marks only the selected text as a note on an Action line, leaving the rest of the line intact', () => {
59
+ const state = stateWith('action', 'Add the rain here please')
60
+ const withSelection = state.apply(
61
+ state.tr.setSelection(TextSelection.create(state.doc, 1, 1 + 'Add the rain'.length))
62
+ )
63
+ let next = withSelection
64
+ const applied = makeSetOrToggleNote()(withSelection, (tr) => { next = withSelection.apply(tr) })
65
+
66
+ expect(applied).toBe(true)
67
+ expect(next.doc.firstChild!.type.name).toBe('action')
68
+ expect(noteMarkedText(next.doc)).toBe('Add the rain')
69
+ expect(next.doc.textContent).toBe('Add the rain here please')
70
+ })
71
+
72
+ it('marks the whole line as a note on an Action line with no selection', () => {
73
+ const state = stateWith('action', 'Add the rain here please')
74
+ let next = state
75
+ const applied = makeSetOrToggleNote()(state, (tr) => { next = state.apply(tr) })
76
+
77
+ expect(applied).toBe(true)
78
+ expect(next.doc.firstChild!.type.name).toBe('action')
79
+ expect(noteMarkedText(next.doc)).toBe('Add the rain here please')
80
+ })
81
+
82
+ it('falls back to converting the whole block to a note element on any line other than Action', () => {
83
+ const state = stateWith('dialogue', 'We should go home now.')
84
+ const withSelection = state.apply(
85
+ state.tr.setSelection(TextSelection.create(state.doc, 1, 1 + 'We should'.length))
86
+ )
87
+ let next = withSelection
88
+ const applied = makeSetOrToggleNote()(withSelection, (tr) => { next = withSelection.apply(tr) })
89
+
90
+ expect(applied).toBe(true)
91
+ expect(next.doc.firstChild!.type.name).toBe('note')
92
+ })
93
+ })
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseVoiceCommand } from '../src/ai/commands';
3
+
4
+ describe('Voice Commands Pre-processor', () => {
5
+ it('should parse English commands correctly', () => {
6
+ const result1 = parseVoiceCommand('new scene INT. HALLWAYS - DAY', 'en-US');
7
+ expect(result1.type).toBe('scene');
8
+ expect(result1.text).toBe('INT. HALLWAYS - DAY');
9
+ expect(result1.isLiteral).toBe(false);
10
+
11
+ const result2 = parseVoiceCommand('character RAJ', 'en-US');
12
+ expect(result2.type).toBe('character');
13
+ expect(result2.text).toBe('RAJ');
14
+
15
+ const result3 = parseVoiceCommand('dialogue Hello world', 'en-US');
16
+ expect(result3.type).toBe('dialogue');
17
+ expect(result3.text).toBe('Hello world');
18
+ });
19
+
20
+ it('should parse Hindi commands correctly', () => {
21
+ const result1 = parseVoiceCommand('नया दृश्य अंतःपुर - दिन', 'hi-IN');
22
+ expect(result1.type).toBe('scene');
23
+ expect(result1.text).toBe('अंतःपुर - दिन');
24
+
25
+ const result2 = parseVoiceCommand('पात्र राहुल', 'hi-IN');
26
+ expect(result2.type).toBe('character');
27
+ expect(result2.text).toBe('राहुल');
28
+ });
29
+
30
+ it('should parse Tamil commands correctly', () => {
31
+ const result1 = parseVoiceCommand('புதிய காட்சி தெரு - பகல்', 'ta-IN');
32
+ expect(result1.type).toBe('scene');
33
+ expect(result1.text).toBe('தெரு - பகல்');
34
+
35
+ const result2 = parseVoiceCommand('வசனம் வணக்கம் உலகமே', 'ta-IN');
36
+ expect(result2.type).toBe('dialogue');
37
+ expect(result2.text).toBe('வணக்கம் உலகமே');
38
+ });
39
+
40
+ it('should handle literal bypass keyword to escape commands', () => {
41
+ const result1 = parseVoiceCommand('literal new scene heading', 'en-US');
42
+ expect(result1.type).toBe('action');
43
+ expect(result1.text).toBe('new scene heading');
44
+ expect(result1.isLiteral).toBe(true);
45
+
46
+ const result2 = parseVoiceCommand('शब्दशः नया दृश्य', 'hi-IN');
47
+ expect(result2.type).toBe('action');
48
+ expect(result2.text).toBe('नया दृश्य');
49
+ expect(result2.isLiteral).toBe(true);
50
+ });
51
+
52
+ it('should default to action type for plain speech', () => {
53
+ const result = parseVoiceCommand('The sun rises slowly over the mountains.', 'en-US');
54
+ expect(result.type).toBe('action');
55
+ expect(result.text).toBe('The sun rises slowly over the mountains.');
56
+ expect(result.isLiteral).toBe(false);
57
+ });
58
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "declaration": true,
11
+ "outDir": "./dist",
12
+ "rootDir": "./src",
13
+ "resolveJsonModule": true,
14
+ "skipLibCheck": true
15
+ },
16
+ "include": ["src"]
17
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'vitest/config'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ test: {
7
+ environment: 'jsdom',
8
+ globals: true,
9
+ setupFiles: ['./vitest.setup.ts'],
10
+ },
11
+ })
@@ -0,0 +1,65 @@
1
+ import '@testing-library/jest-dom/vitest'
2
+ import { vi } from 'vitest'
3
+
4
+ // jsdom doesn't implement URL.createObjectURL / revokeObjectURL
5
+ if (typeof URL.createObjectURL === 'undefined') {
6
+ URL.createObjectURL = vi.fn(() => 'blob:mock')
7
+ URL.revokeObjectURL = vi.fn()
8
+ }
9
+
10
+ // Mock canvas for jsdom environment - jsdom doesn't fully implement canvas API
11
+ if (typeof window !== 'undefined') {
12
+ const mockCanvas2DContext = {
13
+ fillStyle: 'black',
14
+ font: '12px monospace',
15
+ fillText: vi.fn(),
16
+ clearRect: vi.fn(),
17
+ getImageData: vi.fn((x, y, width, height) => {
18
+ // Return a mock ImageData object
19
+ const data = new Uint8ClampedArray(width * height * 4)
20
+ return {
21
+ data,
22
+ width,
23
+ height,
24
+ colorSpace: 'srgb',
25
+ }
26
+ }),
27
+ }
28
+
29
+ // Mock HTMLCanvasElement.prototype.getContext
30
+ HTMLCanvasElement.prototype.getContext = vi.fn(function (
31
+ contextType: string
32
+ ) {
33
+ if (contextType === '2d') {
34
+ return { ...mockCanvas2DContext }
35
+ }
36
+ return null
37
+ })
38
+ }
39
+
40
+ // Mock localStorage for jsdom / Node 22 compatibility
41
+ const createStorageMock = () => {
42
+ let store: Record<string, string> = {}
43
+ return {
44
+ getItem: vi.fn((key: string) => store[key] ?? null),
45
+ setItem: vi.fn((key: string, value: string) => { store[key] = String(value) }),
46
+ removeItem: vi.fn((key: string) => { delete store[key] }),
47
+ clear: vi.fn(() => { store = {} }),
48
+ get length() { return Object.keys(store).length },
49
+ key: vi.fn((i: number) => Object.keys(store)[i] ?? null),
50
+ }
51
+ }
52
+
53
+ const storageMock = createStorageMock()
54
+ if (typeof window !== 'undefined') {
55
+ Object.defineProperty(window, 'localStorage', {
56
+ value: storageMock,
57
+ writable: true,
58
+ configurable: true,
59
+ })
60
+ }
61
+ Object.defineProperty(globalThis, 'localStorage', {
62
+ value: storageMock,
63
+ writable: true,
64
+ configurable: true,
65
+ })