@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.
- package/README.md +87 -0
- package/dist/ai/generated-providers.json +1 -1
- package/package.json +30 -3
- package/scripts/copy-assets.mjs +0 -16
- package/scripts/generate-providers.mjs +0 -127
- package/src/ai/OnboardingDialog.tsx +0 -390
- package/src/ai/VoiceConfirmDialog.tsx +0 -137
- package/src/ai/VoiceToolbar.tsx +0 -321
- package/src/ai/ai-client.ts +0 -596
- package/src/ai/commands.ts +0 -164
- package/src/ai/generated-providers.json +0 -3696
- package/src/ai/prompts.ts +0 -62
- package/src/ai/voice-service.ts +0 -96
- package/src/autocomplete/AutocompletePlugin.ts +0 -21
- package/src/autocomplete/suggestions.ts +0 -147
- package/src/context/DocumentContext.tsx +0 -648
- package/src/desktop-api.d.ts +0 -40
- package/src/editor/EditorView.tsx +0 -82
- package/src/editor/SelectionMenu.tsx +0 -152
- package/src/editor/attr-pencil-plugin.ts +0 -172
- package/src/editor/character-decoration-plugin.ts +0 -104
- package/src/editor/editor-bus.ts +0 -51
- package/src/editor/find-highlight-plugin.ts +0 -68
- package/src/editor/frontmatter-field-view.ts +0 -52
- package/src/editor/frontmatter-field.ts +0 -115
- package/src/editor/grapheme-cursor.ts +0 -99
- package/src/editor/ime-plugin.ts +0 -85
- package/src/editor/insert-helper.ts +0 -43
- package/src/editor/keymap.ts +0 -124
- package/src/editor/language-class-plugin.ts +0 -91
- package/src/editor/plugins.ts +0 -26
- package/src/editor/prosemirror-to-sutra.ts +0 -192
- package/src/editor/scene-heading-view.ts +0 -147
- package/src/editor/schema.ts +0 -156
- package/src/editor/sutra-to-prosemirror.ts +0 -156
- package/src/extensions/storage-adapter.ts +0 -65
- package/src/extensions/types.ts +0 -60
- package/src/file/docx-exporter.ts +0 -585
- package/src/file/docx-importer.ts +0 -313
- package/src/file/print-paginator.ts +0 -301
- package/src/file/templates.ts +0 -73
- package/src/file/workflow-reports.ts +0 -820
- package/src/findreplace/FindReplace.tsx +0 -242
- package/src/findreplace/find-engine.ts +0 -97
- package/src/findreplace/scroll-to-match.ts +0 -89
- package/src/fonts/tofu-detect.ts +0 -53
- package/src/i18n/LanguageContext.tsx +0 -207
- package/src/i18n/language-detect.ts +0 -73
- package/src/i18n/useTranslation.tsx +0 -74
- package/src/index.ts +0 -27
- package/src/locales/bn.json +0 -143
- package/src/locales/en.json +0 -143
- package/src/locales/gu.json +0 -143
- package/src/locales/hi.json +0 -143
- package/src/locales/kn.json +0 -143
- package/src/locales/ml.json +0 -143
- package/src/locales/mr.json +0 -143
- package/src/locales/or.json +0 -143
- package/src/locales/pa.json +0 -143
- package/src/locales/si.json +0 -143
- package/src/locales/ta.json +0 -143
- package/src/locales/te.json +0 -143
- package/src/navigator/SceneNavigator.tsx +0 -844
- package/src/navigator/StatisticsDialog.tsx +0 -249
- package/src/navigator/scene-list.ts +0 -84
- package/src/settings/SettingsDialog.tsx +0 -303
- package/src/shell/AppBar.tsx +0 -354
- package/src/shell/AppShell.tsx +0 -71
- package/src/shell/ConfirmDialog.tsx +0 -71
- package/src/shell/ElementToolbar.tsx +0 -175
- package/src/shell/LanguageStatus.tsx +0 -101
- package/src/shell/StatusBar.tsx +0 -135
- package/src/shell/SutrataLogo.tsx +0 -89
- package/src/shell/Toast.tsx +0 -46
- package/src/shell/ToolbarButton.tsx +0 -30
- package/src/shell/VersionHistory.tsx +0 -212
- package/src/shell/Watermark.tsx +0 -9
- package/src/shell/fonts/fonts.css +0 -1133
- package/src/shell/icons.tsx +0 -335
- package/src/shell/toolbar-actions.ts +0 -59
- package/src/source/SourceView.tsx +0 -111
- package/src/source/find-highlight-source.ts +0 -57
- package/src/source/sutra-lang.ts +0 -121
- package/src/spellcheck/dictionaries.ts +0 -21
- package/src/spellcheck/hunspell.ts +0 -110
- package/src/spellcheck/spellcheck-plugin.ts +0 -83
- package/src/styles/StyleDialog.tsx +0 -148
- package/src/styles/builtin-styles.ts +0 -139
- package/src/styles/css-adapter.ts +0 -76
- package/src/styles/docx-adapter.ts +0 -109
- package/src/styles/embed-fonts.ts +0 -81
- package/src/styles/pagination.ts +0 -27
- package/src/styles/print-adapter.ts +0 -288
- package/src/styles/registry.ts +0 -20
- package/src/styles/screenplay.css +0 -3006
- package/src/styles/script-ranges.ts +0 -17
- package/src/styles/types.ts +0 -104
- package/src/styles/validate.ts +0 -114
- package/src/templates/templates.ts +0 -194
- package/src/titlepage/ExportDialog.tsx +0 -494
- package/src/titlepage/TitlePageDialog.tsx +0 -312
- package/src/titlepage/frontmatter-form.ts +0 -280
- package/src/titlepage/languages.ts +0 -37
- package/src/types.ts +0 -7
- package/tests/AppBar.test.tsx +0 -35
- package/tests/AppShell.test.tsx +0 -22
- package/tests/ElementToolbar.test.tsx +0 -36
- package/tests/LanguageStatus.test.tsx +0 -129
- package/tests/SelectionMenu.test.tsx +0 -71
- package/tests/ToolbarButton.test.tsx +0 -24
- package/tests/ai-onboarding.test.ts +0 -102
- package/tests/ai-status.test.tsx +0 -72
- package/tests/ai-structured.test.ts +0 -94
- package/tests/autocomplete/AutocompletePlugin.test.ts +0 -20
- package/tests/autocomplete/suggestions.test.ts +0 -106
- package/tests/document-context.test.tsx +0 -565
- package/tests/docx-importer.test.ts +0 -160
- package/tests/editor/character-decoration-plugin.test.ts +0 -101
- package/tests/editor/character-extension.test.ts +0 -44
- package/tests/editor/grapheme-cursor.test.ts +0 -118
- package/tests/editor/ime-plugin.test.ts +0 -72
- package/tests/editor/inline-note-bridge.test.ts +0 -32
- package/tests/editor/language-class-plugin.test.ts +0 -265
- package/tests/editor/title-page.test.ts +0 -124
- package/tests/embed-fonts.test.ts +0 -17
- package/tests/exporters.test.ts +0 -365
- package/tests/findreplace/FindReplace.test.tsx +0 -391
- package/tests/findreplace/find-engine.test.ts +0 -86
- package/tests/findreplace/scroll-to-match.test.ts +0 -50
- package/tests/fonts/tofu-detect.test.ts +0 -122
- package/tests/i18n/LanguageContext.test.tsx +0 -146
- package/tests/i18n/language-detect.test.ts +0 -54
- package/tests/i18n/locale-parity.test.ts +0 -25
- package/tests/i18n/useTranslation.test.tsx +0 -45
- package/tests/keymap.test.ts +0 -18
- package/tests/navigator/SceneNavigator.test.tsx +0 -177
- package/tests/navigator/StatisticsDialog.test.tsx +0 -63
- package/tests/navigator/scene-list.test.ts +0 -71
- package/tests/print-paginator.test.ts +0 -82
- package/tests/shell/VersionHistory.test.tsx +0 -129
- package/tests/spellcheck/hunspell.test.ts +0 -82
- package/tests/spellcheck/spellcheck-plugin.test.ts +0 -56
- package/tests/styles.test.ts +0 -315
- package/tests/templates/templates.test.ts +0 -83
- package/tests/titlepage/TitlePageDialog.test.tsx +0 -90
- package/tests/titlepage/apply-title-page.test.ts +0 -61
- package/tests/titlepage/frontmatter-form.test.ts +0 -115
- package/tests/toolbar-actions.test.ts +0 -93
- package/tests/voice-commands.test.ts +0 -58
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -11
- 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
|
-
}
|
package/src/shell/StatusBar.tsx
DELETED
|
@@ -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
|
-
}
|
package/src/shell/Toast.tsx
DELETED
|
@@ -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
|
-
}
|
package/src/shell/Watermark.tsx
DELETED
|
@@ -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
|
-
}
|