@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,75 @@
1
+ import { Plugin } from 'prosemirror-state';
2
+ import { Decoration, DecorationSet } from 'prosemirror-view';
3
+ import { loadDictionary } from './hunspell';
4
+ // Module-level state
5
+ let currentChecker = null;
6
+ let currentLang = 'en';
7
+ export function setSpellcheckLanguage(lang) {
8
+ if (lang === currentLang)
9
+ return;
10
+ currentLang = lang;
11
+ currentChecker = null; // force reload
12
+ loadDictionary(lang).then(checker => {
13
+ currentChecker = checker;
14
+ });
15
+ }
16
+ /**
17
+ * Tokenize text into words (splitting on whitespace and punctuation).
18
+ * Returns array of {word, start, end} with character offsets.
19
+ * Uses Unicode letter/mark categories to support Indic scripts.
20
+ */
21
+ export function tokenizeWords(text) {
22
+ const results = [];
23
+ const regex = /[\p{L}\p{M}'-]+/gu;
24
+ let match;
25
+ while ((match = regex.exec(text)) !== null) {
26
+ results.push({ word: match[0], start: match.index, end: match.index + match[0].length });
27
+ }
28
+ return results;
29
+ }
30
+ /**
31
+ * ProseMirror plugin that underlines misspelled words via Decoration API.
32
+ * Recomputes decorations on every doc change.
33
+ */
34
+ export function createSpellcheckPlugin() {
35
+ return new Plugin({
36
+ state: {
37
+ init() {
38
+ return DecorationSet.empty;
39
+ },
40
+ apply(tr, set, _oldState, newState) {
41
+ if (!tr.docChanged)
42
+ return set;
43
+ if (!currentChecker)
44
+ return DecorationSet.empty;
45
+ return computeDecorations(newState, currentChecker);
46
+ },
47
+ },
48
+ props: {
49
+ decorations(state) {
50
+ return this.getState(state);
51
+ },
52
+ },
53
+ });
54
+ }
55
+ function computeDecorations(state, checker) {
56
+ const decorations = [];
57
+ state.doc.forEach((node, offset) => {
58
+ if (!node.isBlock || !node.textContent)
59
+ return;
60
+ const text = node.textContent;
61
+ const tokens = tokenizeWords(text);
62
+ for (const token of tokens) {
63
+ if (!checker.check(token.word)) {
64
+ // Misspelled: add wavy underline decoration
65
+ const from = offset + 1 + token.start;
66
+ const to = offset + 1 + token.end;
67
+ decorations.push(Decoration.inline(from, to, {
68
+ class: 'cs-spell-error',
69
+ title: `"${token.word}" may be misspelled`,
70
+ }));
71
+ }
72
+ }
73
+ });
74
+ return DecorationSet.create(state.doc, decorations);
75
+ }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface Props {
3
+ onClose: () => void;
4
+ }
5
+ export declare function StyleDialog({ onClose }: Props): React.JSX.Element;
6
+ export {};
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { useDocument } from '../context/DocumentContext';
4
+ import { useTranslation } from '../i18n/useTranslation';
5
+ import { allStyles } from './registry';
6
+ import { isBuiltinStyleId } from './builtin-styles';
7
+ import { validateStyleJson } from './validate';
8
+ import { useStorageAdapter } from '../extensions/storage-adapter';
9
+ export function StyleDialog({ onClose }) {
10
+ const { resolvedStyle, customStyles, setDocumentStyle, reloadCustomStyles } = useDocument();
11
+ const { t } = useTranslation();
12
+ const storageAdapter = useStorageAdapter();
13
+ const dialogRef = useRef(null);
14
+ const [error, setError] = useState(null);
15
+ useEffect(() => {
16
+ dialogRef.current?.focus();
17
+ }, []);
18
+ const handleKeyDown = (e) => {
19
+ if (e.key === 'Escape')
20
+ onClose();
21
+ };
22
+ async function handleImport() {
23
+ setError(null);
24
+ const picked = await storageAdapter.openStyleJson();
25
+ if (!picked)
26
+ return;
27
+ let data;
28
+ try {
29
+ data = JSON.parse(picked.text);
30
+ }
31
+ catch {
32
+ setError(`${t('style.importInvalid')}: not valid JSON`);
33
+ return;
34
+ }
35
+ const result = validateStyleJson(data);
36
+ if (!result.ok) {
37
+ setError(`${t('style.importInvalid')}: ${result.errors.join('; ')}`);
38
+ return;
39
+ }
40
+ if (isBuiltinStyleId(result.style.id)) {
41
+ setError(t('style.importIdReserved'));
42
+ return;
43
+ }
44
+ if (customStyles.some(s => s.id === result.style.id) && !confirm(t('style.importOverwriteConfirm'))) {
45
+ return;
46
+ }
47
+ await storageAdapter.saveStyle(result.style);
48
+ await reloadCustomStyles();
49
+ }
50
+ async function handleDelete(id) {
51
+ if (!confirm(t('style.deleteConfirm')))
52
+ return;
53
+ await storageAdapter.deleteStyle(id);
54
+ await reloadCustomStyles();
55
+ }
56
+ function renderRow(style, deletable) {
57
+ const active = style.id === resolvedStyle.id;
58
+ const swatchColor = style.elements.sceneHeading.color ? `#${style.elements.sceneHeading.color}` : '#999';
59
+ return (_jsxs("div", { className: "cs-settings-row", style: { alignItems: 'flex-start', cursor: 'pointer', padding: '6px 0' }, onClick: () => setDocumentStyle(style.id), children: [_jsxs("div", { style: { display: 'flex', gap: '10px', alignItems: 'flex-start' }, children: [_jsx("span", { style: { display: 'inline-block', width: '10px', height: '10px', borderRadius: '50%', background: swatchColor, flexShrink: 0, marginTop: '4px' } }), _jsxs("div", { children: [_jsxs("div", { className: "cs-settings-label", style: { fontWeight: active ? 600 : 400 }, children: [style.name, active && _jsxs("span", { style: { color: '#1976d2', fontWeight: 600 }, children: [" \u2014 ", t('style.current')] })] }), style.description && (_jsx("div", { style: { fontSize: '11px', color: '#888', marginTop: '2px' }, children: style.description }))] })] }), deletable && (_jsx("button", { className: "cs-panel-close", "aria-label": t('style.delete'), onClick: e => { e.stopPropagation(); void handleDelete(style.id); }, children: "\u00D7" }))] }, style.id));
60
+ }
61
+ const builtins = allStyles([]).filter(s => isBuiltinStyleId(s.id));
62
+ return (_jsx("div", { className: "cs-dialog-overlay", onClick: onClose, children: _jsxs("div", { ref: dialogRef, className: "cs-dialog cs-settings-dialog", role: "dialog", "aria-modal": "true", "aria-label": t('style.title'), tabIndex: -1, onClick: e => e.stopPropagation(), onKeyDown: handleKeyDown, children: [_jsxs("div", { className: "cs-dialog-header", children: [_jsx("span", { className: "cs-dialog-title", children: t('style.title') }), _jsx("button", { className: "cs-fr-close", "aria-label": t('style.close'), onClick: onClose, children: "\u00D7" })] }), _jsxs("div", { className: "cs-settings-body", children: [error && (_jsx("div", { className: "cs-export-warnings", style: { marginTop: 0 }, children: _jsx("div", { className: "cs-export-warn-item", children: error }) })), _jsx("div", { className: "cs-settings-section-label", children: t('style.builtIn') }), builtins.map(s => renderRow(s, false)), customStyles.length > 0 && (_jsxs(_Fragment, { children: [_jsx("div", { className: "cs-settings-section-label", style: { marginTop: '8px' }, children: t('style.custom') }), customStyles.map(s => renderRow(s, true))] })), _jsx("button", { className: "cs-export-btn-cancel", style: { marginTop: '8px', alignSelf: 'flex-start' }, onClick: () => { void handleImport(); }, children: t('style.import') })] })] }) }));
63
+ }
@@ -0,0 +1,43 @@
1
+ import type { ScreenplayStyleDefinition } from './types.js';
2
+ /**
3
+ * The app's long-standing default look, extracted from the values that were
4
+ * hardcoded across screenplay.css / docx-exporter.ts / workflow-reports.ts
5
+ * before those three were unified onto this shared definition. Where the
6
+ * three disagreed (they had drifted — see the multi-style implementation
7
+ * plan), the live editor's CSS is treated as canonical for font-size/color/
8
+ * margin-%, so the editor's appearance is unchanged; DOCX/PDF absorb the
9
+ * (small, confirmed) corrections needed to match it.
10
+ */
11
+ export declare const CLASSIC_STYLE: ScreenplayStyleDefinition;
12
+ /**
13
+ * A second built-in style with a genuinely different look — monospace,
14
+ * monochrome (no `color` on any element, exercising the fallback-to-black
15
+ * path), industry-standard flush-left margins — to prove the architecture
16
+ * isn't secretly hardcoded to CLASSIC_STYLE's shape.
17
+ */
18
+ export declare const INDUSTRY_COURIER_STYLE: ScreenplayStyleDefinition;
19
+ /**
20
+ * Modeled on a real Tamil feature screenplay PDF (betterfountain/Fountain export):
21
+ * bold scene headings, underlined (not bold) character cues, fully monochrome,
22
+ * flush-left action/scene headings, A4 with 1in margins all around.
23
+ * Measurements (font sizes, indents as % of content width) were taken
24
+ * directly from the PDF's text positions; elements the reference document
25
+ * doesn't print at all (section/note/comment — Fountain strips these from
26
+ * output) are extrapolated to match its scale rather than measured.
27
+ *
28
+ * The reference PDF actually used Vijaya, a proprietary Microsoft font — not
29
+ * bundled here (unlike Noto, it can't legally ship in the repo, and depending
30
+ * on the reader's OS/Word already having it installed is a worse default than
31
+ * a font that just works). Noto Serif is the deliberate second choice: not a
32
+ * perfect visual match, but it already has a variant for every script Noto
33
+ * Sans does (so this style isn't just "Tamil-only" the way a Vijaya-based one
34
+ * would be) and ships under the OFL, so it's self-hosted for real instead of
35
+ * hoped-for. Tamil specifically renders in Noto Serif Tamil's *italic* face by
36
+ * default (`italicScripts`, forced even on bold scene headings/sections) since
37
+ * that slant reads closer to Vijaya than the upright serif does — every other
38
+ * script stays upright serif, `italicScripts` only lists 'ta'.
39
+ */
40
+ export declare const TRADITIONAL_STYLE: ScreenplayStyleDefinition;
41
+ export declare const BUILTIN_STYLES: ScreenplayStyleDefinition[];
42
+ export declare function getBuiltinStyle(id: string): ScreenplayStyleDefinition | undefined;
43
+ export declare function isBuiltinStyleId(id: string): boolean;
@@ -0,0 +1,131 @@
1
+ import { DEFAULT_STYLE_ID } from './types.js';
2
+ /**
3
+ * The app's long-standing default look, extracted from the values that were
4
+ * hardcoded across screenplay.css / docx-exporter.ts / workflow-reports.ts
5
+ * before those three were unified onto this shared definition. Where the
6
+ * three disagreed (they had drifted — see the multi-style implementation
7
+ * plan), the live editor's CSS is treated as canonical for font-size/color/
8
+ * margin-%, so the editor's appearance is unchanged; DOCX/PDF absorb the
9
+ * (small, confirmed) corrections needed to match it.
10
+ */
11
+ export const CLASSIC_STYLE = {
12
+ id: 'classic',
13
+ name: 'Classic',
14
+ description: 'The original Sutrata look — teal scene headings, rust character cues.',
15
+ fontFamily: 'Noto Sans',
16
+ page: { marginTopIn: 1, marginBottomIn: 1, marginLeftIn: 1, marginRightIn: 1 },
17
+ elements: {
18
+ sceneHeading: { fontSizePt: 13, bold: true, color: '0A5C6A', marginLeftPct: 10, marginRightPct: 8, spaceBeforePt: 13, spaceAfterPt: 7.2 },
19
+ action: { fontSizePt: 12, marginLeftPct: 10, marginRightPct: 8, spaceBeforePt: 6, spaceAfterPt: 7.2 },
20
+ character: { fontSizePt: 13, bold: true, color: '9C4221', marginLeftPct: 17, spaceBeforePt: 6.5, spaceAfterPt: 0 },
21
+ parenthetical: { fontSizePt: 13, marginLeftPct: 17, marginRightPct: 20, spaceAfterPt: 0 },
22
+ dialogue: { fontSizePt: 12, marginLeftPct: 17, marginRightPct: 17, spaceAfterPt: 7.2 },
23
+ transition: { fontSizePt: 13, align: 'right', marginRightPct: 8, spaceBeforePt: 6.5, spaceAfterPt: 7.2 },
24
+ centered: { fontSizePt: 13, align: 'center', spaceBeforePt: 6.5, spaceAfterPt: 7.2 },
25
+ lyrics: { fontSizePt: 13, italic: true, marginLeftPct: 10, marginRightPct: 8, spaceAfterPt: 3.6 },
26
+ section: { fontSizePt: 14, bold: true, color: '6F6F6F', marginLeftPct: 10, spaceBeforePt: 21, spaceAfterPt: 7.2 },
27
+ note: { fontSizePt: 10, color: '7A6000', marginLeftPct: 10, marginRightPct: 8, spaceAfterPt: 3.6 },
28
+ comment: { fontSizePt: 10, color: 'AAAAAA', italic: true, marginLeftPct: 10, spaceAfterPt: 3.6 },
29
+ title: { fontSizePt: 24, bold: true, color: '0A5C6A', align: 'center', spaceAfterPt: 14.4 },
30
+ author: { fontSizePt: 16, align: 'center', spaceAfterPt: 7.2 },
31
+ logline: { fontSizePt: 12, italic: true, color: '555555', align: 'center', spaceAfterPt: 7.2 },
32
+ },
33
+ };
34
+ /**
35
+ * A second built-in style with a genuinely different look — monospace,
36
+ * monochrome (no `color` on any element, exercising the fallback-to-black
37
+ * path), industry-standard flush-left margins — to prove the architecture
38
+ * isn't secretly hardcoded to CLASSIC_STYLE's shape.
39
+ */
40
+ export const INDUSTRY_COURIER_STYLE = {
41
+ id: 'industry-courier',
42
+ name: 'Industry Courier',
43
+ description: 'Monospace, monochrome, flush-left margins — the classic spec-script look.',
44
+ fontFamily: 'Courier Prime',
45
+ page: { marginTopIn: 1, marginBottomIn: 1, marginLeftIn: 1.5, marginRightIn: 1 },
46
+ elements: {
47
+ sceneHeading: { fontSizePt: 12, bold: true, spaceBeforePt: 24, spaceAfterPt: 12 },
48
+ action: { fontSizePt: 12, spaceBeforePt: 12, spaceAfterPt: 12 },
49
+ character: { fontSizePt: 12, bold: true, marginLeftPct: 30, spaceBeforePt: 12, spaceAfterPt: 0 },
50
+ parenthetical: { fontSizePt: 12, marginLeftPct: 26, marginRightPct: 15, spaceAfterPt: 0 },
51
+ dialogue: { fontSizePt: 12, marginLeftPct: 20, marginRightPct: 15, spaceAfterPt: 12 },
52
+ transition: { fontSizePt: 12, align: 'right', spaceBeforePt: 12, spaceAfterPt: 12 },
53
+ centered: { fontSizePt: 12, align: 'center', spaceBeforePt: 12, spaceAfterPt: 12 },
54
+ lyrics: { fontSizePt: 12, italic: true, spaceAfterPt: 6 },
55
+ section: { fontSizePt: 12, bold: true, spaceBeforePt: 24, spaceAfterPt: 12 },
56
+ note: { fontSizePt: 10, spaceAfterPt: 6 },
57
+ comment: { fontSizePt: 10, italic: true, spaceAfterPt: 6 },
58
+ title: { fontSizePt: 24, bold: true, align: 'center', spaceAfterPt: 14.4 },
59
+ author: { fontSizePt: 16, align: 'center', spaceAfterPt: 7.2 },
60
+ logline: { fontSizePt: 12, italic: true, align: 'center', spaceAfterPt: 7.2 },
61
+ },
62
+ };
63
+ /** Every Indic script Sutrata already self-hosts Noto Sans for, pointed at its Noto
64
+ * Serif equivalent instead (all bundled in shell/fonts/fonts.css the same way as Noto
65
+ * Sans — see fonts-serif-block.css generation notes in that file's history). Reused by
66
+ * TRADITIONAL_STYLE's `complexScriptFontOverrides`: Noto Serif was chosen over the
67
+ * originally-considered Vijaya specifically because it already covers every script
68
+ * Noto Sans does and carries an OFL license, so — unlike Vijaya — it can actually ship
69
+ * in the repo instead of depending on the reader's OS/Word already having it installed. */
70
+ const NOTO_SERIF_COMPLEX_SCRIPT_FONTS = {
71
+ hi: 'Noto Serif Devanagari', ta: 'Noto Serif Tamil', te: 'Noto Serif Telugu',
72
+ kn: 'Noto Serif Kannada', ml: 'Noto Serif Malayalam', bn: 'Noto Serif Bengali',
73
+ gu: 'Noto Serif Gujarati', pa: 'Noto Serif Gurmukhi', or: 'Noto Serif Oriya',
74
+ si: 'Noto Serif Sinhala',
75
+ };
76
+ /**
77
+ * Modeled on a real Tamil feature screenplay PDF (betterfountain/Fountain export):
78
+ * bold scene headings, underlined (not bold) character cues, fully monochrome,
79
+ * flush-left action/scene headings, A4 with 1in margins all around.
80
+ * Measurements (font sizes, indents as % of content width) were taken
81
+ * directly from the PDF's text positions; elements the reference document
82
+ * doesn't print at all (section/note/comment — Fountain strips these from
83
+ * output) are extrapolated to match its scale rather than measured.
84
+ *
85
+ * The reference PDF actually used Vijaya, a proprietary Microsoft font — not
86
+ * bundled here (unlike Noto, it can't legally ship in the repo, and depending
87
+ * on the reader's OS/Word already having it installed is a worse default than
88
+ * a font that just works). Noto Serif is the deliberate second choice: not a
89
+ * perfect visual match, but it already has a variant for every script Noto
90
+ * Sans does (so this style isn't just "Tamil-only" the way a Vijaya-based one
91
+ * would be) and ships under the OFL, so it's self-hosted for real instead of
92
+ * hoped-for. Tamil specifically renders in Noto Serif Tamil's *italic* face by
93
+ * default (`italicScripts`, forced even on bold scene headings/sections) since
94
+ * that slant reads closer to Vijaya than the upright serif does — every other
95
+ * script stays upright serif, `italicScripts` only lists 'ta'.
96
+ */
97
+ export const TRADITIONAL_STYLE = {
98
+ id: 'traditional',
99
+ name: 'Traditional',
100
+ description: 'Bold serif scene headings, underlined cues, italic Tamil — modeled on a real Tamil shooting script.',
101
+ fontFamily: 'Noto Serif',
102
+ complexScriptFontOverrides: NOTO_SERIF_COMPLEX_SCRIPT_FONTS,
103
+ italicScripts: ['ta'],
104
+ // The reference PDF's cover: title / "by <author>" / logline flush top-left,
105
+ // date and contact down in the bottom-right corner.
106
+ coverLayout: 'corners',
107
+ page: { marginTopIn: 1, marginBottomIn: 1, marginLeftIn: 1, marginRightIn: 1 },
108
+ elements: {
109
+ sceneHeading: { fontSizePt: 12, bold: true, spaceBeforePt: 17, spaceAfterPt: 9 },
110
+ action: { fontSizePt: 10, spaceBeforePt: 8, spaceAfterPt: 9 },
111
+ character: { fontSizePt: 10, underline: true, marginLeftPct: 32, spaceBeforePt: 8, spaceAfterPt: 0 },
112
+ parenthetical: { fontSizePt: 10, marginLeftPct: 32, marginRightPct: 20, spaceAfterPt: 0 },
113
+ dialogue: { fontSizePt: 10, marginLeftPct: 24, marginRightPct: 17, spaceAfterPt: 9 },
114
+ transition: { fontSizePt: 10, align: 'right', spaceBeforePt: 9, spaceAfterPt: 9 },
115
+ centered: { fontSizePt: 10, align: 'center', italic: true, spaceBeforePt: 9, spaceAfterPt: 9 },
116
+ lyrics: { fontSizePt: 10, italic: true, marginLeftPct: 6, spaceAfterPt: 5 },
117
+ section: { fontSizePt: 12, bold: true, spaceBeforePt: 27, spaceAfterPt: 9 },
118
+ note: { fontSizePt: 10, color: '666666', spaceAfterPt: 5 },
119
+ comment: { fontSizePt: 13, italic: true, spaceAfterPt: 5 },
120
+ title: { fontSizePt: 32, bold: true, align: 'left', spaceAfterPt: 16 },
121
+ author: { fontSizePt: 16, align: 'left', spaceAfterPt: 8 },
122
+ logline: { fontSizePt: 10, align: 'left', spaceAfterPt: 8 },
123
+ },
124
+ };
125
+ export const BUILTIN_STYLES = [CLASSIC_STYLE, INDUSTRY_COURIER_STYLE, TRADITIONAL_STYLE];
126
+ export function getBuiltinStyle(id) {
127
+ return BUILTIN_STYLES.find(s => s.id === id);
128
+ }
129
+ export function isBuiltinStyleId(id) {
130
+ return id === DEFAULT_STYLE_ID || BUILTIN_STYLES.some(s => s.id === id);
131
+ }
@@ -0,0 +1,32 @@
1
+ import type { ScreenplayStyleDefinition } from './types.js';
2
+ /**
3
+ * Converts a style into the CSS custom properties screenplay.css reads via
4
+ * `var(--cs-<element>-<prop>, <classic-fallback>)`. Applied to `.cs-editor-mount`
5
+ * so the live editor picks up whichever style the current document (or an
6
+ * explicit override) resolves to — works identically for built-in and
7
+ * user-imported styles since both are plain `ScreenplayStyleDefinition` objects
8
+ * by the time they reach here.
9
+ *
10
+ * Deliberately does NOT emit anything for `spaceAfterPt` — the live editor only
11
+ * ever expresses "space before" via margin-top; adjacent elements form their gap
12
+ * from the *following* element's margin-top, not a margin-bottom on the
13
+ * preceding one (see types.ts). `spaceAfterPt` is DOCX/print-only.
14
+ *
15
+ * A `null` entry means "no override" and must be cleared with `removeProperty`,
16
+ * never `setProperty(k, 'inherit')`: for a CUSTOM property, the literal value
17
+ * `inherit` is a CSS-wide keyword that makes the property itself inherit from
18
+ * *its own* parent (here, .cs-editor-mount's parent, which never defines it) —
19
+ * it does not store the string "inherit" for descendants to read. That breaks
20
+ * inheritance for every element below it, not just the unset one.
21
+ */
22
+ export declare function styleToCssVars(style: ScreenplayStyleDefinition): Record<string, string | null>;
23
+ /** Applies every var from `styleToCssVars` onto an element (the `.cs-editor-mount` ref),
24
+ * clearing any that resolved to `null` so a previous style's value can't linger. Also
25
+ * toggles a `.cs-force-italic-<code>` class per `style.italicScripts` — a class rather
26
+ * than a CSS var because `.cs-lang-<code>` rules already exist for lyrics/comments etc.
27
+ * that are legitimately italic on their own; a `font-style` var on `.cs-lang-<code>`
28
+ * itself would unconditionally win the cascade and stomp that even for styles that
29
+ * don't ask for forced italics (see types.ts's `italicScripts` doc for the full
30
+ * reasoning). The class only ever *adds* specificity to force italic on; it's absent
31
+ * entirely for scripts/styles that don't want it, so nothing is stomped either way. */
32
+ export declare function applyStyleVars(el: HTMLElement, style: ScreenplayStyleDefinition): void;
@@ -0,0 +1,71 @@
1
+ import { BODY_ELEMENT_KEYS, COMPLEX_SCRIPT_CODES } from './types.js';
2
+ function kebab(key) {
3
+ return key.replace(/([A-Z])/g, '-$1').toLowerCase();
4
+ }
5
+ /**
6
+ * Converts a style into the CSS custom properties screenplay.css reads via
7
+ * `var(--cs-<element>-<prop>, <classic-fallback>)`. Applied to `.cs-editor-mount`
8
+ * so the live editor picks up whichever style the current document (or an
9
+ * explicit override) resolves to — works identically for built-in and
10
+ * user-imported styles since both are plain `ScreenplayStyleDefinition` objects
11
+ * by the time they reach here.
12
+ *
13
+ * Deliberately does NOT emit anything for `spaceAfterPt` — the live editor only
14
+ * ever expresses "space before" via margin-top; adjacent elements form their gap
15
+ * from the *following* element's margin-top, not a margin-bottom on the
16
+ * preceding one (see types.ts). `spaceAfterPt` is DOCX/print-only.
17
+ *
18
+ * A `null` entry means "no override" and must be cleared with `removeProperty`,
19
+ * never `setProperty(k, 'inherit')`: for a CUSTOM property, the literal value
20
+ * `inherit` is a CSS-wide keyword that makes the property itself inherit from
21
+ * *its own* parent (here, .cs-editor-mount's parent, which never defines it) —
22
+ * it does not store the string "inherit" for descendants to read. That breaks
23
+ * inheritance for every element below it, not just the unset one.
24
+ */
25
+ export function styleToCssVars(style) {
26
+ const vars = {
27
+ '--cs-body-font-family': style.fontFamily,
28
+ };
29
+ for (const key of BODY_ELEMENT_KEYS) {
30
+ const el = style.elements[key];
31
+ const prefix = `--cs-${kebab(key)}`;
32
+ vars[`${prefix}-font-size`] = `${el.fontSizePt}pt`;
33
+ vars[`${prefix}-font-weight`] = el.bold ? 'bold' : 'normal';
34
+ vars[`${prefix}-font-style`] = el.italic ? 'italic' : 'normal';
35
+ vars[`${prefix}-text-decoration`] = el.underline ? 'underline' : 'none';
36
+ vars[`${prefix}-color`] = el.color ? `#${el.color}` : null;
37
+ vars[`${prefix}-margin-left`] = `${el.marginLeftPct ?? 0}%`;
38
+ vars[`${prefix}-margin-right`] = `${el.marginRightPct ?? 0}%`;
39
+ vars[`${prefix}-text-align`] = el.align ?? 'left';
40
+ vars[`${prefix}-margin-top`] = `${el.spaceBeforePt ?? 0}pt`;
41
+ }
42
+ // Per-script font override: screenplay.css's .cs-lang-<code> rules prefer this over
43
+ // the generic --cs-body-font-family fallback. Emit `null` for every code the style
44
+ // doesn't override so a previously-applied style's choice can't linger (see
45
+ // applyStyleVars). italicScripts is NOT emitted as a var — see applyStyleVars.
46
+ for (const code of COMPLEX_SCRIPT_CODES) {
47
+ vars[`--cs-lang-${code}-font-family`] = style.complexScriptFontOverrides?.[code] ?? null;
48
+ }
49
+ return vars;
50
+ }
51
+ /** Applies every var from `styleToCssVars` onto an element (the `.cs-editor-mount` ref),
52
+ * clearing any that resolved to `null` so a previous style's value can't linger. Also
53
+ * toggles a `.cs-force-italic-<code>` class per `style.italicScripts` — a class rather
54
+ * than a CSS var because `.cs-lang-<code>` rules already exist for lyrics/comments etc.
55
+ * that are legitimately italic on their own; a `font-style` var on `.cs-lang-<code>`
56
+ * itself would unconditionally win the cascade and stomp that even for styles that
57
+ * don't ask for forced italics (see types.ts's `italicScripts` doc for the full
58
+ * reasoning). The class only ever *adds* specificity to force italic on; it's absent
59
+ * entirely for scripts/styles that don't want it, so nothing is stomped either way. */
60
+ export function applyStyleVars(el, style) {
61
+ const vars = styleToCssVars(style);
62
+ for (const [k, v] of Object.entries(vars)) {
63
+ if (v === null)
64
+ el.style.removeProperty(k);
65
+ else
66
+ el.style.setProperty(k, v);
67
+ }
68
+ for (const code of COMPLEX_SCRIPT_CODES) {
69
+ el.classList.toggle(`cs-force-italic-${code}`, style.italicScripts?.includes(code) ?? false);
70
+ }
71
+ }
@@ -0,0 +1,7 @@
1
+ import type { IParagraphStyleOptions } from 'docx';
2
+ import type { ScreenplayStyleDefinition } from './types.js';
3
+ /** Builds the `paragraphStyles` array for `new Document({ styles: { paragraphStyles } })`
4
+ * from a style definition. `contentWidthTwips` is the page's printable width (page width
5
+ * minus left+right margins) — percentages are resolved against it, so indentation scales
6
+ * correctly with page size/margins instead of reusing one page format's fixed twips. */
7
+ export declare function styleToDocxParagraphStyles(style: ScreenplayStyleDefinition, contentWidthTwips: number): IParagraphStyleOptions[];
@@ -0,0 +1,84 @@
1
+ import { AlignmentType } from 'docx';
2
+ import { BODY_ELEMENT_KEYS, COVER_ELEMENT_KEYS } from './types.js';
3
+ import { KEEP_WITH_NEXT_ELEMENTS, KEEP_TOGETHER_ELEMENTS } from './pagination.js';
4
+ /** Sutra element -> stable docx paragraph style id. Fixed forever: docx-importer.ts
5
+ * matches re-imported .docx files against these exact ids, regardless of appearance. */
6
+ const DOCX_STYLE_IDS = {
7
+ sceneHeading: 'CineSceneHeading',
8
+ action: 'CineAction',
9
+ character: 'CineCharacter',
10
+ parenthetical: 'CineParenthetical',
11
+ dialogue: 'CineDialogue',
12
+ transition: 'CineTransition',
13
+ centered: 'CineCentered',
14
+ lyrics: 'CineLyrics',
15
+ section: 'CineSection',
16
+ note: 'CineNote',
17
+ comment: 'CineComment',
18
+ };
19
+ /** Cover-page element -> docx paragraph style id, referenced by docx-exporter.ts's
20
+ * title page. These must be emitted alongside the body styles: a paragraph naming a
21
+ * style the document doesn't define silently falls back to Word's Normal style
22
+ * (left-aligned 11pt black), which is exactly how the title page used to render. */
23
+ const DOCX_COVER_STYLE_IDS = {
24
+ title: 'CineTitle',
25
+ author: 'CineAuthor',
26
+ logline: 'CineLogline',
27
+ };
28
+ const ALIGN_MAP = {
29
+ left: AlignmentType.LEFT,
30
+ center: AlignmentType.CENTER,
31
+ right: AlignmentType.RIGHT,
32
+ };
33
+ function pctToTwips(pct, contentWidthTwips) {
34
+ if (pct === undefined)
35
+ return undefined;
36
+ return Math.round((pct / 100) * contentWidthTwips);
37
+ }
38
+ /** `pagination` carries the screenplay page-break rules for body elements (see
39
+ * pagination.ts); cover-page elements pass none — a cover page is one page by
40
+ * construction. */
41
+ function elementToDocxStyle(id, el, fontFamily, contentWidthTwips, pagination = {}) {
42
+ const left = pctToTwips(el.marginLeftPct, contentWidthTwips);
43
+ const right = pctToTwips(el.marginRightPct, contentWidthTwips);
44
+ const paragraph = {
45
+ alignment: el.align ? ALIGN_MAP[el.align] : undefined,
46
+ indent: (left !== undefined || right !== undefined) ? { left, right } : undefined,
47
+ spacing: {
48
+ before: el.spaceBeforePt !== undefined ? Math.round(el.spaceBeforePt * 20) : undefined,
49
+ after: el.spaceAfterPt !== undefined ? Math.round(el.spaceAfterPt * 20) : undefined,
50
+ },
51
+ keepNext: pagination.keepNext,
52
+ keepLines: pagination.keepLines,
53
+ // Stated rather than left to Word's default: action/dialogue stay breakable
54
+ // (no keepLines), so widow control is what stops a page break from stranding a
55
+ // single line of them on either side.
56
+ widowControl: true,
57
+ };
58
+ return {
59
+ id,
60
+ name: id,
61
+ run: {
62
+ font: fontFamily,
63
+ size: Math.round(el.fontSizePt * 2),
64
+ bold: el.bold || undefined,
65
+ italics: el.italic || undefined,
66
+ underline: el.underline ? {} : undefined,
67
+ color: el.color,
68
+ },
69
+ paragraph,
70
+ };
71
+ }
72
+ /** Builds the `paragraphStyles` array for `new Document({ styles: { paragraphStyles } })`
73
+ * from a style definition. `contentWidthTwips` is the page's printable width (page width
74
+ * minus left+right margins) — percentages are resolved against it, so indentation scales
75
+ * correctly with page size/margins instead of reusing one page format's fixed twips. */
76
+ export function styleToDocxParagraphStyles(style, contentWidthTwips) {
77
+ return [
78
+ ...BODY_ELEMENT_KEYS.map(key => elementToDocxStyle(DOCX_STYLE_IDS[key], style.elements[key], style.fontFamily, contentWidthTwips, {
79
+ keepNext: KEEP_WITH_NEXT_ELEMENTS.includes(key) || undefined,
80
+ keepLines: KEEP_TOGETHER_ELEMENTS.includes(key) || undefined,
81
+ })),
82
+ ...COVER_ELEMENT_KEYS.map(key => elementToDocxStyle(DOCX_COVER_STYLE_IDS[key], style.elements[key], style.fontFamily, contentWidthTwips)),
83
+ ];
84
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Embeds self-hosted @font-face rules (from shell/fonts/fonts.css, already loaded into
3
+ * the running app's own document) as data: URIs, for splicing into the PDF/print-preview
4
+ * HTML — a separate, standalone document that never sees the app's own stylesheets.
5
+ *
6
+ * Introspects the CSSOM (`document.styleSheets`) rather than hardcoding filenames/unicode
7
+ * ranges per family: whatever fonts.css declares for a family is exactly what gets embedded,
8
+ * so this can never drift out of sync with the actual font files bundled in public/fonts.
9
+ *
10
+ * Why embed at all instead of just linking `/fonts/...` from the print HTML: the native
11
+ * desktop PDF export writes the HTML to a temp file and loads it via a `file://` URL in a
12
+ * hidden webview, which cannot resolve a root-relative `/fonts/...` path back to the running
13
+ * app's own server/origin. A data: URI works regardless of the HTML's own location.
14
+ */
15
+ /** Builds `@font-face` CSS text embedding every self-hosted rule for the given family
16
+ * names, as data: URIs. Families with no matching rules (e.g. web-safe fonts that were
17
+ * never in fonts.css) are silently skipped — the Google Fonts `<link>`/generic fallback
18
+ * stack in print-adapter.ts's font stack still applies. Fetch failures for an individual
19
+ * rule are likewise skipped rather than failing the whole export. */
20
+ export declare function embedFontFacesFor(familyNames: string[]): Promise<string>;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Embeds self-hosted @font-face rules (from shell/fonts/fonts.css, already loaded into
3
+ * the running app's own document) as data: URIs, for splicing into the PDF/print-preview
4
+ * HTML — a separate, standalone document that never sees the app's own stylesheets.
5
+ *
6
+ * Introspects the CSSOM (`document.styleSheets`) rather than hardcoding filenames/unicode
7
+ * ranges per family: whatever fonts.css declares for a family is exactly what gets embedded,
8
+ * so this can never drift out of sync with the actual font files bundled in public/fonts.
9
+ *
10
+ * Why embed at all instead of just linking `/fonts/...` from the print HTML: the native
11
+ * desktop PDF export writes the HTML to a temp file and loads it via a `file://` URL in a
12
+ * hidden webview, which cannot resolve a root-relative `/fonts/...` path back to the running
13
+ * app's own server/origin. A data: URI works regardless of the HTML's own location.
14
+ */
15
+ function urlToDataUri(url) {
16
+ return fetch(url)
17
+ .then(res => {
18
+ if (!res.ok)
19
+ throw new Error(`Failed to fetch font: ${url}`);
20
+ return res.blob();
21
+ })
22
+ .then(blob => new Promise((resolve, reject) => {
23
+ const reader = new FileReader();
24
+ reader.onload = () => resolve(reader.result);
25
+ reader.onerror = () => reject(reader.error ?? new Error('Failed to read font file'));
26
+ reader.readAsDataURL(blob);
27
+ }));
28
+ }
29
+ function findFontFaceRules(familyName) {
30
+ const rules = [];
31
+ for (const sheet of Array.from(document.styleSheets)) {
32
+ let cssRules;
33
+ try {
34
+ cssRules = sheet.cssRules;
35
+ }
36
+ catch {
37
+ continue; // cross-origin stylesheets throw on access; nothing we bundle is cross-origin
38
+ }
39
+ for (const rule of Array.from(cssRules)) {
40
+ if (rule instanceof CSSFontFaceRule) {
41
+ const family = rule.style.getPropertyValue('font-family').replace(/^['"]|['"]$/g, '');
42
+ if (family === familyName)
43
+ rules.push(rule);
44
+ }
45
+ }
46
+ }
47
+ return rules;
48
+ }
49
+ /** Builds `@font-face` CSS text embedding every self-hosted rule for the given family
50
+ * names, as data: URIs. Families with no matching rules (e.g. web-safe fonts that were
51
+ * never in fonts.css) are silently skipped — the Google Fonts `<link>`/generic fallback
52
+ * stack in print-adapter.ts's font stack still applies. Fetch failures for an individual
53
+ * rule are likewise skipped rather than failing the whole export. */
54
+ export async function embedFontFacesFor(familyNames) {
55
+ const blocks = [];
56
+ for (const family of familyNames) {
57
+ for (const rule of findFontFaceRules(family)) {
58
+ const srcRaw = rule.style.getPropertyValue('src');
59
+ const match = /url\(["']?([^"')]+)["']?\)/.exec(srcRaw);
60
+ if (!match)
61
+ continue;
62
+ try {
63
+ const dataUri = await urlToDataUri(match[1]);
64
+ const weight = rule.style.getPropertyValue('font-weight') || '400';
65
+ const style = rule.style.getPropertyValue('font-style') || 'normal';
66
+ const unicodeRange = rule.style.getPropertyValue('unicode-range');
67
+ blocks.push('@font-face {\n' +
68
+ ` font-family: '${family}';\n` +
69
+ ` font-weight: ${weight};\n` +
70
+ ` font-style: ${style};\n` +
71
+ ` src: url(${dataUri}) format('woff2');\n` +
72
+ (unicodeRange ? ` unicode-range: ${unicodeRange};\n` : '') +
73
+ '}');
74
+ }
75
+ catch {
76
+ // Skip; the Google Fonts link / generic fallback stack still covers this family.
77
+ }
78
+ }
79
+ }
80
+ return blocks.join('\n');
81
+ }
@@ -0,0 +1,19 @@
1
+ import type { BodyElementKey } from './types.js';
2
+ /** Screenplay pagination rules — format conventions, not per-style visual choices, so
3
+ * they live here rather than in ScreenplayStyleDefinition: no style (built-in or
4
+ * user-imported) gets to end a page on a character cue. Consumed by both export
5
+ * paths, which express the same rule differently: DOCX via `keepNext`/`keepLines`
6
+ * paragraph properties (docx-adapter.ts), PDF/print via `break-after`/`break-inside`
7
+ * (print-adapter.ts). The live editor has no pagination, so it ignores these. */
8
+ /** Elements that must not be the last thing on a page — a scene heading or character
9
+ * cue stranded at the bottom with its action/dialogue overleaf is the classic
10
+ * screenplay pagination error. A parenthetical is included for the same reason: it
11
+ * belongs to the dialogue line that follows it. */
12
+ export declare const KEEP_WITH_NEXT_ELEMENTS: readonly BodyElementKey[];
13
+ /** Elements short enough that splitting them across a page boundary is always wrong.
14
+ * Deliberately excludes action/dialogue/lyrics/note — those can legitimately run
15
+ * longer than a page, so they get orphan/widow control instead of an unbreakable box. */
16
+ export declare const KEEP_TOGETHER_ELEMENTS: readonly BodyElementKey[];
17
+ /** Minimum lines of a breakable element left on either side of a page boundary. Two is
18
+ * the standard screenplay/typography minimum (a single stranded line reads as an error). */
19
+ export declare const MIN_ORPHAN_WIDOW_LINES = 2;