@sutrata/editor 0.1.1 → 0.1.2

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 (152) hide show
  1. package/README.md +87 -0
  2. package/dist/ai/generated-providers.json +1 -1
  3. package/package.json +30 -3
  4. package/scripts/copy-assets.mjs +0 -16
  5. package/scripts/generate-providers.mjs +0 -127
  6. package/src/ai/OnboardingDialog.tsx +0 -390
  7. package/src/ai/VoiceConfirmDialog.tsx +0 -137
  8. package/src/ai/VoiceToolbar.tsx +0 -321
  9. package/src/ai/ai-client.ts +0 -596
  10. package/src/ai/commands.ts +0 -164
  11. package/src/ai/generated-providers.json +0 -3696
  12. package/src/ai/prompts.ts +0 -62
  13. package/src/ai/voice-service.ts +0 -96
  14. package/src/autocomplete/AutocompletePlugin.ts +0 -21
  15. package/src/autocomplete/suggestions.ts +0 -147
  16. package/src/context/DocumentContext.tsx +0 -648
  17. package/src/desktop-api.d.ts +0 -40
  18. package/src/editor/EditorView.tsx +0 -82
  19. package/src/editor/SelectionMenu.tsx +0 -152
  20. package/src/editor/attr-pencil-plugin.ts +0 -172
  21. package/src/editor/character-decoration-plugin.ts +0 -104
  22. package/src/editor/editor-bus.ts +0 -51
  23. package/src/editor/find-highlight-plugin.ts +0 -68
  24. package/src/editor/frontmatter-field-view.ts +0 -52
  25. package/src/editor/frontmatter-field.ts +0 -115
  26. package/src/editor/grapheme-cursor.ts +0 -99
  27. package/src/editor/ime-plugin.ts +0 -85
  28. package/src/editor/insert-helper.ts +0 -43
  29. package/src/editor/keymap.ts +0 -124
  30. package/src/editor/language-class-plugin.ts +0 -91
  31. package/src/editor/plugins.ts +0 -26
  32. package/src/editor/prosemirror-to-sutra.ts +0 -192
  33. package/src/editor/scene-heading-view.ts +0 -147
  34. package/src/editor/schema.ts +0 -156
  35. package/src/editor/sutra-to-prosemirror.ts +0 -156
  36. package/src/extensions/storage-adapter.ts +0 -65
  37. package/src/extensions/types.ts +0 -60
  38. package/src/file/docx-exporter.ts +0 -585
  39. package/src/file/docx-importer.ts +0 -313
  40. package/src/file/print-paginator.ts +0 -301
  41. package/src/file/templates.ts +0 -73
  42. package/src/file/workflow-reports.ts +0 -820
  43. package/src/findreplace/FindReplace.tsx +0 -242
  44. package/src/findreplace/find-engine.ts +0 -97
  45. package/src/findreplace/scroll-to-match.ts +0 -89
  46. package/src/fonts/tofu-detect.ts +0 -53
  47. package/src/i18n/LanguageContext.tsx +0 -207
  48. package/src/i18n/language-detect.ts +0 -73
  49. package/src/i18n/useTranslation.tsx +0 -74
  50. package/src/index.ts +0 -27
  51. package/src/locales/bn.json +0 -143
  52. package/src/locales/en.json +0 -143
  53. package/src/locales/gu.json +0 -143
  54. package/src/locales/hi.json +0 -143
  55. package/src/locales/kn.json +0 -143
  56. package/src/locales/ml.json +0 -143
  57. package/src/locales/mr.json +0 -143
  58. package/src/locales/or.json +0 -143
  59. package/src/locales/pa.json +0 -143
  60. package/src/locales/si.json +0 -143
  61. package/src/locales/ta.json +0 -143
  62. package/src/locales/te.json +0 -143
  63. package/src/navigator/SceneNavigator.tsx +0 -844
  64. package/src/navigator/StatisticsDialog.tsx +0 -249
  65. package/src/navigator/scene-list.ts +0 -84
  66. package/src/settings/SettingsDialog.tsx +0 -303
  67. package/src/shell/AppBar.tsx +0 -354
  68. package/src/shell/AppShell.tsx +0 -71
  69. package/src/shell/ConfirmDialog.tsx +0 -71
  70. package/src/shell/ElementToolbar.tsx +0 -175
  71. package/src/shell/LanguageStatus.tsx +0 -101
  72. package/src/shell/StatusBar.tsx +0 -135
  73. package/src/shell/SutrataLogo.tsx +0 -89
  74. package/src/shell/Toast.tsx +0 -46
  75. package/src/shell/ToolbarButton.tsx +0 -30
  76. package/src/shell/VersionHistory.tsx +0 -212
  77. package/src/shell/Watermark.tsx +0 -9
  78. package/src/shell/fonts/fonts.css +0 -1133
  79. package/src/shell/icons.tsx +0 -335
  80. package/src/shell/toolbar-actions.ts +0 -59
  81. package/src/source/SourceView.tsx +0 -111
  82. package/src/source/find-highlight-source.ts +0 -57
  83. package/src/source/sutra-lang.ts +0 -121
  84. package/src/spellcheck/dictionaries.ts +0 -21
  85. package/src/spellcheck/hunspell.ts +0 -110
  86. package/src/spellcheck/spellcheck-plugin.ts +0 -83
  87. package/src/styles/StyleDialog.tsx +0 -148
  88. package/src/styles/builtin-styles.ts +0 -139
  89. package/src/styles/css-adapter.ts +0 -76
  90. package/src/styles/docx-adapter.ts +0 -109
  91. package/src/styles/embed-fonts.ts +0 -81
  92. package/src/styles/pagination.ts +0 -27
  93. package/src/styles/print-adapter.ts +0 -288
  94. package/src/styles/registry.ts +0 -20
  95. package/src/styles/screenplay.css +0 -3006
  96. package/src/styles/script-ranges.ts +0 -17
  97. package/src/styles/types.ts +0 -104
  98. package/src/styles/validate.ts +0 -114
  99. package/src/templates/templates.ts +0 -194
  100. package/src/titlepage/ExportDialog.tsx +0 -494
  101. package/src/titlepage/TitlePageDialog.tsx +0 -312
  102. package/src/titlepage/frontmatter-form.ts +0 -280
  103. package/src/titlepage/languages.ts +0 -37
  104. package/src/types.ts +0 -7
  105. package/tests/AppBar.test.tsx +0 -35
  106. package/tests/AppShell.test.tsx +0 -22
  107. package/tests/ElementToolbar.test.tsx +0 -36
  108. package/tests/LanguageStatus.test.tsx +0 -129
  109. package/tests/SelectionMenu.test.tsx +0 -71
  110. package/tests/ToolbarButton.test.tsx +0 -24
  111. package/tests/ai-onboarding.test.ts +0 -102
  112. package/tests/ai-status.test.tsx +0 -72
  113. package/tests/ai-structured.test.ts +0 -94
  114. package/tests/autocomplete/AutocompletePlugin.test.ts +0 -20
  115. package/tests/autocomplete/suggestions.test.ts +0 -106
  116. package/tests/document-context.test.tsx +0 -565
  117. package/tests/docx-importer.test.ts +0 -160
  118. package/tests/editor/character-decoration-plugin.test.ts +0 -101
  119. package/tests/editor/character-extension.test.ts +0 -44
  120. package/tests/editor/grapheme-cursor.test.ts +0 -118
  121. package/tests/editor/ime-plugin.test.ts +0 -72
  122. package/tests/editor/inline-note-bridge.test.ts +0 -32
  123. package/tests/editor/language-class-plugin.test.ts +0 -265
  124. package/tests/editor/title-page.test.ts +0 -124
  125. package/tests/embed-fonts.test.ts +0 -17
  126. package/tests/exporters.test.ts +0 -365
  127. package/tests/findreplace/FindReplace.test.tsx +0 -391
  128. package/tests/findreplace/find-engine.test.ts +0 -86
  129. package/tests/findreplace/scroll-to-match.test.ts +0 -50
  130. package/tests/fonts/tofu-detect.test.ts +0 -122
  131. package/tests/i18n/LanguageContext.test.tsx +0 -146
  132. package/tests/i18n/language-detect.test.ts +0 -54
  133. package/tests/i18n/locale-parity.test.ts +0 -25
  134. package/tests/i18n/useTranslation.test.tsx +0 -45
  135. package/tests/keymap.test.ts +0 -18
  136. package/tests/navigator/SceneNavigator.test.tsx +0 -177
  137. package/tests/navigator/StatisticsDialog.test.tsx +0 -63
  138. package/tests/navigator/scene-list.test.ts +0 -71
  139. package/tests/print-paginator.test.ts +0 -82
  140. package/tests/shell/VersionHistory.test.tsx +0 -129
  141. package/tests/spellcheck/hunspell.test.ts +0 -82
  142. package/tests/spellcheck/spellcheck-plugin.test.ts +0 -56
  143. package/tests/styles.test.ts +0 -315
  144. package/tests/templates/templates.test.ts +0 -83
  145. package/tests/titlepage/TitlePageDialog.test.tsx +0 -90
  146. package/tests/titlepage/apply-title-page.test.ts +0 -61
  147. package/tests/titlepage/frontmatter-form.test.ts +0 -115
  148. package/tests/toolbar-actions.test.ts +0 -93
  149. package/tests/voice-commands.test.ts +0 -58
  150. package/tsconfig.json +0 -17
  151. package/vitest.config.ts +0 -11
  152. package/vitest.setup.ts +0 -65
@@ -1,21 +0,0 @@
1
- export interface DictionaryManifest {
2
- aff: string // path relative to public/
3
- dic: string // path relative to public/
4
- }
5
-
6
- /**
7
- * Dictionary manifest: maps BCP-47 language code → asset paths under public/dict/.
8
- * The actual .dic/.aff files are not committed; see public/dict/README.md for acquisition.
9
- */
10
- export const DICTIONARIES: Record<string, DictionaryManifest> = {
11
- 'en': { aff: '/dict/en_US.aff', dic: '/dict/en_US.dic' },
12
- 'hi': { aff: '/dict/hi_IN.aff', dic: '/dict/hi_IN.dic' },
13
- 'ta': { aff: '/dict/ta_IN.aff', dic: '/dict/ta_IN.dic' },
14
- 'te': { aff: '/dict/te_IN.aff', dic: '/dict/te_IN.dic' },
15
- 'kn': { aff: '/dict/kn_IN.aff', dic: '/dict/kn_IN.dic' },
16
- 'ml': { aff: '/dict/ml_IN.aff', dic: '/dict/ml_IN.dic' },
17
- 'bn': { aff: '/dict/bn_IN.aff', dic: '/dict/bn_IN.dic' },
18
- 'gu': { aff: '/dict/gu_IN.aff', dic: '/dict/gu_IN.dic' },
19
- 'pa': { aff: '/dict/pa_IN.aff', dic: '/dict/pa_IN.dic' },
20
- 'or': { aff: '/dict/or_IN.aff', dic: '/dict/or_IN.dic' },
21
- }
@@ -1,110 +0,0 @@
1
- import { DICTIONARIES } from './dictionaries'
2
-
3
- export interface SpellChecker {
4
- check(word: string): boolean
5
- suggest(word: string): string[]
6
- }
7
-
8
- /**
9
- * In-memory dictionary: Set-based word lookup.
10
- * Production: load from .dic file; tests: use fixture words.
11
- */
12
- class SimpleSpellChecker implements SpellChecker {
13
- private words: Set<string>
14
- private customWords: Set<string>
15
-
16
- constructor(words: string[]) {
17
- this.words = new Set(words.map(w => w.toLowerCase().trim()).filter(Boolean))
18
- this.customWords = new Set()
19
- }
20
-
21
- check(word: string): boolean {
22
- const lower = word.toLowerCase()
23
- return this.words.has(lower) || this.customWords.has(lower)
24
- }
25
-
26
- suggest(_word: string): string[] {
27
- // Minimal: return empty (full Hunspell suggestions require .aff parsing)
28
- return []
29
- }
30
-
31
- addCustomWord(word: string) {
32
- this.customWords.add(word.toLowerCase())
33
- }
34
- }
35
-
36
- // Cache: lang → checker
37
- const checkerCache = new Map<string, SpellChecker>()
38
-
39
- // Custom words per-project (in-memory; production uses IndexedDB)
40
- const customWords = new Map<string, Set<string>>()
41
-
42
- /**
43
- * Load a dictionary for the given language.
44
- * In production: fetches .dic file, parses word list.
45
- * In test/jsdom: fetch will fail gracefully, returning an empty checker.
46
- */
47
- export async function loadDictionary(lang: string): Promise<SpellChecker> {
48
- if (checkerCache.has(lang)) {
49
- return checkerCache.get(lang)!
50
- }
51
-
52
- const manifest = DICTIONARIES[lang]
53
- if (!manifest) {
54
- // Fallback: empty checker for unknown language codes
55
- const checker = new SimpleSpellChecker([])
56
- checkerCache.set(lang, checker)
57
- return checker
58
- }
59
-
60
- try {
61
- // Fetch .dic file (word list, one per line)
62
- const response = await fetch(manifest.dic)
63
- if (!response.ok) throw new Error(`Failed to load dictionary: ${manifest.dic}`)
64
- const text = await response.text()
65
- const words = text.split('\n').filter(Boolean)
66
- const checker = new SimpleSpellChecker(words)
67
-
68
- // Apply any cached custom words
69
- const custom = customWords.get(lang)
70
- if (custom) {
71
- for (const w of custom) checker.addCustomWord(w)
72
- }
73
-
74
- checkerCache.set(lang, checker)
75
- return checker
76
- } catch {
77
- // Network unavailable (expected in dev/CI without dict files)
78
- const checker = new SimpleSpellChecker([])
79
- checkerCache.set(lang, checker)
80
- return checker
81
- }
82
- }
83
-
84
- /**
85
- * Add a word to the custom dictionary for a language.
86
- * Persists in-memory; production would write to IndexedDB.
87
- */
88
- export function addToCustomDictionary(lang: string, word: string) {
89
- if (!customWords.has(lang)) customWords.set(lang, new Set())
90
- customWords.get(lang)!.add(word.toLowerCase())
91
-
92
- // Update cached checker if loaded
93
- const checker = checkerCache.get(lang) as SimpleSpellChecker | undefined
94
- if (checker) checker.addCustomWord(word)
95
- }
96
-
97
- /**
98
- * Create a spell checker from a word list (for testing).
99
- */
100
- export function createCheckerFromWords(words: string[]): SpellChecker {
101
- return new SimpleSpellChecker(words)
102
- }
103
-
104
- /**
105
- * Clear cached checkers (for testing).
106
- */
107
- export function clearCheckerCache() {
108
- checkerCache.clear()
109
- customWords.clear()
110
- }
@@ -1,83 +0,0 @@
1
- import { Plugin, EditorState } from 'prosemirror-state'
2
- import { Decoration, DecorationSet } from 'prosemirror-view'
3
- import { loadDictionary, SpellChecker } from './hunspell'
4
-
5
- // Module-level state
6
- let currentChecker: SpellChecker | null = null
7
- let currentLang = 'en'
8
-
9
- export function setSpellcheckLanguage(lang: string) {
10
- if (lang === currentLang) return
11
- currentLang = lang
12
- currentChecker = null // force reload
13
- loadDictionary(lang).then(checker => {
14
- currentChecker = checker
15
- })
16
- }
17
-
18
- /**
19
- * Tokenize text into words (splitting on whitespace and punctuation).
20
- * Returns array of {word, start, end} with character offsets.
21
- * Uses Unicode letter/mark categories to support Indic scripts.
22
- */
23
- export function tokenizeWords(text: string): Array<{ word: string; start: number; end: number }> {
24
- const results: Array<{ word: string; start: number; end: number }> = []
25
- const regex = /[\p{L}\p{M}'-]+/gu
26
- let match
27
- while ((match = regex.exec(text)) !== null) {
28
- results.push({ word: match[0], start: match.index, end: match.index + match[0].length })
29
- }
30
- return results
31
- }
32
-
33
- /**
34
- * ProseMirror plugin that underlines misspelled words via Decoration API.
35
- * Recomputes decorations on every doc change.
36
- */
37
- export function createSpellcheckPlugin() {
38
- return new Plugin({
39
- state: {
40
- init() {
41
- return DecorationSet.empty
42
- },
43
- apply(tr, set, _oldState, newState) {
44
- if (!tr.docChanged) return set
45
- if (!currentChecker) return DecorationSet.empty
46
-
47
- return computeDecorations(newState, currentChecker)
48
- },
49
- },
50
- props: {
51
- decorations(state) {
52
- return this.getState(state)
53
- },
54
- },
55
- })
56
- }
57
-
58
- function computeDecorations(state: EditorState, checker: SpellChecker): DecorationSet {
59
- const decorations: Decoration[] = []
60
-
61
- state.doc.forEach((node, offset) => {
62
- if (!node.isBlock || !node.textContent) return
63
-
64
- const text = node.textContent
65
- const tokens = tokenizeWords(text)
66
-
67
- for (const token of tokens) {
68
- if (!checker.check(token.word)) {
69
- // Misspelled: add wavy underline decoration
70
- const from = offset + 1 + token.start
71
- const to = offset + 1 + token.end
72
- decorations.push(
73
- Decoration.inline(from, to, {
74
- class: 'cs-spell-error',
75
- title: `"${token.word}" may be misspelled`,
76
- })
77
- )
78
- }
79
- }
80
- })
81
-
82
- return DecorationSet.create(state.doc, decorations)
83
- }
@@ -1,148 +0,0 @@
1
- import React, { useEffect, useRef, useState } from 'react'
2
- import { useDocument } from '../context/DocumentContext'
3
- import { useTranslation } from '../i18n/useTranslation'
4
- import { allStyles } from './registry'
5
- import { isBuiltinStyleId } from './builtin-styles'
6
- import { validateStyleJson } from './validate'
7
- import { useStorageAdapter } from '../extensions/storage-adapter'
8
- import type { ScreenplayStyleDefinition } from './types'
9
-
10
- interface Props {
11
- onClose: () => void
12
- }
13
-
14
- export function StyleDialog({ onClose }: Props) {
15
- const { resolvedStyle, customStyles, setDocumentStyle, reloadCustomStyles } = useDocument()
16
- const { t } = useTranslation()
17
- const storageAdapter = useStorageAdapter()
18
- const dialogRef = useRef<HTMLDivElement>(null)
19
- const [error, setError] = useState<string | null>(null)
20
-
21
- useEffect(() => {
22
- dialogRef.current?.focus()
23
- }, [])
24
-
25
- const handleKeyDown = (e: React.KeyboardEvent) => {
26
- if (e.key === 'Escape') onClose()
27
- }
28
-
29
- async function handleImport() {
30
- setError(null)
31
- const picked = await storageAdapter.openStyleJson()
32
- if (!picked) return
33
-
34
- let data: unknown
35
- try {
36
- data = JSON.parse(picked.text)
37
- } catch {
38
- setError(`${t('style.importInvalid')}: not valid JSON`)
39
- return
40
- }
41
-
42
- const result = validateStyleJson(data)
43
- if (!result.ok) {
44
- setError(`${t('style.importInvalid')}: ${result.errors.join('; ')}`)
45
- return
46
- }
47
-
48
- if (isBuiltinStyleId(result.style.id)) {
49
- setError(t('style.importIdReserved'))
50
- return
51
- }
52
- if (customStyles.some(s => s.id === result.style.id) && !confirm(t('style.importOverwriteConfirm'))) {
53
- return
54
- }
55
-
56
- await storageAdapter.saveStyle(result.style)
57
- await reloadCustomStyles()
58
- }
59
-
60
- async function handleDelete(id: string) {
61
- if (!confirm(t('style.deleteConfirm'))) return
62
- await storageAdapter.deleteStyle(id)
63
- await reloadCustomStyles()
64
- }
65
-
66
- function renderRow(style: ScreenplayStyleDefinition, deletable: boolean) {
67
- const active = style.id === resolvedStyle.id
68
- const swatchColor = style.elements.sceneHeading.color ? `#${style.elements.sceneHeading.color}` : '#999'
69
- return (
70
- <div
71
- key={style.id}
72
- className="cs-settings-row"
73
- style={{ alignItems: 'flex-start', cursor: 'pointer', padding: '6px 0' }}
74
- onClick={() => setDocumentStyle(style.id)}
75
- >
76
- <div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
77
- <span style={{ display: 'inline-block', width: '10px', height: '10px', borderRadius: '50%', background: swatchColor, flexShrink: 0, marginTop: '4px' }} />
78
- <div>
79
- <div className="cs-settings-label" style={{ fontWeight: active ? 600 : 400 }}>
80
- {style.name}
81
- {active && <span style={{ color: '#1976d2', fontWeight: 600 }}> — {t('style.current')}</span>}
82
- </div>
83
- {style.description && (
84
- <div style={{ fontSize: '11px', color: '#888', marginTop: '2px' }}>{style.description}</div>
85
- )}
86
- </div>
87
- </div>
88
- {deletable && (
89
- <button
90
- className="cs-panel-close"
91
- aria-label={t('style.delete')}
92
- onClick={e => { e.stopPropagation(); void handleDelete(style.id) }}
93
- >
94
- ×
95
- </button>
96
- )}
97
- </div>
98
- )
99
- }
100
-
101
- const builtins = allStyles([]).filter(s => isBuiltinStyleId(s.id))
102
-
103
- return (
104
- <div className="cs-dialog-overlay" onClick={onClose}>
105
- <div
106
- ref={dialogRef}
107
- className="cs-dialog cs-settings-dialog"
108
- role="dialog"
109
- aria-modal="true"
110
- aria-label={t('style.title')}
111
- tabIndex={-1}
112
- onClick={e => e.stopPropagation()}
113
- onKeyDown={handleKeyDown}
114
- >
115
- <div className="cs-dialog-header">
116
- <span className="cs-dialog-title">{t('style.title')}</span>
117
- <button className="cs-fr-close" aria-label={t('style.close')} onClick={onClose}>×</button>
118
- </div>
119
-
120
- <div className="cs-settings-body">
121
- {error && (
122
- <div className="cs-export-warnings" style={{ marginTop: 0 }}>
123
- <div className="cs-export-warn-item">{error}</div>
124
- </div>
125
- )}
126
-
127
- <div className="cs-settings-section-label">{t('style.builtIn')}</div>
128
- {builtins.map(s => renderRow(s, false))}
129
-
130
- {customStyles.length > 0 && (
131
- <>
132
- <div className="cs-settings-section-label" style={{ marginTop: '8px' }}>{t('style.custom')}</div>
133
- {customStyles.map(s => renderRow(s, true))}
134
- </>
135
- )}
136
-
137
- <button
138
- className="cs-export-btn-cancel"
139
- style={{ marginTop: '8px', alignSelf: 'flex-start' }}
140
- onClick={() => { void handleImport() }}
141
- >
142
- {t('style.import')}
143
- </button>
144
- </div>
145
- </div>
146
- </div>
147
- )
148
- }
@@ -1,139 +0,0 @@
1
- import type { ScreenplayStyleDefinition } from './types.js'
2
- import { DEFAULT_STYLE_ID } from './types.js'
3
-
4
- /**
5
- * The app's long-standing default look, extracted from the values that were
6
- * hardcoded across screenplay.css / docx-exporter.ts / workflow-reports.ts
7
- * before those three were unified onto this shared definition. Where the
8
- * three disagreed (they had drifted — see the multi-style implementation
9
- * plan), the live editor's CSS is treated as canonical for font-size/color/
10
- * margin-%, so the editor's appearance is unchanged; DOCX/PDF absorb the
11
- * (small, confirmed) corrections needed to match it.
12
- */
13
- export const CLASSIC_STYLE: ScreenplayStyleDefinition = {
14
- id: 'classic',
15
- name: 'Classic',
16
- description: 'The original Sutrata look — teal scene headings, rust character cues.',
17
- fontFamily: 'Noto Sans',
18
- page: { marginTopIn: 1, marginBottomIn: 1, marginLeftIn: 1, marginRightIn: 1 },
19
- elements: {
20
- sceneHeading: { fontSizePt: 13, bold: true, color: '0A5C6A', marginLeftPct: 10, marginRightPct: 8, spaceBeforePt: 13, spaceAfterPt: 7.2 },
21
- action: { fontSizePt: 12, marginLeftPct: 10, marginRightPct: 8, spaceBeforePt: 6, spaceAfterPt: 7.2 },
22
- character: { fontSizePt: 13, bold: true, color: '9C4221', marginLeftPct: 17, spaceBeforePt: 6.5, spaceAfterPt: 0 },
23
- parenthetical: { fontSizePt: 13, marginLeftPct: 17, marginRightPct: 20, spaceAfterPt: 0 },
24
- dialogue: { fontSizePt: 12, marginLeftPct: 17, marginRightPct: 17, spaceAfterPt: 7.2 },
25
- transition: { fontSizePt: 13, align: 'right', marginRightPct: 8, spaceBeforePt: 6.5, spaceAfterPt: 7.2 },
26
- centered: { fontSizePt: 13, align: 'center', spaceBeforePt: 6.5, spaceAfterPt: 7.2 },
27
- lyrics: { fontSizePt: 13, italic: true, marginLeftPct: 10, marginRightPct: 8, spaceAfterPt: 3.6 },
28
- section: { fontSizePt: 14, bold: true, color: '6F6F6F', marginLeftPct: 10, spaceBeforePt: 21, spaceAfterPt: 7.2 },
29
- note: { fontSizePt: 10, color: '7A6000', marginLeftPct: 10, marginRightPct: 8, spaceAfterPt: 3.6 },
30
- comment: { fontSizePt: 10, color: 'AAAAAA', italic: true, marginLeftPct: 10, spaceAfterPt: 3.6 },
31
- title: { fontSizePt: 24, bold: true, color: '0A5C6A', align: 'center', spaceAfterPt: 14.4 },
32
- author: { fontSizePt: 16, align: 'center', spaceAfterPt: 7.2 },
33
- logline: { fontSizePt: 12, italic: true, color: '555555', align: 'center', spaceAfterPt: 7.2 },
34
- },
35
- }
36
-
37
- /**
38
- * A second built-in style with a genuinely different look — monospace,
39
- * monochrome (no `color` on any element, exercising the fallback-to-black
40
- * path), industry-standard flush-left margins — to prove the architecture
41
- * isn't secretly hardcoded to CLASSIC_STYLE's shape.
42
- */
43
- export const INDUSTRY_COURIER_STYLE: ScreenplayStyleDefinition = {
44
- id: 'industry-courier',
45
- name: 'Industry Courier',
46
- description: 'Monospace, monochrome, flush-left margins — the classic spec-script look.',
47
- fontFamily: 'Courier Prime',
48
- page: { marginTopIn: 1, marginBottomIn: 1, marginLeftIn: 1.5, marginRightIn: 1 },
49
- elements: {
50
- sceneHeading: { fontSizePt: 12, bold: true, spaceBeforePt: 24, spaceAfterPt: 12 },
51
- action: { fontSizePt: 12, spaceBeforePt: 12, spaceAfterPt: 12 },
52
- character: { fontSizePt: 12, bold: true, marginLeftPct: 30, spaceBeforePt: 12, spaceAfterPt: 0 },
53
- parenthetical: { fontSizePt: 12, marginLeftPct: 26, marginRightPct: 15, spaceAfterPt: 0 },
54
- dialogue: { fontSizePt: 12, marginLeftPct: 20, marginRightPct: 15, spaceAfterPt: 12 },
55
- transition: { fontSizePt: 12, align: 'right', spaceBeforePt: 12, spaceAfterPt: 12 },
56
- centered: { fontSizePt: 12, align: 'center', spaceBeforePt: 12, spaceAfterPt: 12 },
57
- lyrics: { fontSizePt: 12, italic: true, spaceAfterPt: 6 },
58
- section: { fontSizePt: 12, bold: true, spaceBeforePt: 24, spaceAfterPt: 12 },
59
- note: { fontSizePt: 10, spaceAfterPt: 6 },
60
- comment: { fontSizePt: 10, italic: true, spaceAfterPt: 6 },
61
- title: { fontSizePt: 24, bold: true, align: 'center', spaceAfterPt: 14.4 },
62
- author: { fontSizePt: 16, align: 'center', spaceAfterPt: 7.2 },
63
- logline: { fontSizePt: 12, italic: true, align: 'center', spaceAfterPt: 7.2 },
64
- },
65
- }
66
-
67
- /** Every Indic script Sutrata already self-hosts Noto Sans for, pointed at its Noto
68
- * Serif equivalent instead (all bundled in shell/fonts/fonts.css the same way as Noto
69
- * Sans — see fonts-serif-block.css generation notes in that file's history). Reused by
70
- * TRADITIONAL_STYLE's `complexScriptFontOverrides`: Noto Serif was chosen over the
71
- * originally-considered Vijaya specifically because it already covers every script
72
- * Noto Sans does and carries an OFL license, so — unlike Vijaya — it can actually ship
73
- * in the repo instead of depending on the reader's OS/Word already having it installed. */
74
- const NOTO_SERIF_COMPLEX_SCRIPT_FONTS: Record<string, string> = {
75
- hi: 'Noto Serif Devanagari', ta: 'Noto Serif Tamil', te: 'Noto Serif Telugu',
76
- kn: 'Noto Serif Kannada', ml: 'Noto Serif Malayalam', bn: 'Noto Serif Bengali',
77
- gu: 'Noto Serif Gujarati', pa: 'Noto Serif Gurmukhi', or: 'Noto Serif Oriya',
78
- si: 'Noto Serif Sinhala',
79
- }
80
-
81
- /**
82
- * Modeled on a real Tamil feature screenplay PDF (betterfountain/Fountain export):
83
- * bold scene headings, underlined (not bold) character cues, fully monochrome,
84
- * flush-left action/scene headings, A4 with 1in margins all around.
85
- * Measurements (font sizes, indents as % of content width) were taken
86
- * directly from the PDF's text positions; elements the reference document
87
- * doesn't print at all (section/note/comment — Fountain strips these from
88
- * output) are extrapolated to match its scale rather than measured.
89
- *
90
- * The reference PDF actually used Vijaya, a proprietary Microsoft font — not
91
- * bundled here (unlike Noto, it can't legally ship in the repo, and depending
92
- * on the reader's OS/Word already having it installed is a worse default than
93
- * a font that just works). Noto Serif is the deliberate second choice: not a
94
- * perfect visual match, but it already has a variant for every script Noto
95
- * Sans does (so this style isn't just "Tamil-only" the way a Vijaya-based one
96
- * would be) and ships under the OFL, so it's self-hosted for real instead of
97
- * hoped-for. Tamil specifically renders in Noto Serif Tamil's *italic* face by
98
- * default (`italicScripts`, forced even on bold scene headings/sections) since
99
- * that slant reads closer to Vijaya than the upright serif does — every other
100
- * script stays upright serif, `italicScripts` only lists 'ta'.
101
- */
102
- export const TRADITIONAL_STYLE: ScreenplayStyleDefinition = {
103
- id: 'traditional',
104
- name: 'Traditional',
105
- description: 'Bold serif scene headings, underlined cues, italic Tamil — modeled on a real Tamil shooting script.',
106
- fontFamily: 'Noto Serif',
107
- complexScriptFontOverrides: NOTO_SERIF_COMPLEX_SCRIPT_FONTS,
108
- italicScripts: ['ta'],
109
- // The reference PDF's cover: title / "by <author>" / logline flush top-left,
110
- // date and contact down in the bottom-right corner.
111
- coverLayout: 'corners',
112
- page: { marginTopIn: 1, marginBottomIn: 1, marginLeftIn: 1, marginRightIn: 1 },
113
- elements: {
114
- sceneHeading: { fontSizePt: 12, bold: true, spaceBeforePt: 17, spaceAfterPt: 9 },
115
- action: { fontSizePt: 10, spaceBeforePt: 8, spaceAfterPt: 9 },
116
- character: { fontSizePt: 10, underline: true, marginLeftPct: 32, spaceBeforePt: 8, spaceAfterPt: 0 },
117
- parenthetical: { fontSizePt: 10, marginLeftPct: 32, marginRightPct: 20, spaceAfterPt: 0 },
118
- dialogue: { fontSizePt: 10, marginLeftPct: 24, marginRightPct: 17, spaceAfterPt: 9 },
119
- transition: { fontSizePt: 10, align: 'right', spaceBeforePt: 9, spaceAfterPt: 9 },
120
- centered: { fontSizePt: 10, align: 'center', italic: true, spaceBeforePt: 9, spaceAfterPt: 9 },
121
- lyrics: { fontSizePt: 10, italic: true, marginLeftPct: 6, spaceAfterPt: 5 },
122
- section: { fontSizePt: 12, bold: true, spaceBeforePt: 27, spaceAfterPt: 9 },
123
- note: { fontSizePt: 10, color: '666666', spaceAfterPt: 5 },
124
- comment: { fontSizePt: 13, italic: true, spaceAfterPt: 5 },
125
- title: { fontSizePt: 32, bold: true, align: 'left', spaceAfterPt: 16 },
126
- author: { fontSizePt: 16, align: 'left', spaceAfterPt: 8 },
127
- logline: { fontSizePt: 10, align: 'left', spaceAfterPt: 8 },
128
- },
129
- }
130
-
131
- export const BUILTIN_STYLES: ScreenplayStyleDefinition[] = [CLASSIC_STYLE, INDUSTRY_COURIER_STYLE, TRADITIONAL_STYLE]
132
-
133
- export function getBuiltinStyle(id: string): ScreenplayStyleDefinition | undefined {
134
- return BUILTIN_STYLES.find(s => s.id === id)
135
- }
136
-
137
- export function isBuiltinStyleId(id: string): boolean {
138
- return id === DEFAULT_STYLE_ID || BUILTIN_STYLES.some(s => s.id === id)
139
- }
@@ -1,76 +0,0 @@
1
- import { BODY_ELEMENT_KEYS, COMPLEX_SCRIPT_CODES } from './types.js'
2
- import type { ScreenplayStyleDefinition, ElementStyle } from './types.js'
3
-
4
- function kebab(key: string): string {
5
- return key.replace(/([A-Z])/g, '-$1').toLowerCase()
6
- }
7
-
8
- /**
9
- * Converts a style into the CSS custom properties screenplay.css reads via
10
- * `var(--cs-<element>-<prop>, <classic-fallback>)`. Applied to `.cs-editor-mount`
11
- * so the live editor picks up whichever style the current document (or an
12
- * explicit override) resolves to — works identically for built-in and
13
- * user-imported styles since both are plain `ScreenplayStyleDefinition` objects
14
- * by the time they reach here.
15
- *
16
- * Deliberately does NOT emit anything for `spaceAfterPt` — the live editor only
17
- * ever expresses "space before" via margin-top; adjacent elements form their gap
18
- * from the *following* element's margin-top, not a margin-bottom on the
19
- * preceding one (see types.ts). `spaceAfterPt` is DOCX/print-only.
20
- *
21
- * A `null` entry means "no override" and must be cleared with `removeProperty`,
22
- * never `setProperty(k, 'inherit')`: for a CUSTOM property, the literal value
23
- * `inherit` is a CSS-wide keyword that makes the property itself inherit from
24
- * *its own* parent (here, .cs-editor-mount's parent, which never defines it) —
25
- * it does not store the string "inherit" for descendants to read. That breaks
26
- * inheritance for every element below it, not just the unset one.
27
- */
28
- export function styleToCssVars(style: ScreenplayStyleDefinition): Record<string, string | null> {
29
- const vars: Record<string, string | null> = {
30
- '--cs-body-font-family': style.fontFamily,
31
- }
32
-
33
- for (const key of BODY_ELEMENT_KEYS) {
34
- const el: ElementStyle = style.elements[key]
35
- const prefix = `--cs-${kebab(key)}`
36
- vars[`${prefix}-font-size`] = `${el.fontSizePt}pt`
37
- vars[`${prefix}-font-weight`] = el.bold ? 'bold' : 'normal'
38
- vars[`${prefix}-font-style`] = el.italic ? 'italic' : 'normal'
39
- vars[`${prefix}-text-decoration`] = el.underline ? 'underline' : 'none'
40
- vars[`${prefix}-color`] = el.color ? `#${el.color}` : null
41
- vars[`${prefix}-margin-left`] = `${el.marginLeftPct ?? 0}%`
42
- vars[`${prefix}-margin-right`] = `${el.marginRightPct ?? 0}%`
43
- vars[`${prefix}-text-align`] = el.align ?? 'left'
44
- vars[`${prefix}-margin-top`] = `${el.spaceBeforePt ?? 0}pt`
45
- }
46
-
47
- // Per-script font override: screenplay.css's .cs-lang-<code> rules prefer this over
48
- // the generic --cs-body-font-family fallback. Emit `null` for every code the style
49
- // doesn't override so a previously-applied style's choice can't linger (see
50
- // applyStyleVars). italicScripts is NOT emitted as a var — see applyStyleVars.
51
- for (const code of COMPLEX_SCRIPT_CODES) {
52
- vars[`--cs-lang-${code}-font-family`] = style.complexScriptFontOverrides?.[code] ?? null
53
- }
54
-
55
- return vars
56
- }
57
-
58
- /** Applies every var from `styleToCssVars` onto an element (the `.cs-editor-mount` ref),
59
- * clearing any that resolved to `null` so a previous style's value can't linger. Also
60
- * toggles a `.cs-force-italic-<code>` class per `style.italicScripts` — a class rather
61
- * than a CSS var because `.cs-lang-<code>` rules already exist for lyrics/comments etc.
62
- * that are legitimately italic on their own; a `font-style` var on `.cs-lang-<code>`
63
- * itself would unconditionally win the cascade and stomp that even for styles that
64
- * don't ask for forced italics (see types.ts's `italicScripts` doc for the full
65
- * reasoning). The class only ever *adds* specificity to force italic on; it's absent
66
- * entirely for scripts/styles that don't want it, so nothing is stomped either way. */
67
- export function applyStyleVars(el: HTMLElement, style: ScreenplayStyleDefinition): void {
68
- const vars = styleToCssVars(style)
69
- for (const [k, v] of Object.entries(vars)) {
70
- if (v === null) el.style.removeProperty(k)
71
- else el.style.setProperty(k, v)
72
- }
73
- for (const code of COMPLEX_SCRIPT_CODES) {
74
- el.classList.toggle(`cs-force-italic-${code}`, style.italicScripts?.includes(code) ?? false)
75
- }
76
- }