@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,101 +0,0 @@
1
- import React, { useState, useEffect, useRef } from 'react'
2
- import { useLanguage } from '../i18n/LanguageContext'
3
- import { useDocument } from '../context/DocumentContext'
4
- import { getEditorView } from '../editor/editor-bus'
5
-
6
- const LANG_NAMES: Record<string, string> = {
7
- en: 'English',
8
- hi: 'Hindi (Devanagari)',
9
- ta: 'Tamil',
10
- te: 'Telugu',
11
- kn: 'Kannada',
12
- ml: 'Malayalam',
13
- bn: 'Bengali',
14
- gu: 'Gujarati',
15
- pa: 'Gurmukhi',
16
- or: 'Odia',
17
- si: 'Sinhala',
18
- }
19
-
20
- export function LanguageStatus() {
21
- const { currentLanguage } = useLanguage()
22
- const { text, setText } = useDocument()
23
- const [menuOpen, setMenuOpen] = useState(false)
24
- const menuRef = useRef<HTMLDivElement>(null)
25
-
26
- const langName = LANG_NAMES[currentLanguage] ?? currentLanguage
27
-
28
- useEffect(() => {
29
- if (!menuOpen) return
30
- const handleClickOutside = (e: MouseEvent) => {
31
- if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
32
- setMenuOpen(false)
33
- }
34
- }
35
- const handleKeyDown = (e: KeyboardEvent) => {
36
- if (e.key === 'Escape') setMenuOpen(false)
37
- }
38
- document.addEventListener('mousedown', handleClickOutside)
39
- document.addEventListener('keydown', handleKeyDown)
40
- return () => {
41
- document.removeEventListener('mousedown', handleClickOutside)
42
- document.removeEventListener('keydown', handleKeyDown)
43
- }
44
- }, [menuOpen])
45
-
46
- const handleSelectLanguage = (code: string) => {
47
- const view = getEditorView()
48
- if (view) {
49
- const { state, dispatch } = view
50
- const { from, to } = state.selection
51
- const tagText = `{lang=${code}}`
52
- dispatch(state.tr.insertText(tagText, from, to))
53
- view.focus()
54
- } else {
55
- setText(text + `\n{lang=${code}}`)
56
- }
57
- setMenuOpen(false)
58
- }
59
-
60
- return (
61
- <div className="cs-lang-status" ref={menuRef}>
62
- <button
63
- type="button"
64
- className="cs-lang-status-btn cs-lang-pill"
65
- title={`Script language at caret: ${langName} (${currentLanguage.toUpperCase()})`}
66
- aria-label={`Script language: ${langName}`}
67
- aria-haspopup="menu"
68
- aria-expanded={menuOpen}
69
- onClick={() => setMenuOpen(!menuOpen)}
70
- >
71
- <span className="cs-lang-pill-script">अ/A</span>
72
- <span className="cs-lang-code">{currentLanguage.toUpperCase()}</span>
73
- </button>
74
-
75
- {menuOpen && (
76
- <div className="cs-lang-menu" role="menu" aria-label="Script languages">
77
- {Object.entries(LANG_NAMES).map(([code, name]) => {
78
- const isActive = currentLanguage === code
79
- return (
80
- <button
81
- type="button"
82
- key={code}
83
- role="menuitem"
84
- className={`cs-lang-option${isActive ? ' cs-lang-active' : ''}`}
85
- onClick={(e) => {
86
- e.stopPropagation()
87
- handleSelectLanguage(code)
88
- }}
89
- >
90
- <span>{name}</span>
91
- <span className="cs-lang-code" style={{ fontSize: '10px', opacity: 0.7 }}>
92
- {code.toUpperCase()}
93
- </span>
94
- </button>
95
- )
96
- })}
97
- </div>
98
- )}
99
- </div>
100
- )
101
- }
@@ -1,135 +0,0 @@
1
- import React, { useMemo, useState, useEffect } from 'react'
2
- import { useDocument } from '../context/DocumentContext'
3
- import { LanguageStatus } from './LanguageStatus'
4
- import { buildSceneList } from '../navigator/scene-list'
5
- import { StatisticsDialog } from '../navigator/StatisticsDialog'
6
- import { VersionHistory } from './VersionHistory'
7
- import { StatsIcon, HistoryIcon } from './icons'
8
- import { subscribeToAICalls, formatActiveCalls } from '../ai/ai-client'
9
- import { useTranslation } from '../i18n/useTranslation'
10
-
11
- function AIStatusBarStatus() {
12
- const [activeCalls, setActiveCalls] = useState<{ providerName: string; model: string }[]>([])
13
-
14
- useEffect(() => {
15
- return subscribeToAICalls(setActiveCalls)
16
- }, [])
17
-
18
- if (activeCalls.length === 0) return null
19
-
20
- return (
21
- <div
22
- className="cs-ai-status-indicator"
23
- style={{
24
- display: 'inline-flex',
25
- alignItems: 'center',
26
- gap: '6px',
27
- color: 'var(--cs-ui-accent, #c4760a)',
28
- fontWeight: 600,
29
- fontSize: '11px',
30
- marginRight: '8px',
31
- }}
32
- >
33
- <span className="cs-spinner" style={{ width: '10px', height: '10px', borderWidth: '1.5px' }} />
34
- <span>{formatActiveCalls(activeCalls)}</span>
35
- </div>
36
- )
37
- }
38
-
39
- export function StatusBar() {
40
- const { text, isDirty, lastSaveTarget, filePath, versions, versionHistoryVisible, setVersionHistoryVisible, restoreVersion, clearVersionHistory } = useDocument()
41
- const { t } = useTranslation()
42
- const [statsOpen, setStatsOpen] = useState(false)
43
-
44
- // VS Code-style: a local autosave backs the content up but never counts as
45
- // "saved" in the main label — only a real write to the file (saveFile/
46
- // saveFileAs, which clears isDirty) does. While still dirty, the tooltip
47
- // says whether that local backup has caught up with the latest edits yet.
48
- const saveLabel = isDirty
49
- ? t('statusbar.unsavedChanges')
50
- : lastSaveTarget === 'file' ? t('statusbar.savedToFile')
51
- : t('statusbar.allChangesSaved')
52
- const saveTooltip = isDirty
53
- ? lastSaveTarget === 'local' ? t('statusbar.savedLocallyTooltip') : undefined
54
- : lastSaveTarget === 'file' ? t('statusbar.savedToFileTooltip')
55
- : undefined
56
-
57
- const scenes = useMemo(() => buildSceneList(text), [text])
58
-
59
- const { wordCount, pageEstimate } = useMemo(() => {
60
- const trimmed = text.trim()
61
- const words = trimmed ? trimmed.split(/\s+/).length : 0
62
- // Standard screenplay rule: ~54 lines or ~200-250 words per page
63
- const lines = text.split('\n').length
64
- const estPages = Math.max(1, Math.round(lines / 54))
65
- return { wordCount: words, pageEstimate: estPages }
66
- }, [text])
67
-
68
- return (
69
- <>
70
- <footer className="cs-status-bar" role="contentinfo" aria-label="Status bar">
71
- <div className="cs-sb-left">
72
- <span className="cs-sb-metric cs-sb-metric-scenes" title={t('statusbar.scenesTooltip')}>
73
- <strong>{scenes.length}</strong> <span className="cs-sb-metric-label">{scenes.length === 1 ? t('statusbar.scene') : t('statusbar.scenes')}</span>
74
- </span>
75
- <span className="cs-sb-sep">•</span>
76
- <span className="cs-sb-metric cs-sb-metric-pages" title={t('statusbar.pagesTooltip')}>
77
- <strong>~{pageEstimate}</strong> <span className="cs-sb-metric-label">{pageEstimate === 1 ? t('statusbar.page') : t('statusbar.pages')}</span>
78
- </span>
79
- <span className="cs-sb-sep cs-sb-words-sep">•</span>
80
- <span className="cs-sb-metric cs-sb-metric-words" title={t('statusbar.wordsTooltip')}>
81
- <strong>{wordCount.toLocaleString()}</strong> <span className="cs-sb-metric-label">{t('statusbar.words')}</span>
82
- </span>
83
- </div>
84
-
85
- <div className="cs-sb-center">
86
- <div className="cs-sb-save" role="status" aria-live="polite" title={saveTooltip}>
87
- <span className={`cs-sb-dot ${isDirty ? 'cs-sb-dot-dirty' : 'cs-sb-dot-clean'}`} />
88
- <span className={isDirty ? 'cs-sb-save-dirty' : 'cs-sb-save-clean'}>
89
- {saveLabel}
90
- </span>
91
- </div>
92
- </div>
93
-
94
- <div className="cs-sb-right">
95
- <AIStatusBarStatus />
96
- {filePath && versions.length > 0 && (
97
- <button
98
- type="button"
99
- className="cs-nav-action-btn"
100
- onClick={() => setVersionHistoryVisible(true)}
101
- title={`${versions.length} version${versions.length === 1 ? '' : 's'} saved`}
102
- style={{ fontSize: '11px', gap: '4px', padding: '2px 6px' }}
103
- >
104
- <HistoryIcon size={13} />
105
- <span>{versions.length}</span>
106
- </button>
107
- )}
108
- <button
109
- type="button"
110
- className="cs-nav-action-btn"
111
- onClick={() => setStatsOpen(true)}
112
- title={t('statusbar.statsTooltip')}
113
- style={{ fontSize: '11px', gap: '4px', padding: '2px 6px' }}
114
- >
115
- <StatsIcon size={13} />
116
- <span>{t('statusbar.statsButton')}</span>
117
- </button>
118
- <LanguageStatus />
119
- </div>
120
- </footer>
121
-
122
- {statsOpen && <StatisticsDialog onClose={() => setStatsOpen(false)} />}
123
- {filePath && (
124
- <VersionHistory
125
- versions={versions}
126
- filePath={filePath}
127
- onRestore={restoreVersion}
128
- onClearAll={clearVersionHistory}
129
- visible={versionHistoryVisible}
130
- onClose={() => setVersionHistoryVisible(false)}
131
- />
132
- )}
133
- </>
134
- )
135
- }
@@ -1,89 +0,0 @@
1
- import React from 'react'
2
-
3
- // The Sutrata monogram: an S-shaped filmstrip ribbon with sprocket holes and a
4
- // center reel, in the app's Deep Amber / Amber Glow palette. Source of truth for
5
- // the shape and color values is packages/app/public/sutrata-monogram.svg — keep
6
- // the two in sync if either changes.
7
-
8
- interface LogoProps {
9
- size?: number
10
- className?: string
11
- showText?: boolean
12
- }
13
-
14
- const MONOGRAM_ASPECT = 231.65818 / 324.60373
15
-
16
- export function SutrataLogo({ size = 20, className = '', showText = true }: LogoProps) {
17
- const height = size
18
- const width = size * MONOGRAM_ASPECT
19
-
20
- return (
21
- <div className={`cs-brand-mark ${className}`} style={{ display: 'inline-flex', alignItems: 'center', gap: '7px', userSelect: 'none' }}>
22
- <svg
23
- width={width}
24
- height={height}
25
- viewBox="0 0 231.65818 324.60373"
26
- xmlns="http://www.w3.org/2000/svg"
27
- aria-hidden="true"
28
- style={{ flexShrink: 0 }}
29
- >
30
- <g transform="translate(-3.0466903,-3.069361)">
31
- <g>
32
- <path
33
- fill="#502308"
34
- d="M 76.35291,222.53041 C 75.478969,209.22098 71.352034,194.29907 56.664655,177.24787 35.352192,152.22995 46.850499,139.85601 62.978003,130.13046 L 3.6899176,150.4351 c 5.0786411,20.05339 13.9650544,36.08746 28.6678214,45.98205 16.601069,11.28577 30.572578,19.06546 43.995171,26.11326 z" />
35
- <path
36
- fill="#92400e"
37
- d="m 213.6921,3.069361 c 0,0 0.96049,32.353598 -0.14192,43.494212 -1.10241,11.140614 -14.06146,22.120287 -23.28179,26.493068 -26.78003,12.700542 -124.035721,55.394599 -124.035721,55.394599 0,0 -7.314493,3.8005 -14.852175,9.23243 -7.537682,5.43193 -23.670324,18.71663 -28.098707,33.71845 -1.670439,5.65887 -2.408461,14.04935 -2.408461,14.04935 C 8.2528446,173.99227 -4.2995876,149.42142 8.2289074,120.92479 18.129451,103.32842 31.24605,87.051456 55.796006,75.465101 101.11959,52.013746 150.99666,33.115884 198.69801,12.042303 Z" />
38
- <g>
39
- <path
40
- fill="#fbbf24"
41
- d="m 157.01648,44.42498 c 14.17698,-5.71484 11.14517,-6.35429 11.1893,10.23596 -0.40601,13.28313 2.27097,11.17087 -9.33278,16.05641 -13.29184,4.92069 -12.50077,7.73152 -12.14266,-9.03173 -0.29088,-15.61229 -1.75968,-11.39786 10.28614,-17.26064 z" />
42
- <path
43
- fill="#fbbf24"
44
- d="m 118.47741,61.789741 c 14.17699,-5.71484 11.14518,-6.35429 11.18931,10.23596 -0.40601,13.28313 2.27097,11.17087 -9.33279,16.05641 -13.29183,4.92069 -12.50076,7.73152 -12.14265,-9.03173 -0.29089,-15.61229 -1.75968,-11.39786 10.28613,-17.26064 z" />
45
- <path
46
- fill="#fbbf24"
47
- d="m 80.42697,79.119631 c 14.176988,-5.71484 11.145175,-6.35429 11.189307,10.23596 -0.406014,13.283129 2.27097,11.170869 -9.332786,16.056409 -13.291838,4.92069 -12.500765,7.73152 -12.142656,-9.031729 -0.290884,-15.61229 -1.759682,-11.39786 10.286135,-17.26064 z" />
48
- </g>
49
- </g>
50
- <g transform="rotate(180,118.87578,165.37123)">
51
- <path
52
- fill="#502308"
53
- d="M 76.35291,222.53041 C 75.478969,209.22098 71.352034,194.29907 56.664655,177.24787 35.352192,152.22995 46.850499,139.85601 62.978003,130.13046 L 3.6899176,150.4351 c 5.0786411,20.05339 13.9650544,36.08746 28.6678214,45.98205 16.601069,11.28577 30.572578,19.06546 43.995171,26.11326 z" />
54
- <path
55
- fill="#92400e"
56
- d="m 213.6921,3.069361 c 0,0 0.96049,32.353598 -0.14192,43.494212 -1.10241,11.140614 -14.06146,22.120287 -23.28179,26.493068 -26.78003,12.700542 -124.035721,55.394599 -124.035721,55.394599 0,0 -7.314493,3.8005 -14.852175,9.23243 -7.537682,5.43193 -23.670324,18.71663 -28.098707,33.71845 -1.670439,5.65887 -2.408461,14.04935 -2.408461,14.04935 C 8.2528446,173.99227 -4.2995876,149.42142 8.2289074,120.92479 18.129451,103.32842 31.24605,87.051456 55.796006,75.465101 101.11959,52.013746 150.99666,33.115884 198.69801,12.042303 Z" />
57
- <g>
58
- <path
59
- fill="#fbbf24"
60
- d="m 157.01648,44.42498 c 14.17698,-5.71484 11.14517,-6.35429 11.1893,10.23596 -0.40601,13.28313 2.27097,11.17087 -9.33278,16.05641 -13.29184,4.92069 -12.50077,7.73152 -12.14266,-9.03173 -0.29088,-15.61229 -1.75968,-11.39786 10.28614,-17.26064 z" />
61
- <path
62
- fill="#fbbf24"
63
- d="m 118.47741,61.789741 c 14.17699,-5.71484 11.14518,-6.35429 11.18931,10.23596 -0.40601,13.28313 2.27097,11.17087 -9.33279,16.05641 -13.29183,4.92069 -12.50076,7.73152 -12.14265,-9.03173 -0.29089,-15.61229 -1.75968,-11.39786 10.28613,-17.26064 z" />
64
- <path
65
- fill="#fbbf24"
66
- d="m 80.42697,79.119631 c 14.176988,-5.71484 11.145175,-6.35429 11.189307,10.23596 -0.406014,13.283129 2.27097,11.170869 -9.332786,16.056409 -13.291838,4.92069 -12.500765,7.73152 -12.142656,-9.031729 -0.290884,-15.61229 -1.759682,-11.39786 10.286135,-17.26064 z" />
67
- </g>
68
- </g>
69
- <g transform="matrix(1.1983554,-0.06280315,0.06280315,1.1983554,-34.773977,-29.239318)">
70
- <path
71
- fill="#fbbf24"
72
- d="m 90.317275,164.77885 48.369915,-17.86274 c -6.75724,-6.27184 -19.73649,-10.33577 -32.1128,-4.41552 -11.797753,5.74748 -14.462218,13.89845 -16.257115,22.27826 z" />
73
- <path
74
- fill="#fbbf24"
75
- d="M 148.18495,171.26956 99.815045,189.1323 c 6.757235,6.27184 19.736485,10.33577 32.112795,4.41552 11.79775,-5.74748 14.46221,-13.89845 16.25711,-22.27826 z" />
76
- <path
77
- fill="#fbbf24"
78
- d="m 90.417628,169.79648 51.882262,-19.36804 c 3.81102,4.8999 5.68801,10.3205 6.02115,16.15676 l -52.032789,19.06698 c -3.269689,-4.15996 -5.408353,-9.28938 -5.870623,-15.8557 z" />
79
- </g>
80
- </g>
81
- </svg>
82
- {showText && (
83
- <span className="cs-brand-text" style={{ fontWeight: 700, fontSize: '11px', letterSpacing: '1.2px', textTransform: 'uppercase', color: 'var(--cs-ui-accent-badge, #d97706)' }}>
84
- Sutrata
85
- </span>
86
- )}
87
- </div>
88
- )
89
- }
@@ -1,46 +0,0 @@
1
- import React, { useEffect } from 'react'
2
-
3
- export interface ToastItem {
4
- id: string
5
- message: string
6
- type?: 'info' | 'success' | 'warn' | 'error'
7
- }
8
-
9
- interface ToastContainerProps {
10
- toasts: ToastItem[]
11
- onDismiss: (id: string) => void
12
- }
13
-
14
- export function ToastContainer({ toasts, onDismiss }: ToastContainerProps) {
15
- return (
16
- <div className="cs-toast-container" aria-live="polite" aria-atomic="true">
17
- {toasts.map((toast) => (
18
- <ToastMessage key={toast.id} toast={toast} onDismiss={onDismiss} />
19
- ))}
20
- </div>
21
- )
22
- }
23
-
24
- function ToastMessage({ toast, onDismiss }: { toast: ToastItem; onDismiss: (id: string) => void }) {
25
- useEffect(() => {
26
- const timer = setTimeout(() => {
27
- onDismiss(toast.id)
28
- }, 3500)
29
- return () => clearTimeout(timer)
30
- }, [toast.id, onDismiss])
31
-
32
- const typeClass =
33
- toast.type === 'success'
34
- ? 'cs-toast-success'
35
- : toast.type === 'error'
36
- ? 'cs-toast-error'
37
- : toast.type === 'warn'
38
- ? 'cs-toast-warn'
39
- : 'cs-toast-info'
40
-
41
- return (
42
- <div className={`cs-toast ${typeClass}`} role="status">
43
- <span>{toast.message}</span>
44
- </div>
45
- )
46
- }
@@ -1,30 +0,0 @@
1
- import React from 'react'
2
-
3
- interface ToolbarButtonProps {
4
- label: string
5
- sigil?: string
6
- shortcut?: string
7
- active?: boolean
8
- onClick: () => void
9
- children: React.ReactNode
10
- }
11
-
12
- export function ToolbarButton({ label, sigil, shortcut, active, onClick, children }: ToolbarButtonProps) {
13
- return (
14
- <button
15
- type="button"
16
- aria-label={label}
17
- className={`cs-tb-btn${active ? ' cs-tb-active' : ''}`}
18
- onClick={onClick}
19
- >
20
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
21
- {children}
22
- </div>
23
- <span className="cs-tb-tip" role="tooltip">
24
- <span>{label}</span>
25
- {sigil ? <span className="cs-tb-sigil">{sigil}</span> : null}
26
- {shortcut ? <span className="cs-tb-key">{shortcut}</span> : null}
27
- </span>
28
- </button>
29
- )
30
- }
@@ -1,212 +0,0 @@
1
- import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'
2
- import { diffWordsWithSpace, type Change } from 'diff'
3
- import type { VersionEntry } from '../types'
4
-
5
- interface VersionHistoryProps {
6
- versions: VersionEntry[]
7
- filePath: string | null
8
- onRestore: (entry: VersionEntry) => void
9
- onClearAll: () => Promise<void>
10
- visible: boolean
11
- onClose: () => void
12
- }
13
-
14
- interface DiffMarker {
15
- type: 'added' | 'removed'
16
- position: number // 0..1, fraction of the diff pane's scrollHeight
17
- }
18
-
19
- // Word-level diff over the full text (not line-bucketed): if only one word in
20
- // a sentence changed, only that word is marked added/removed — the rest of
21
- // the sentence comes back as an unchanged, unstyled Change chunk. Whitespace
22
- // (including newlines) is treated as part of the diff so line breaks survive.
23
- function computeDiff(oldText: string, newText: string): Change[] {
24
- return diffWordsWithSpace(oldText, newText)
25
- }
26
-
27
- // Two markers within this many pixels of each other collapse into one, so a
28
- // tight run of word edits doesn't paint a solid wall of ticks.
29
- const CLUSTER_PX = 10
30
-
31
- function formatTimestamp(ts: number): string {
32
- return new Date(ts).toLocaleString(undefined, {
33
- month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit',
34
- })
35
- }
36
-
37
- export function VersionHistory({ versions, filePath, onRestore, onClearAll, visible, onClose }: VersionHistoryProps) {
38
- const [selectedId, setSelectedId] = useState<string | null>(null)
39
- const diffScrollRef = useRef<HTMLDivElement>(null)
40
- // Maps a diff-part index (only added/removed parts) to its rendered <span>,
41
- // so marker positions come from real layout instead of a text-based estimate —
42
- // screenplay format has wildly uneven character density per line (short
43
- // character cues and blank lines vs. long action paragraphs), which threw
44
- // off an earlier character-offset heuristic by a page or more.
45
- const spanRefMap = useRef(new Map<number, HTMLSpanElement>())
46
- const [markers, setMarkers] = useState<DiffMarker[]>([])
47
-
48
- const currentVersion = versions[0] ?? null
49
- const selectedVersion = useMemo(
50
- () => versions.find(v => v.id === selectedId) ?? null,
51
- [versions, selectedId],
52
- )
53
-
54
- const diff = useMemo(() => {
55
- if (!selectedVersion || !currentVersion || selectedVersion.id === currentVersion.id) return null
56
- return computeDiff(selectedVersion.content, currentVersion.content)
57
- }, [selectedVersion, currentVersion])
58
-
59
- useLayoutEffect(() => {
60
- const container = diffScrollRef.current
61
- if (!container || !diff) { setMarkers([]); return }
62
-
63
- const raw: { type: 'added' | 'removed'; top: number }[] = []
64
- diff.forEach((part, i) => {
65
- if (!part.added && !part.removed) return
66
- const span = spanRefMap.current.get(i)
67
- if (!span) return
68
- raw.push({ type: part.added ? 'added' : 'removed', top: span.offsetTop })
69
- })
70
- raw.sort((a, b) => a.top - b.top)
71
-
72
- const scrollHeight = container.scrollHeight || 1
73
- const clustered: DiffMarker[] = []
74
- let lastTop = -Infinity
75
- let lastType: 'added' | 'removed' | null = null
76
- for (const m of raw) {
77
- if (lastType === m.type && m.top - lastTop < CLUSTER_PX) continue
78
- clustered.push({ type: m.type, position: m.top / scrollHeight })
79
- lastTop = m.top
80
- lastType = m.type
81
- }
82
- setMarkers(clustered)
83
- }, [diff])
84
-
85
- const scrollToMarker = (position: number) => {
86
- const el = diffScrollRef.current
87
- if (!el) return
88
- const target = Math.max(0, position * el.scrollHeight - el.clientHeight / 2)
89
- if (typeof el.scrollTo === 'function') el.scrollTo({ top: target, behavior: 'smooth' })
90
- else el.scrollTop = target
91
- }
92
-
93
- if (!visible || !filePath) return null
94
-
95
- return (
96
- <div
97
- className="cs-dialog-overlay"
98
- role="presentation"
99
- onClick={e => { if (e.target === e.currentTarget) onClose() }}
100
- >
101
- <div
102
- className="cs-dialog"
103
- role="dialog"
104
- aria-modal="true"
105
- aria-labelledby="cs-version-dialog-title"
106
- style={{ maxWidth: '620px', width: '92%', maxHeight: '80vh', display: 'flex', flexDirection: 'column' }}
107
- >
108
- <div className="cs-panel-header">
109
- <span id="cs-version-dialog-title" className="cs-panel-title">Version History</span>
110
- <button className="cs-panel-close" onClick={onClose} aria-label="Close">×</button>
111
- </div>
112
-
113
- <div className="cs-version-body">
114
- <div className="cs-version-list">
115
- {versions.length === 0 ? (
116
- <div className="cs-version-empty">No versions saved yet</div>
117
- ) : (
118
- versions.map((v, idx) => (
119
- <button
120
- key={v.id}
121
- type="button"
122
- className={`cs-version-list-item${selectedId === v.id ? ' cs-version-list-item-active' : ''}`}
123
- onClick={() => setSelectedId(v.id)}
124
- >
125
- <div className="cs-version-list-item-label">{idx === 0 ? 'Current' : formatTimestamp(v.timestamp)}</div>
126
- {idx !== 0 && <div className="cs-version-list-item-date">{formatTimestamp(v.timestamp)}</div>}
127
- </button>
128
- ))
129
- )}
130
- </div>
131
-
132
- <div className="cs-version-diff-wrap">
133
- <div className="cs-version-diff" ref={diffScrollRef}>
134
- {!selectedVersion ? (
135
- <div className="cs-version-empty">Select a version to compare against the current document</div>
136
- ) : selectedVersion.id === currentVersion?.id ? (
137
- <div className="cs-version-empty">This is the current version</div>
138
- ) : diff ? (
139
- diff.every(part => !part.added && !part.removed) ? (
140
- <div className="cs-version-empty">No differences from the current version</div>
141
- ) : (
142
- <div className="cs-version-diff-content">
143
- {diff.map((part, i) => {
144
- const isChange = part.added || part.removed
145
- return (
146
- <span
147
- key={i}
148
- ref={isChange ? (el) => {
149
- if (el) spanRefMap.current.set(i, el)
150
- else spanRefMap.current.delete(i)
151
- } : undefined}
152
- className={
153
- part.added ? 'cs-version-diff-added'
154
- : part.removed ? 'cs-version-diff-removed'
155
- : undefined
156
- }
157
- >
158
- {part.value}
159
- </span>
160
- )
161
- })}
162
- </div>
163
- )
164
- ) : null}
165
- </div>
166
-
167
- {/* Rendered unconditionally (even with zero markers) so its width is
168
- reserved from the diff pane's very first layout pass — otherwise
169
- measuring offsetTop before this strip exists, then having it pop
170
- in afterward and narrow the text column, would make every
171
- position stale by however much the text rewrapped. */}
172
- <div className="cs-version-diff-minimap" aria-hidden="true">
173
- {markers.map((m, i) => (
174
- <button
175
- key={i}
176
- type="button"
177
- className={`cs-version-diff-marker cs-version-diff-marker-${m.type}`}
178
- style={{ top: `${m.position * 100}%` }}
179
- title={m.type === 'added' ? 'Jump to added text' : 'Jump to removed text'}
180
- onClick={() => scrollToMarker(m.position)}
181
- />
182
- ))}
183
- </div>
184
- </div>
185
- </div>
186
-
187
- <div className="cs-version-footer">
188
- <button
189
- type="button"
190
- className="cs-confirm-btn-cancel cs-version-clear-btn"
191
- onClick={() => void onClearAll()}
192
- disabled={versions.length === 0}
193
- >
194
- Delete Version History
195
- </button>
196
- <div style={{ display: 'flex', gap: '10px' }}>
197
- <button type="button" className="cs-confirm-btn-cancel" onClick={onClose}>Close</button>
198
- {selectedVersion && selectedVersion.id !== currentVersion?.id && (
199
- <button
200
- type="button"
201
- className="cs-confirm-btn-primary"
202
- onClick={() => onRestore(selectedVersion)}
203
- >
204
- Restore This Version
205
- </button>
206
- )}
207
- </div>
208
- </div>
209
- </div>
210
- </div>
211
- )
212
- }
@@ -1,9 +0,0 @@
1
- import { useDocument } from '../context/DocumentContext'
2
-
3
- /** Renders the document's frontmatter `watermark:` text.
4
- * Hidden in the screen editor UI, visible only in print output. */
5
- export function Watermark() {
6
- const { watermarkText } = useDocument()
7
- if (!watermarkText) return null
8
- return <div className="cs-watermark" aria-hidden="true">{watermarkText}</div>
9
- }