@sutrata/editor 0.1.0 → 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 +36 -9
  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,146 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { render } from '@testing-library/react'
3
- import { LanguageProvider, useLanguage } from '../../src/i18n/LanguageContext'
4
-
5
- // Test component that uses the hook
6
- function TestComponent({
7
- text,
8
- caretPos,
9
- frontmatter,
10
- }: {
11
- text: string
12
- caretPos: number
13
- frontmatter?: Record<string, any>
14
- }) {
15
- const { resolveLanguageAtCaret } = useLanguage()
16
- const lang = resolveLanguageAtCaret(text, caretPos, frontmatter)
17
- return <div data-testid="lang-result">{lang}</div>
18
- }
19
-
20
- describe('LanguageContext', () => {
21
- it('uses document frontmatter lang as default with no other context', () => {
22
- const text = '## Scene 1\n\nSome english text'
23
- const { getByTestId } = render(
24
- <LanguageProvider>
25
- <TestComponent text={text} caretPos={20} frontmatter={{ lang: 'ta' }} />
26
- </LanguageProvider>
27
- )
28
- expect(getByTestId('lang-result').textContent).toBe('ta')
29
- })
30
-
31
- it('scene metadata lang beats frontmatter lang', () => {
32
- // Scene with & lang: metadata should take precedence over frontmatter
33
- const text = '## Scene 1\n& lang: hi\n\nयह एक हिंदी पंक्ति है।'
34
- const { getByTestId } = render(
35
- <LanguageProvider>
36
- <TestComponent text={text} caretPos={30} frontmatter={{ lang: 'ta' }} />
37
- </LanguageProvider>
38
- )
39
- expect(getByTestId('lang-result').textContent).toBe('hi')
40
- })
41
-
42
- it('inline {lang=…} tag beats all precedence', () => {
43
- const text = '## Scene 1\n\nAction with {lang=ml} tag in it.'
44
- const { getByTestId } = render(
45
- <LanguageProvider>
46
- <TestComponent text={text} caretPos={25} frontmatter={{ lang: 'ta' }} />
47
- </LanguageProvider>
48
- )
49
- expect(getByTestId('lang-result').textContent).toBe('ml')
50
- })
51
-
52
- it('falls back to detectScript when no metadata', () => {
53
- const text = '## Scene 1\n\nஇது ஒரு தமிழ் வரி.'
54
- const { getByTestId } = render(
55
- <LanguageProvider>
56
- <TestComponent text={text} caretPos={20} />
57
- </LanguageProvider>
58
- )
59
- expect(getByTestId('lang-result').textContent).toBe('ta')
60
- })
61
-
62
- it('defaults to English for unrecognized text with no metadata', () => {
63
- const text = '## Scene 1\n\n12345 !!!!'
64
- const { getByTestId } = render(
65
- <LanguageProvider>
66
- <TestComponent text={text} caretPos={20} />
67
- </LanguageProvider>
68
- )
69
- expect(getByTestId('lang-result').textContent).toBe('en')
70
- })
71
-
72
- it('throws error when useLanguage used outside provider', () => {
73
- // This should error during render
74
- const BadComponent = () => {
75
- const { currentLanguage } = useLanguage()
76
- return <div>{currentLanguage}</div>
77
- }
78
-
79
- expect(() => {
80
- render(<BadComponent />)
81
- }).toThrow('useLanguage must be used within LanguageProvider')
82
- })
83
-
84
- it('inline tag beats scene metadata', () => {
85
- // Scene has metadata 'hi', but inline tag 'ml' should win
86
- const text = '## Scene 1\n& lang: hi\n\nयह {lang=ml} पंक्ति है।'
87
- const { getByTestId } = render(
88
- <LanguageProvider>
89
- <TestComponent text={text} caretPos={35} frontmatter={{ lang: 'ta' }} />
90
- </LanguageProvider>
91
- )
92
- expect(getByTestId('lang-result').textContent).toBe('ml')
93
- })
94
-
95
- it('detects Hindi from Devanagari script', () => {
96
- const text = '## Scene 1\n\nयह एक नमूना पंक्ति है।'
97
- const { getByTestId } = render(
98
- <LanguageProvider>
99
- <TestComponent text={text} caretPos={20} />
100
- </LanguageProvider>
101
- )
102
- expect(getByTestId('lang-result').textContent).toBe('hi')
103
- })
104
-
105
- it('detects Bengali from Bengali script', () => {
106
- const text = '## Scene 1\n\nএটি একটি নমুনা লাইন।'
107
- const { getByTestId } = render(
108
- <LanguageProvider>
109
- <TestComponent text={text} caretPos={20} />
110
- </LanguageProvider>
111
- )
112
- expect(getByTestId('lang-result').textContent).toBe('bn')
113
- })
114
-
115
- it('detects Malayalam from Malayalam script', () => {
116
- const text = '## Scene 1\n\nഇത് ഒരു സാമ്പിൾ ലൈൻ ആണ്.'
117
- const { getByTestId } = render(
118
- <LanguageProvider>
119
- <TestComponent text={text} caretPos={20} />
120
- </LanguageProvider>
121
- )
122
- expect(getByTestId('lang-result').textContent).toBe('ml')
123
- })
124
-
125
- it('falls back to frontmatter when scene has no metadata', () => {
126
- // Scene without metadata, only frontmatter specified
127
- const text = '## Scene 1\n\nEnglish text without any script markers'
128
- const { getByTestId } = render(
129
- <LanguageProvider>
130
- <TestComponent text={text} caretPos={20} frontmatter={{ lang: 'hi' }} />
131
- </LanguageProvider>
132
- )
133
- expect(getByTestId('lang-result').textContent).toBe('hi')
134
- })
135
-
136
- it('uses scene metadata when no inline tag present', () => {
137
- // Scene metadata present, no inline tag
138
- const text = '## Scene 1\n& lang: gu\n\nGujarati text here'
139
- const { getByTestId } = render(
140
- <LanguageProvider>
141
- <TestComponent text={text} caretPos={25} frontmatter={{ lang: 'ta' }} />
142
- </LanguageProvider>
143
- )
144
- expect(getByTestId('lang-result').textContent).toBe('gu')
145
- })
146
- })
@@ -1,54 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { detectScript } from '../../src/i18n/language-detect'
3
-
4
- describe('language-detect', () => {
5
- it('detects pure Devanagari text as Hindi', () => {
6
- const result = detectScript('यह एक नमूना पंक्ति है।')
7
- expect(result).toBe('hi')
8
- })
9
-
10
- it('detects pure Tamil text as Tamil', () => {
11
- const result = detectScript('இது ஒரு மாதிரி வரி.')
12
- expect(result).toBe('ta')
13
- })
14
-
15
- it('detects pure Telugu text as Telugu', () => {
16
- const result = detectScript('ఇది ఒక నమూనా లైన్.')
17
- expect(result).toBe('te')
18
- })
19
-
20
- it('detects mixed Devanagari + Latin as dominant Devanagari (Hindi)', () => {
21
- const result = detectScript('यह एक नमूना hello')
22
- expect(result).toBe('hi')
23
- })
24
-
25
- it('detects mixed Tamil + Latin as dominant Tamil', () => {
26
- const result = detectScript('இது hello வரி')
27
- expect(result).toBe('ta')
28
- })
29
-
30
- it('detects pure Latin / English as English', () => {
31
- const result = detectScript('This is a sample line.')
32
- expect(result).toBe('en')
33
- })
34
-
35
- it('returns English for empty string', () => {
36
- const result = detectScript('')
37
- expect(result).toBe('en')
38
- })
39
-
40
- it('detects Bengali text', () => {
41
- const result = detectScript('এটি একটি নমুনা লাইন।')
42
- expect(result).toBe('bn')
43
- })
44
-
45
- it('detects Gujarati text', () => {
46
- const result = detectScript('આ એક નમૂનો લાઇન છે.')
47
- expect(result).toBe('gu')
48
- })
49
-
50
- it('detects Malayalam text', () => {
51
- const result = detectScript('ഇത് ഒരു സാമ്പിൾ ലൈൻ ആണ്.')
52
- expect(result).toBe('ml')
53
- })
54
- })
@@ -1,25 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import en from '../../src/locales/en.json'
3
- import hi from '../../src/locales/hi.json'
4
- import ta from '../../src/locales/ta.json'
5
- import te from '../../src/locales/te.json'
6
- import ml from '../../src/locales/ml.json'
7
- import kn from '../../src/locales/kn.json'
8
- import bn from '../../src/locales/bn.json'
9
- import gu from '../../src/locales/gu.json'
10
- import mr from '../../src/locales/mr.json'
11
- import pa from '../../src/locales/pa.json'
12
- import or from '../../src/locales/or.json'
13
- import si from '../../src/locales/si.json'
14
-
15
- const LOCALES: Record<string, Record<string, string>> = { hi, ta, te, ml, kn, bn, gu, mr, pa, or, si }
16
- const EN_KEYS = Object.keys(en)
17
-
18
- describe('locale parity', () => {
19
- for (const [lang, strings] of Object.entries(LOCALES)) {
20
- it(`${lang}.json has all keys from en.json`, () => {
21
- const missingKeys = EN_KEYS.filter(key => !(key in strings))
22
- expect(missingKeys).toEqual([])
23
- })
24
- }
25
- })
@@ -1,45 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest'
2
- import { renderHook, act } from '@testing-library/react'
3
- import { TranslationProvider, useTranslation } from '../../src/i18n/useTranslation'
4
- import React from 'react'
5
-
6
- function wrapper({ children }: { children: React.ReactNode }) {
7
- return React.createElement(TranslationProvider, null, children)
8
- }
9
-
10
- describe('useTranslation', () => {
11
- beforeEach(() => {
12
- localStorage.clear()
13
- })
14
-
15
- it('t() returns English string by default', () => {
16
- const { result } = renderHook(() => useTranslation(), { wrapper })
17
- expect(result.current.t('toolbar.undo')).toBe('Undo')
18
- })
19
-
20
- it('t() returns Hindi string after setLocale("hi")', () => {
21
- const { result } = renderHook(() => useTranslation(), { wrapper })
22
- act(() => result.current.setLocale('hi'))
23
- expect(result.current.t('toolbar.undo')).toBe('पूर्ववत करें')
24
- })
25
-
26
- it('t() falls back to English for missing key in locale', () => {
27
- const { result } = renderHook(() => useTranslation(), { wrapper })
28
- act(() => result.current.setLocale('hi'))
29
- // Key that exists in English but hypothetically missing in Hindi would fallback
30
- const val = result.current.t('toolbar.undo')
31
- expect(typeof val).toBe('string')
32
- expect(val.length).toBeGreaterThan(0)
33
- })
34
-
35
- it('t() returns key itself for completely missing key', () => {
36
- const { result } = renderHook(() => useTranslation(), { wrapper })
37
- expect(result.current.t('nonexistent.key')).toBe('nonexistent.key')
38
- })
39
-
40
- it('setLocale ignores unknown locale codes', () => {
41
- const { result } = renderHook(() => useTranslation(), { wrapper })
42
- act(() => result.current.setLocale('xx')) // unknown
43
- expect(result.current.locale).toBe('en') // unchanged
44
- })
45
- })
@@ -1,18 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { keymapBindings } from '../src/editor/keymap'
3
-
4
- describe('keymap bindings', () => {
5
- it('does not bind element-insertion shortcuts Mod-1..Mod-7', () => {
6
- for (let n = 1; n <= 7; n++) {
7
- expect(keymapBindings[`Mod-${n}`]).toBeUndefined()
8
- }
9
- })
10
-
11
- it('keeps editing shortcuts', () => {
12
- expect(keymapBindings['Mod-z']).toBeDefined()
13
- expect(keymapBindings['Mod-b']).toBeDefined()
14
- expect(keymapBindings['Mod-i']).toBeDefined()
15
- expect(keymapBindings['Mod-u']).toBeDefined()
16
- expect(keymapBindings['Enter']).toBeDefined()
17
- })
18
- })
@@ -1,177 +0,0 @@
1
- import React, { useEffect } from 'react'
2
- import { describe, it, expect, vi } from 'vitest'
3
- import { render, screen, act, fireEvent } from '@testing-library/react'
4
- import { SceneNavigator } from '../../src/navigator/SceneNavigator'
5
- import { DocumentProvider, useDocument } from '../../src/context/DocumentContext'
6
- import { LanguageProvider } from '../../src/i18n/LanguageContext'
7
- import { TranslationProvider } from '../../src/i18n/useTranslation'
8
- import { setSourceView } from '../../src/editor/editor-bus'
9
-
10
- const SAMPLE_TEXT =
11
- '## INT. HOUSE - DAY\n& synopsis: A scene.\n\n## EXT. PARK - NIGHT\n& synopsis: Another scene.\n'
12
-
13
- /**
14
- * Helper component that sets the document text after mounting.
15
- * DocumentProvider does not accept an initialText prop, so we drive
16
- * the text through the context API.
17
- */
18
- function TextSetter({ text }: { text: string }) {
19
- const { setText } = useDocument()
20
- useEffect(() => {
21
- setText(text)
22
- // eslint-disable-next-line react-hooks/exhaustive-deps
23
- }, [])
24
- return null
25
- }
26
-
27
- function Wrapper({ text = SAMPLE_TEXT }: { text?: string }) {
28
- return (
29
- <TranslationProvider>
30
- <DocumentProvider>
31
- <LanguageProvider>
32
- <TextSetter text={text} />
33
- <SceneNavigator />
34
- </LanguageProvider>
35
- </DocumentProvider>
36
- </TranslationProvider>
37
- )
38
- }
39
-
40
- describe('SceneNavigator', () => {
41
- it('renders scene headings from text', async () => {
42
- await act(async () => {
43
- render(<Wrapper />)
44
- })
45
- expect(screen.getByText('INT. HOUSE - DAY')).toBeDefined()
46
- expect(screen.getByText('EXT. PARK - NIGHT')).toBeDefined()
47
- })
48
-
49
- it('shows scene synopses below headings', async () => {
50
- await act(async () => {
51
- render(<Wrapper />)
52
- })
53
- expect(screen.getByText('A scene.')).toBeDefined()
54
- expect(screen.getByText('Another scene.')).toBeDefined()
55
- })
56
-
57
- it('shows empty state when no scenes', async () => {
58
- await act(async () => {
59
- render(<Wrapper text="Just some plain text." />)
60
- })
61
- expect(screen.getByText(/No scenes yet/)).toBeDefined()
62
- })
63
-
64
- it('shows scene count in header', async () => {
65
- let container!: HTMLElement
66
- await act(async () => {
67
- ;({ container } = render(<Wrapper />))
68
- })
69
- const countBadge = container.querySelector('.cs-nav-count')
70
- expect(countBadge).not.toBeNull()
71
- expect(countBadge!.textContent).toBe('2')
72
- })
73
-
74
- it('renders the navigator panel when mounted', async () => {
75
- let container!: HTMLElement
76
- await act(async () => {
77
- ;({ container } = render(<Wrapper />))
78
- })
79
- // AppShell gates visibility via navVisible; SceneNavigator itself always renders
80
- expect(container.querySelector('.cs-navigator')).not.toBeNull()
81
- })
82
-
83
- it('dispatches to CodeMirror when clicked in source mode', async () => {
84
- const mockCmView = {
85
- dispatch: vi.fn(),
86
- focus: vi.fn(),
87
- }
88
- act(() => {
89
- setSourceView(mockCmView as any)
90
- })
91
-
92
- await act(async () => {
93
- render(<Wrapper />)
94
- })
95
-
96
- const sceneItem = screen.getByText('INT. HOUSE - DAY')
97
- fireEvent.click(sceneItem)
98
-
99
- expect(mockCmView.dispatch).toHaveBeenCalledWith(
100
- expect.objectContaining({
101
- selection: { anchor: 0 },
102
- effects: expect.anything(),
103
- })
104
- )
105
- expect(mockCmView.focus).toHaveBeenCalled()
106
- })
107
-
108
- it('shows a single merged synopsis+duration AI action as an icon-only button beside the synopsis field, not as labeled buttons in a separate row', async () => {
109
- let container!: HTMLElement
110
- await act(async () => {
111
- ;({ container } = render(<Wrapper />))
112
- })
113
-
114
- // No visible text labels — icon-only per-scene action now.
115
- expect(screen.queryByText('Synopsis')).toBeNull()
116
- expect(screen.queryByText('Duration')).toBeNull()
117
-
118
- // Still reachable via its accessible name, and living inside the
119
- // synopsis row (sibling of the synopsis text/input), not a separate row.
120
- const metadataBtn = screen.getAllByLabelText('Generate synopsis & estimate duration')[0]!
121
- const row = metadataBtn.closest('.cs-nav-synopsis-row')
122
- expect(row).not.toBeNull()
123
- expect(row!.querySelector('.cs-nav-scene-synopsis')).not.toBeNull()
124
- })
125
-
126
- it('hides estimated duration along with synopsis when "Hide Synopsis & Duration" is toggled', async () => {
127
- const text = '## INT. HOUSE - DAY\n& synopsis: A scene.\n& est-duration: 01:15\n'
128
- await act(async () => {
129
- render(<Wrapper text={text} />)
130
- })
131
-
132
- expect(screen.getByText('A scene.')).toBeDefined()
133
- expect(screen.getByText('⏱ 01:15')).toBeDefined()
134
-
135
- const toggle = screen.getByLabelText('Hide Synopsis & Duration')
136
- await act(async () => {
137
- fireEvent.click(toggle)
138
- })
139
-
140
- expect(screen.queryByText('A scene.')).toBeNull()
141
- expect(screen.queryByText('⏱ 01:15')).toBeNull()
142
-
143
- const showToggle = screen.getByLabelText('Show Synopsis & Duration')
144
- await act(async () => {
145
- fireEvent.click(showToggle)
146
- })
147
-
148
- expect(screen.getByText('A scene.')).toBeDefined()
149
- expect(screen.getByText('⏱ 01:15')).toBeDefined()
150
- })
151
-
152
- it('dispatches to CodeMirror for frontmatter jump in source mode', async () => {
153
- const mockCmView = {
154
- dispatch: vi.fn(),
155
- focus: vi.fn(),
156
- }
157
- act(() => {
158
- setSourceView(mockCmView as any)
159
- })
160
-
161
- await act(async () => {
162
- render(<Wrapper />)
163
- })
164
-
165
- const fmBtn = screen.getByTitle('Title Page')
166
- fireEvent.click(fmBtn)
167
-
168
- expect(mockCmView.dispatch).toHaveBeenCalledWith(
169
- expect.objectContaining({
170
- selection: { anchor: 0 },
171
- effects: expect.anything(),
172
- })
173
- )
174
- expect(mockCmView.focus).toHaveBeenCalled()
175
- })
176
- })
177
-
@@ -1,63 +0,0 @@
1
- import React, { useEffect } from 'react'
2
- import { describe, it, expect } from 'vitest'
3
- import { render, screen, act } from '@testing-library/react'
4
- import { StatisticsDialog } from '../../src/navigator/StatisticsDialog'
5
- import { DocumentProvider, useDocument } from '../../src/context/DocumentContext'
6
-
7
- /**
8
- * Reproduces the reported bug: many scenes whose seconds components alone
9
- * (ignored by the old MM-only parser) add up to well over a minute. Three
10
- * scenes here sum to 01:15 + 00:50 + 00:40 = 2:45 total — the buggy
11
- * MM-only sum would report "1 min" (1+0+0), the fix must report "3 min"
12
- * (rounded from 2m45s).
13
- */
14
- const SAMPLE_TEXT =
15
- '## INT. HOUSE - DAY\n& est-duration: 01:15\n\n' +
16
- '## EXT. PARK - NIGHT\n& est-duration: 00:50\n\n' +
17
- '## INT. OFFICE - DAY\n& est-duration: 00:40\n'
18
-
19
- function TextSetter({ text }: { text: string }) {
20
- const { setText } = useDocument()
21
- useEffect(() => {
22
- setText(text)
23
- // eslint-disable-next-line react-hooks/exhaustive-deps
24
- }, [])
25
- return null
26
- }
27
-
28
- function Wrapper({ text = SAMPLE_TEXT }: { text?: string }) {
29
- return (
30
- <DocumentProvider>
31
- <TextSetter text={text} />
32
- <StatisticsDialog onClose={() => {}} />
33
- </DocumentProvider>
34
- )
35
- }
36
-
37
- describe('StatisticsDialog', () => {
38
- it('sums scene durations by total seconds, not just the MM part', async () => {
39
- await act(async () => {
40
- render(<Wrapper />)
41
- })
42
- // 75 + 50 + 40 = 165s = 2m45s -> rounds to 3 min, not 1 min.
43
- expect(screen.getByText('3 min')).toBeInTheDocument()
44
- })
45
-
46
- it('formats totals of an hour or more as "Xh Ym"', async () => {
47
- const text =
48
- '## INT. A - DAY\n& est-duration: 30:00\n\n' +
49
- '## INT. B - DAY\n& est-duration: 35:30\n'
50
- await act(async () => {
51
- render(<Wrapper text={text} />)
52
- })
53
- // 30:00 + 35:30 = 65m30s -> rounds to 66 min -> "1h 6m"
54
- expect(screen.getByText('1h 6m')).toBeInTheDocument()
55
- })
56
-
57
- it('shows TBD when no scene has an estimated duration', async () => {
58
- await act(async () => {
59
- render(<Wrapper text="## INT. HOUSE - DAY\nSome action.\n" />)
60
- })
61
- expect(screen.getByText('TBD')).toBeInTheDocument()
62
- })
63
- })
@@ -1,71 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { buildSceneList } from '../../src/navigator/scene-list'
3
-
4
- const SAMPLE = `---
5
- title: Test Script
6
- lang: hi
7
- ---
8
-
9
- ## INT. LIVING ROOM - DAY {#scene-001}
10
- & synopsis: The family gathers.
11
- & status: done
12
- & location: INT
13
-
14
- This is action text.
15
-
16
- ## EXT. STREET - NIGHT {#scene-002}
17
- & synopsis: Rain falls hard.
18
- & status: wip
19
-
20
- More action.
21
-
22
- ## INT. CAFE - DAY
23
- & synopsis: A quiet moment.
24
- `
25
-
26
- describe('buildSceneList', () => {
27
- it('returns 3 scenes from the sample', () => {
28
- const scenes = buildSceneList(SAMPLE)
29
- expect(scenes.length).toBe(3)
30
- })
31
-
32
- it('extracts scene headings correctly', () => {
33
- const scenes = buildSceneList(SAMPLE)
34
- expect(scenes[0].heading).toBe('INT. LIVING ROOM - DAY')
35
- expect(scenes[1].heading).toBe('EXT. STREET - NIGHT')
36
- expect(scenes[2].heading).toBe('INT. CAFE - DAY')
37
- })
38
-
39
- it('extracts scene synopses from & synopsis: metadata', () => {
40
- const scenes = buildSceneList(SAMPLE)
41
- expect(scenes[0].synopsis).toBe('The family gathers.')
42
- expect(scenes[1].synopsis).toBe('Rain falls hard.')
43
- expect(scenes[2].synopsis).toBe('A quiet moment.')
44
- })
45
-
46
- it('extracts status metadata', () => {
47
- const scenes = buildSceneList(SAMPLE)
48
- expect(scenes[0].status).toBe('done')
49
- expect(scenes[1].status).toBe('wip')
50
- expect(scenes[2].status).toBe('')
51
- })
52
-
53
- it('assigns sequential 1-based index', () => {
54
- const scenes = buildSceneList(SAMPLE)
55
- expect(scenes[0].index).toBe(1)
56
- expect(scenes[1].index).toBe(2)
57
- expect(scenes[2].index).toBe(3)
58
- })
59
-
60
- it('returns empty array for text with no scenes', () => {
61
- const scenes = buildSceneList('Just some action text.')
62
- expect(scenes.length).toBe(0)
63
- })
64
-
65
- it('records textOffset for each scene', () => {
66
- const scenes = buildSceneList(SAMPLE)
67
- // All offsets should be distinct and increasing
68
- expect(scenes[0].textOffset).toBeLessThan(scenes[1].textOffset)
69
- expect(scenes[1].textOffset).toBeLessThan(scenes[2].textOffset)
70
- })
71
- })
@@ -1,82 +0,0 @@
1
- import { paginatePrintDocument } from '../src/file/print-paginator'
2
-
3
- /** jsdom has no layout engine, so every element measures 0. Stub the one measurement the
4
- * placement logic depends on when nothing can be split — a page body's height — as a flat
5
- * 40px per block, which makes page capacity exactly predictable. */
6
- function stubHeights(perBlockPx: number) {
7
- const descriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'scrollHeight')
8
- Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
9
- configurable: true,
10
- get(this: HTMLElement) {
11
- return this.classList.contains('print-page-body') ? this.children.length * perBlockPx : perBlockPx
12
- },
13
- })
14
- return () => {
15
- if (descriptor) Object.defineProperty(HTMLElement.prototype, 'scrollHeight', descriptor)
16
- }
17
- }
18
-
19
- function setup(blocks: string): void {
20
- document.body.innerHTML = `<div id="print-pages"></div><div id="print-flow">${blocks}</div>`
21
- }
22
-
23
- function pages() {
24
- return Array.from(document.querySelectorAll('.print-page')).map(p => ({
25
- continued: p.querySelector('.print-page-continued')!.textContent,
26
- number: p.querySelector('.print-page-number')!.textContent,
27
- blocks: Array.from(p.querySelector('.print-page-body')!.children).map(c => c.className),
28
- }))
29
- }
30
-
31
- const action = (scene: string, n: number) =>
32
- `<div class="print-action" data-scene="${scene}">Action ${n}</div>`
33
- const heading = (scene: string) =>
34
- `<div class="print-scene-heading" data-scene="${scene}" data-scene-start="${scene}">INT. ROOM</div>`
35
-
36
- describe('print paginator', () => {
37
- let restore = () => {}
38
- beforeEach(() => { restore = stubHeights(40) })
39
- afterEach(() => { restore() })
40
-
41
- it('numbers every page and marks only the ones that open mid-scene as CONTINUED', () => {
42
- setup(heading('8') + Array.from({ length: 11 }, (_, i) => action('8', i)).join(''))
43
- // 204px of page height, less the 4px rounding slack, fits five 40px blocks.
44
- paginatePrintDocument({ contentHeightPx: 204, pageNumberPrefix: 'p', continuedSuffix: 'CONTINUED:' })
45
-
46
- const result = pages()
47
- expect(result.map(p => p.number)).toEqual(['p1', 'p2', 'p3'])
48
- expect(result[0]!.continued).toBe('') // the page the scene starts on
49
- expect(result[1]!.continued).toBe('8 CONTINUED:')
50
- expect(result[2]!.continued).toBe('8 CONTINUED:')
51
- expect(document.getElementById('print-flow')).toBeNull()
52
- })
53
-
54
- it('starts a fresh page at a scene heading rather than leaving it stranded at the bottom', () => {
55
- // Four blocks of scene 8, then scene 9's heading would land as the fifth (last) block.
56
- setup(heading('8') + Array.from({ length: 3 }, (_, i) => action('8', i)).join('')
57
- + heading('9') + action('9', 0))
58
- paginatePrintDocument({ contentHeightPx: 204, pageNumberPrefix: 'p', continuedSuffix: 'CONTINUED:' })
59
-
60
- const result = pages()
61
- expect(result).toHaveLength(2)
62
- expect(result[0]!.blocks).toHaveLength(4)
63
- expect(result[1]!.blocks[0]).toContain('print-scene-heading')
64
- expect(result[1]!.continued).toBe('') // scene 9 starts here, so no continuation
65
- })
66
-
67
- it('breaks the page where a Sutra page break says to', () => {
68
- setup(heading('8') + action('8', 0) + '<div class="print-page-break"></div>' + action('8', 1))
69
- paginatePrintDocument({ contentHeightPx: 2000, pageNumberPrefix: 'p', continuedSuffix: 'CONTINUED:' })
70
-
71
- const result = pages()
72
- expect(result).toHaveLength(2)
73
- expect(result[0]!.blocks).toHaveLength(2)
74
- expect(result[1]!.continued).toBe('8 CONTINUED:')
75
- })
76
-
77
- it('does nothing when the document has no flow to paginate', () => {
78
- document.body.innerHTML = '<div id="print-pages"></div>'
79
- expect(() => paginatePrintDocument({ contentHeightPx: 204, pageNumberPrefix: 'p', continuedSuffix: 'CONTINUED:' })).not.toThrow()
80
- expect(document.querySelectorAll('.print-page')).toHaveLength(0)
81
- })
82
- })