@sutrata/editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/dist/ai/OnboardingDialog.d.ts +6 -0
- package/dist/ai/OnboardingDialog.js +115 -0
- package/dist/ai/VoiceConfirmDialog.d.ts +9 -0
- package/dist/ai/VoiceConfirmDialog.js +37 -0
- package/dist/ai/VoiceToolbar.d.ts +2 -0
- package/dist/ai/VoiceToolbar.js +177 -0
- package/dist/ai/ai-client.d.ts +71 -0
- package/dist/ai/ai-client.js +494 -0
- package/dist/ai/commands.d.ts +12 -0
- package/dist/ai/commands.js +140 -0
- package/dist/ai/generated-providers.json +3696 -0
- package/dist/ai/prompts.d.ts +25 -0
- package/dist/ai/prompts.js +59 -0
- package/dist/ai/voice-service.d.ts +15 -0
- package/dist/ai/voice-service.js +81 -0
- package/dist/autocomplete/AutocompletePlugin.d.ts +2 -0
- package/dist/autocomplete/AutocompletePlugin.js +19 -0
- package/dist/autocomplete/suggestions.d.ts +25 -0
- package/dist/autocomplete/suggestions.js +126 -0
- package/dist/context/DocumentContext.d.ts +68 -0
- package/dist/context/DocumentContext.js +605 -0
- package/dist/desktop-api.d.ts +40 -0
- package/dist/editor/EditorView.d.ts +2 -0
- package/dist/editor/EditorView.js +78 -0
- package/dist/editor/SelectionMenu.d.ts +2 -0
- package/dist/editor/SelectionMenu.js +102 -0
- package/dist/editor/attr-pencil-plugin.d.ts +17 -0
- package/dist/editor/attr-pencil-plugin.js +158 -0
- package/dist/editor/character-decoration-plugin.d.ts +26 -0
- package/dist/editor/character-decoration-plugin.js +77 -0
- package/dist/editor/character-view.d.ts +35 -0
- package/dist/editor/character-view.js +112 -0
- package/dist/editor/editor-bus.d.ts +14 -0
- package/dist/editor/editor-bus.js +39 -0
- package/dist/editor/find-highlight-plugin.d.ts +7 -0
- package/dist/editor/find-highlight-plugin.js +57 -0
- package/dist/editor/frontmatter-field-view.d.ts +10 -0
- package/dist/editor/frontmatter-field-view.js +48 -0
- package/dist/editor/frontmatter-field.d.ts +21 -0
- package/dist/editor/frontmatter-field.js +116 -0
- package/dist/editor/grapheme-cursor.d.ts +10 -0
- package/dist/editor/grapheme-cursor.js +104 -0
- package/dist/editor/ime-plugin.d.ts +18 -0
- package/dist/editor/ime-plugin.js +71 -0
- package/dist/editor/insert-helper.d.ts +1 -0
- package/dist/editor/insert-helper.js +42 -0
- package/dist/editor/keymap.d.ts +3 -0
- package/dist/editor/keymap.js +131 -0
- package/dist/editor/language-class-plugin.d.ts +7 -0
- package/dist/editor/language-class-plugin.js +75 -0
- package/dist/editor/plugins.d.ts +1 -0
- package/dist/editor/plugins.js +25 -0
- package/dist/editor/prosemirror-to-sutra.d.ts +2 -0
- package/dist/editor/prosemirror-to-sutra.js +208 -0
- package/dist/editor/scene-heading-view.d.ts +27 -0
- package/dist/editor/scene-heading-view.js +136 -0
- package/dist/editor/schema.d.ts +3 -0
- package/dist/editor/schema.js +135 -0
- package/dist/editor/sutra-to-prosemirror.d.ts +3 -0
- package/dist/editor/sutra-to-prosemirror.js +154 -0
- package/dist/editor/transliterate-input.d.ts +10 -0
- package/dist/editor/transliterate-input.js +88 -0
- package/dist/extensions/storage-adapter.d.ts +59 -0
- package/dist/extensions/storage-adapter.js +9 -0
- package/dist/extensions/types.d.ts +73 -0
- package/dist/extensions/types.js +12 -0
- package/dist/file/docx-exporter.d.ts +8 -0
- package/dist/file/docx-exporter.js +494 -0
- package/dist/file/docx-importer.d.ts +10 -0
- package/dist/file/docx-importer.js +283 -0
- package/dist/file/print-paginator.d.ts +29 -0
- package/dist/file/print-paginator.js +289 -0
- package/dist/file/templates.d.ts +2 -0
- package/dist/file/templates.js +66 -0
- package/dist/file/workflow-reports.d.ts +34 -0
- package/dist/file/workflow-reports.js +719 -0
- package/dist/findreplace/FindReplace.d.ts +2 -0
- package/dist/findreplace/FindReplace.js +164 -0
- package/dist/findreplace/find-engine.d.ts +29 -0
- package/dist/findreplace/find-engine.js +69 -0
- package/dist/findreplace/scroll-to-match.d.ts +29 -0
- package/dist/findreplace/scroll-to-match.js +74 -0
- package/dist/fonts/tofu-detect.d.ts +1 -0
- package/dist/fonts/tofu-detect.js +48 -0
- package/dist/i18n/LanguageContext.d.ts +10 -0
- package/dist/i18n/LanguageContext.js +174 -0
- package/dist/i18n/language-detect.d.ts +1 -0
- package/dist/i18n/language-detect.js +58 -0
- package/dist/i18n/useTranslation.d.ts +11 -0
- package/dist/i18n/useTranslation.js +60 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +5 -0
- package/dist/locales/bn.json +143 -0
- package/dist/locales/en.json +143 -0
- package/dist/locales/gu.json +143 -0
- package/dist/locales/hi.json +143 -0
- package/dist/locales/kn.json +143 -0
- package/dist/locales/ml.json +143 -0
- package/dist/locales/mr.json +143 -0
- package/dist/locales/or.json +143 -0
- package/dist/locales/pa.json +143 -0
- package/dist/locales/si.json +143 -0
- package/dist/locales/ta.json +143 -0
- package/dist/locales/te.json +143 -0
- package/dist/navigator/SceneNavigator.d.ts +2 -0
- package/dist/navigator/SceneNavigator.js +612 -0
- package/dist/navigator/StatisticsDialog.d.ts +6 -0
- package/dist/navigator/StatisticsDialog.js +146 -0
- package/dist/navigator/scene-list.d.ts +16 -0
- package/dist/navigator/scene-list.js +70 -0
- package/dist/settings/SettingsDialog.d.ts +6 -0
- package/dist/settings/SettingsDialog.js +147 -0
- package/dist/shell/AppBar.d.ts +2 -0
- package/dist/shell/AppBar.js +34 -0
- package/dist/shell/AppShell.d.ts +2 -0
- package/dist/shell/AppShell.js +30 -0
- package/dist/shell/ConfirmDialog.d.ts +16 -0
- package/dist/shell/ConfirmDialog.js +23 -0
- package/dist/shell/ElementToolbar.d.ts +2 -0
- package/dist/shell/ElementToolbar.js +30 -0
- package/dist/shell/LanguageStatus.d.ts +2 -0
- package/dist/shell/LanguageStatus.js +65 -0
- package/dist/shell/StatusBar.d.ts +2 -0
- package/dist/shell/StatusBar.js +54 -0
- package/dist/shell/SutrataLogo.d.ts +8 -0
- package/dist/shell/SutrataLogo.js +7 -0
- package/dist/shell/Toast.d.ts +12 -0
- package/dist/shell/Toast.js +21 -0
- package/dist/shell/ToolbarButton.d.ts +11 -0
- package/dist/shell/ToolbarButton.js +4 -0
- package/dist/shell/VersionHistory.d.ts +12 -0
- package/dist/shell/VersionHistory.js +89 -0
- package/dist/shell/Watermark.d.ts +3 -0
- package/dist/shell/Watermark.js +10 -0
- package/dist/shell/fonts/fonts.css +1133 -0
- package/dist/shell/icons.d.ts +45 -0
- package/dist/shell/icons.js +51 -0
- package/dist/shell/toolbar-actions.d.ts +6 -0
- package/dist/shell/toolbar-actions.js +58 -0
- package/dist/source/SourceView.d.ts +1 -0
- package/dist/source/SourceView.js +106 -0
- package/dist/source/find-highlight-source.d.ts +14 -0
- package/dist/source/find-highlight-source.js +47 -0
- package/dist/source/sutra-lang.d.ts +2 -0
- package/dist/source/sutra-lang.js +97 -0
- package/dist/spellcheck/dictionaries.d.ts +9 -0
- package/dist/spellcheck/dictionaries.js +16 -0
- package/dist/spellcheck/hunspell.d.ts +23 -0
- package/dist/spellcheck/hunspell.js +94 -0
- package/dist/spellcheck/spellcheck-plugin.d.ts +18 -0
- package/dist/spellcheck/spellcheck-plugin.js +75 -0
- package/dist/styles/StyleDialog.d.ts +6 -0
- package/dist/styles/StyleDialog.js +63 -0
- package/dist/styles/builtin-styles.d.ts +43 -0
- package/dist/styles/builtin-styles.js +131 -0
- package/dist/styles/css-adapter.d.ts +32 -0
- package/dist/styles/css-adapter.js +71 -0
- package/dist/styles/docx-adapter.d.ts +7 -0
- package/dist/styles/docx-adapter.js +84 -0
- package/dist/styles/embed-fonts.d.ts +20 -0
- package/dist/styles/embed-fonts.js +81 -0
- package/dist/styles/pagination.d.ts +19 -0
- package/dist/styles/pagination.js +22 -0
- package/dist/styles/print-adapter.d.ts +6 -0
- package/dist/styles/print-adapter.js +288 -0
- package/dist/styles/registry.d.ts +6 -0
- package/dist/styles/registry.js +14 -0
- package/dist/styles/screenplay.css +3006 -0
- package/dist/styles/script-ranges.d.ts +6 -0
- package/dist/styles/script-ranges.js +17 -0
- package/dist/styles/types.d.ts +93 -0
- package/dist/styles/types.js +18 -0
- package/dist/styles/validate.d.ts +12 -0
- package/dist/styles/validate.js +105 -0
- package/dist/templates/templates.d.ts +10 -0
- package/dist/templates/templates.js +181 -0
- package/dist/titlepage/ExportDialog.d.ts +1 -0
- package/dist/titlepage/ExportDialog.js +152 -0
- package/dist/titlepage/TitlePageDialog.d.ts +6 -0
- package/dist/titlepage/TitlePageDialog.js +96 -0
- package/dist/titlepage/frontmatter-form.d.ts +56 -0
- package/dist/titlepage/frontmatter-form.js +245 -0
- package/dist/titlepage/languages.d.ts +10 -0
- package/dist/titlepage/languages.js +31 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +1 -0
- package/package.json +47 -0
- package/scripts/copy-assets.mjs +16 -0
- package/scripts/generate-providers.mjs +127 -0
- package/src/ai/OnboardingDialog.tsx +390 -0
- package/src/ai/VoiceConfirmDialog.tsx +137 -0
- package/src/ai/VoiceToolbar.tsx +321 -0
- package/src/ai/ai-client.ts +596 -0
- package/src/ai/commands.ts +164 -0
- package/src/ai/generated-providers.json +3696 -0
- package/src/ai/prompts.ts +62 -0
- package/src/ai/voice-service.ts +96 -0
- package/src/autocomplete/AutocompletePlugin.ts +21 -0
- package/src/autocomplete/suggestions.ts +147 -0
- package/src/context/DocumentContext.tsx +648 -0
- package/src/desktop-api.d.ts +40 -0
- package/src/editor/EditorView.tsx +82 -0
- package/src/editor/SelectionMenu.tsx +152 -0
- package/src/editor/attr-pencil-plugin.ts +172 -0
- package/src/editor/character-decoration-plugin.ts +104 -0
- package/src/editor/editor-bus.ts +51 -0
- package/src/editor/find-highlight-plugin.ts +68 -0
- package/src/editor/frontmatter-field-view.ts +52 -0
- package/src/editor/frontmatter-field.ts +115 -0
- package/src/editor/grapheme-cursor.ts +99 -0
- package/src/editor/ime-plugin.ts +85 -0
- package/src/editor/insert-helper.ts +43 -0
- package/src/editor/keymap.ts +124 -0
- package/src/editor/language-class-plugin.ts +91 -0
- package/src/editor/plugins.ts +26 -0
- package/src/editor/prosemirror-to-sutra.ts +192 -0
- package/src/editor/scene-heading-view.ts +147 -0
- package/src/editor/schema.ts +156 -0
- package/src/editor/sutra-to-prosemirror.ts +156 -0
- package/src/extensions/storage-adapter.ts +65 -0
- package/src/extensions/types.ts +60 -0
- package/src/file/docx-exporter.ts +585 -0
- package/src/file/docx-importer.ts +313 -0
- package/src/file/print-paginator.ts +301 -0
- package/src/file/templates.ts +73 -0
- package/src/file/workflow-reports.ts +820 -0
- package/src/findreplace/FindReplace.tsx +242 -0
- package/src/findreplace/find-engine.ts +97 -0
- package/src/findreplace/scroll-to-match.ts +89 -0
- package/src/fonts/tofu-detect.ts +53 -0
- package/src/i18n/LanguageContext.tsx +207 -0
- package/src/i18n/language-detect.ts +73 -0
- package/src/i18n/useTranslation.tsx +74 -0
- package/src/index.ts +27 -0
- package/src/locales/bn.json +143 -0
- package/src/locales/en.json +143 -0
- package/src/locales/gu.json +143 -0
- package/src/locales/hi.json +143 -0
- package/src/locales/kn.json +143 -0
- package/src/locales/ml.json +143 -0
- package/src/locales/mr.json +143 -0
- package/src/locales/or.json +143 -0
- package/src/locales/pa.json +143 -0
- package/src/locales/si.json +143 -0
- package/src/locales/ta.json +143 -0
- package/src/locales/te.json +143 -0
- package/src/navigator/SceneNavigator.tsx +844 -0
- package/src/navigator/StatisticsDialog.tsx +249 -0
- package/src/navigator/scene-list.ts +84 -0
- package/src/settings/SettingsDialog.tsx +303 -0
- package/src/shell/AppBar.tsx +354 -0
- package/src/shell/AppShell.tsx +71 -0
- package/src/shell/ConfirmDialog.tsx +71 -0
- package/src/shell/ElementToolbar.tsx +175 -0
- package/src/shell/LanguageStatus.tsx +101 -0
- package/src/shell/StatusBar.tsx +135 -0
- package/src/shell/SutrataLogo.tsx +89 -0
- package/src/shell/Toast.tsx +46 -0
- package/src/shell/ToolbarButton.tsx +30 -0
- package/src/shell/VersionHistory.tsx +212 -0
- package/src/shell/Watermark.tsx +9 -0
- package/src/shell/fonts/fonts.css +1133 -0
- package/src/shell/icons.tsx +335 -0
- package/src/shell/toolbar-actions.ts +59 -0
- package/src/source/SourceView.tsx +111 -0
- package/src/source/find-highlight-source.ts +57 -0
- package/src/source/sutra-lang.ts +121 -0
- package/src/spellcheck/dictionaries.ts +21 -0
- package/src/spellcheck/hunspell.ts +110 -0
- package/src/spellcheck/spellcheck-plugin.ts +83 -0
- package/src/styles/StyleDialog.tsx +148 -0
- package/src/styles/builtin-styles.ts +139 -0
- package/src/styles/css-adapter.ts +76 -0
- package/src/styles/docx-adapter.ts +109 -0
- package/src/styles/embed-fonts.ts +81 -0
- package/src/styles/pagination.ts +27 -0
- package/src/styles/print-adapter.ts +288 -0
- package/src/styles/registry.ts +20 -0
- package/src/styles/screenplay.css +3006 -0
- package/src/styles/script-ranges.ts +17 -0
- package/src/styles/types.ts +104 -0
- package/src/styles/validate.ts +114 -0
- package/src/templates/templates.ts +194 -0
- package/src/titlepage/ExportDialog.tsx +494 -0
- package/src/titlepage/TitlePageDialog.tsx +312 -0
- package/src/titlepage/frontmatter-form.ts +280 -0
- package/src/titlepage/languages.ts +37 -0
- package/src/types.ts +7 -0
- package/tests/AppBar.test.tsx +35 -0
- package/tests/AppShell.test.tsx +22 -0
- package/tests/ElementToolbar.test.tsx +36 -0
- package/tests/LanguageStatus.test.tsx +129 -0
- package/tests/SelectionMenu.test.tsx +71 -0
- package/tests/ToolbarButton.test.tsx +24 -0
- package/tests/ai-onboarding.test.ts +102 -0
- package/tests/ai-status.test.tsx +72 -0
- package/tests/ai-structured.test.ts +94 -0
- package/tests/autocomplete/AutocompletePlugin.test.ts +20 -0
- package/tests/autocomplete/suggestions.test.ts +106 -0
- package/tests/document-context.test.tsx +565 -0
- package/tests/docx-importer.test.ts +160 -0
- package/tests/editor/character-decoration-plugin.test.ts +101 -0
- package/tests/editor/character-extension.test.ts +44 -0
- package/tests/editor/grapheme-cursor.test.ts +118 -0
- package/tests/editor/ime-plugin.test.ts +72 -0
- package/tests/editor/inline-note-bridge.test.ts +32 -0
- package/tests/editor/language-class-plugin.test.ts +265 -0
- package/tests/editor/title-page.test.ts +124 -0
- package/tests/embed-fonts.test.ts +17 -0
- package/tests/exporters.test.ts +365 -0
- package/tests/findreplace/FindReplace.test.tsx +391 -0
- package/tests/findreplace/find-engine.test.ts +86 -0
- package/tests/findreplace/scroll-to-match.test.ts +50 -0
- package/tests/fonts/tofu-detect.test.ts +122 -0
- package/tests/i18n/LanguageContext.test.tsx +146 -0
- package/tests/i18n/language-detect.test.ts +54 -0
- package/tests/i18n/locale-parity.test.ts +25 -0
- package/tests/i18n/useTranslation.test.tsx +45 -0
- package/tests/keymap.test.ts +18 -0
- package/tests/navigator/SceneNavigator.test.tsx +177 -0
- package/tests/navigator/StatisticsDialog.test.tsx +63 -0
- package/tests/navigator/scene-list.test.ts +71 -0
- package/tests/print-paginator.test.ts +82 -0
- package/tests/shell/VersionHistory.test.tsx +129 -0
- package/tests/spellcheck/hunspell.test.ts +82 -0
- package/tests/spellcheck/spellcheck-plugin.test.ts +56 -0
- package/tests/styles.test.ts +315 -0
- package/tests/templates/templates.test.ts +83 -0
- package/tests/titlepage/TitlePageDialog.test.tsx +90 -0
- package/tests/titlepage/apply-title-page.test.ts +61 -0
- package/tests/titlepage/frontmatter-form.test.ts +115 -0
- package/tests/toolbar-actions.test.ts +93 -0
- package/tests/voice-commands.test.ts +58 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +11 -0
- package/vitest.setup.ts +65 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { useDocument } from '../context/DocumentContext';
|
|
4
|
+
import { useTranslation } from '../i18n/useTranslation';
|
|
5
|
+
import { allStyles, resolveStyle } from '../styles/registry';
|
|
6
|
+
import { getEditorView } from '../editor/editor-bus';
|
|
7
|
+
import { applyTitlePageText } from '../editor/frontmatter-field';
|
|
8
|
+
import { readTitlePage, writeTitlePage, MANAGED_KEYS } from './frontmatter-form';
|
|
9
|
+
import { languageOptions } from './languages';
|
|
10
|
+
export { OPEN_TITLE_PAGE_EVENT, openTitlePageForm } from '../editor/editor-bus';
|
|
11
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
12
|
+
const TABS = ['story', 'credits', 'draft', 'contact', 'document', 'other'];
|
|
13
|
+
function wordCount(s) {
|
|
14
|
+
return s.split(/\s+/).filter(Boolean).length;
|
|
15
|
+
}
|
|
16
|
+
export function TitlePageDialog({ onClose }) {
|
|
17
|
+
const { ast, text, setText, mode, customStyles, showToast } = useDocument();
|
|
18
|
+
const { t } = useTranslation();
|
|
19
|
+
const data = ast.frontmatter?.data;
|
|
20
|
+
const initial = useMemo(() => readTitlePage(data), []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
21
|
+
const [form, setForm] = useState(initial);
|
|
22
|
+
const firstFieldRef = useRef(null);
|
|
23
|
+
const [tab, setTab] = useState('story');
|
|
24
|
+
const tabRefs = useRef({});
|
|
25
|
+
/** Arrow keys move between tabs (vertical tab list; left/right also work). */
|
|
26
|
+
function onTabKey(e, current) {
|
|
27
|
+
const i = TABS.indexOf(current);
|
|
28
|
+
let next;
|
|
29
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowRight')
|
|
30
|
+
next = TABS[(i + 1) % TABS.length];
|
|
31
|
+
else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft')
|
|
32
|
+
next = TABS[(i - 1 + TABS.length) % TABS.length];
|
|
33
|
+
else if (e.key === 'Home')
|
|
34
|
+
next = TABS[0];
|
|
35
|
+
else if (e.key === 'End')
|
|
36
|
+
next = TABS[TABS.length - 1];
|
|
37
|
+
if (!next)
|
|
38
|
+
return;
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
setTab(next);
|
|
41
|
+
tabRefs.current[next]?.focus();
|
|
42
|
+
}
|
|
43
|
+
const set = (key, value) => setForm(f => ({ ...f, [key]: value }));
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
firstFieldRef.current?.focus();
|
|
46
|
+
const onKey = (e) => { if (e.key === 'Escape')
|
|
47
|
+
onClose(); };
|
|
48
|
+
document.addEventListener('keydown', onKey);
|
|
49
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
50
|
+
}, [onClose]);
|
|
51
|
+
const langs = languageOptions([
|
|
52
|
+
form.lang, ...form.langSecondary, ...form.altTitles.map(a => a.lang),
|
|
53
|
+
]);
|
|
54
|
+
const langLabel = (code) => langs.find(l => l.code === code)?.label ?? code;
|
|
55
|
+
const styles = allStyles(customStyles);
|
|
56
|
+
const defaultStyleName = resolveStyle(undefined, customStyles).name;
|
|
57
|
+
const logWords = wordCount(form.logline);
|
|
58
|
+
// Frontmatter keys the form neither manages nor shows as a custom row: kept as is.
|
|
59
|
+
const preservedCount = data
|
|
60
|
+
? Object.keys(data).filter(k => !MANAGED_KEYS.includes(k) &&
|
|
61
|
+
!initial.custom.some(c => c.key === k)).length
|
|
62
|
+
: 0;
|
|
63
|
+
function save() {
|
|
64
|
+
const current = readTitlePage(ast.frontmatter?.data);
|
|
65
|
+
const next = writeTitlePage(text, current, form);
|
|
66
|
+
if (next !== text) {
|
|
67
|
+
applyTitlePageText(mode === 'formatted' ? getEditorView() : null, next, setText);
|
|
68
|
+
showToast(t('titlePage.saved'), 'success');
|
|
69
|
+
}
|
|
70
|
+
onClose();
|
|
71
|
+
}
|
|
72
|
+
return (_jsx("div", { className: "cs-export-overlay", role: "presentation", onClick: e => { if (e.target === e.currentTarget)
|
|
73
|
+
onClose(); }, children: _jsxs("div", { className: "cs-export-dialog cs-tp-dialog", role: "dialog", "aria-modal": "true", "aria-labelledby": "cs-tp-title", children: [_jsxs("div", { className: "cs-panel-header", children: [_jsx("span", { id: "cs-tp-title", className: "cs-panel-title", children: t('titlePage.title') }), _jsx("button", { className: "cs-panel-close", onClick: onClose, "aria-label": t('titlePage.cancel'), children: "\u00D7" })] }), _jsxs("div", { className: "cs-tp-body", children: [_jsx("div", { className: "cs-tp-tabs", role: "tablist", "aria-orientation": "vertical", "aria-label": t('titlePage.title'), children: TABS.map(id => (_jsx("button", { ref: el => { tabRefs.current[id] = el; }, type: "button", role: "tab", id: `cs-tp-tab-${id}`, "aria-selected": tab === id, "aria-controls": `cs-tp-panel-${id}`, tabIndex: tab === id ? 0 : -1, className: `cs-tp-tab${tab === id ? ' cs-tp-tab-active' : ''}`, onClick: () => setTab(id), onKeyDown: e => onTabKey(e, id), children: t(`titlePage.section.${id}`) }, id))) }), _jsxs("form", { className: "cs-tp-form", onSubmit: e => { e.preventDefault(); save(); }, children: [_jsxs(Panel, { id: "story", active: tab, children: [_jsx(Field, { label: t('titlePage.field.title'), htmlFor: "cs-tp-f-title", children: _jsx("input", { id: "cs-tp-f-title", ref: firstFieldRef, className: "cs-tp-input", value: form.title, onChange: e => set('title', e.target.value) }) }), form.altTitles.map((alt, i) => (_jsxs("div", { className: "cs-tp-row", children: [_jsxs("select", { "aria-label": t('titlePage.field.titleLanguage'), className: "cs-settings-select cs-tp-lang-select", value: alt.lang, onChange: e => set('altTitles', form.altTitles.map((a, j) => j === i ? { ...a, lang: e.target.value } : a)), children: [_jsx("option", { value: "", children: t('titlePage.chooseLanguage') }), langs.map(l => _jsx("option", { value: l.code, children: l.label }, l.code))] }), _jsx("input", { "aria-label": t('titlePage.field.titleIn'), className: "cs-tp-input", value: alt.text, onChange: e => set('altTitles', form.altTitles.map((a, j) => j === i ? { ...a, text: e.target.value } : a)) }), _jsx("button", { type: "button", className: "cs-tp-remove", "aria-label": t('titlePage.remove'), onClick: () => set('altTitles', form.altTitles.filter((_, j) => j !== i)), children: "\u00D7" })] }, i))), _jsxs("button", { type: "button", className: "cs-tp-add", onClick: () => set('altTitles', [...form.altTitles, { lang: '', text: '' }]), children: ["+ ", t('titlePage.addTitle')] }), _jsx(Field, { label: t('titlePage.field.logline'), htmlFor: "cs-tp-f-logline", hint: _jsx("span", { className: logWords > 50 ? 'cs-tp-count-bad' : logWords > 30 ? 'cs-tp-count-warn' : 'cs-tp-count', children: t('titlePage.words').replace('{n}', String(logWords)) }), children: _jsx("textarea", { id: "cs-tp-f-logline", className: "cs-tp-input", rows: 2, value: form.logline, onChange: e => set('logline', e.target.value) }) })] }), _jsxs(Panel, { id: "credits", active: tab, children: [_jsx(TextField, { id: "author", label: t('titlePage.field.author'), value: form.author, onChange: v => set('author', v) }), _jsx(TextField, { id: "credit", label: t('titlePage.field.credit'), placeholder: "Written by", value: form.credit, onChange: v => set('credit', v) }), _jsx(TextField, { id: "source", label: t('titlePage.field.source'), value: form.source, onChange: v => set('source', v) }), _jsx(TextField, { id: "story", label: t('titlePage.field.story'), value: form.story, onChange: v => set('story', v) }), _jsx(TextField, { id: "screenplay", label: t('titlePage.field.screenplay'), value: form.screenplay, onChange: v => set('screenplay', v) }), _jsx(TextField, { id: "dialogue", label: t('titlePage.field.dialogue'), value: form.dialogue, onChange: v => set('dialogue', v) })] }), _jsxs(Panel, { id: "draft", active: tab, children: [_jsx(TextField, { id: "draft", label: t('titlePage.field.draft'), placeholder: "First Draft", value: form.draft, onChange: v => set('draft', v) }), _jsx(TextField, { id: "revision", label: t('titlePage.field.revision'), value: form.revision, onChange: v => set('revision', v) }), _jsx(Field, { label: t('titlePage.field.date'), htmlFor: "cs-tp-f-date", children: _jsx(DateInput, { id: "cs-tp-f-date", value: form.date, onChange: v => set('date', v), usePickerLabel: t('titlePage.usePicker'), useTextLabel: t('titlePage.useText') }) })] }), _jsxs(Panel, { id: "contact", active: tab, children: [_jsx(Field, { label: t('titlePage.field.contact'), htmlFor: "cs-tp-f-contact", children: _jsx("textarea", { id: "cs-tp-f-contact", className: "cs-tp-input", rows: 3, value: form.contact, onChange: e => set('contact', e.target.value) }) }), _jsx(TextField, { id: "copyright", label: t('titlePage.field.copyright'), placeholder: "\u00A9 2026 \u2026", value: form.copyright, onChange: v => set('copyright', v) })] }), _jsxs(Panel, { id: "document", active: tab, children: [_jsx(Field, { label: t('titlePage.field.lang'), htmlFor: "cs-tp-f-lang", children: _jsxs("select", { id: "cs-tp-f-lang", className: "cs-settings-select", value: form.lang, onChange: e => set('lang', e.target.value), children: [_jsx("option", { value: "", children: t('titlePage.notSet') }), langs.map(l => _jsx("option", { value: l.code, children: l.label }, l.code))] }) }), _jsx(Field, { label: t('titlePage.field.langSecondary'), htmlFor: "cs-tp-f-lang2", children: _jsxs("div", { className: "cs-tp-chips", children: [form.langSecondary.map(code => (_jsxs("span", { className: "cs-tp-chip", children: [langLabel(code), _jsx("button", { type: "button", "aria-label": `${t('titlePage.remove')} ${langLabel(code)}`, onClick: () => set('langSecondary', form.langSecondary.filter(c => c !== code)), children: "\u00D7" })] }, code))), _jsxs("select", { id: "cs-tp-f-lang2", className: "cs-settings-select", value: "", onChange: e => {
|
|
74
|
+
const code = e.target.value;
|
|
75
|
+
if (code && !form.langSecondary.includes(code))
|
|
76
|
+
set('langSecondary', [...form.langSecondary, code]);
|
|
77
|
+
}, children: [_jsx("option", { value: "", children: t('titlePage.addLanguage') }), langs.filter(l => l.code !== form.lang && !form.langSecondary.includes(l.code))
|
|
78
|
+
.map(l => _jsx("option", { value: l.code, children: l.label }, l.code))] })] }) }), _jsx(Field, { label: t('titlePage.field.page'), htmlFor: "cs-tp-f-page", children: _jsxs("select", { id: "cs-tp-f-page", className: "cs-settings-select", value: form.page, onChange: e => set('page', e.target.value), children: [_jsx("option", { value: "", children: t('titlePage.pageDefault') }), _jsx("option", { value: "A4", children: "A4" }), _jsx("option", { value: "Letter", children: "US Letter" })] }) }), _jsx(Field, { label: t('titlePage.field.style'), htmlFor: "cs-tp-f-style", children: _jsxs("select", { id: "cs-tp-f-style", className: "cs-settings-select", value: form.style, onChange: e => set('style', e.target.value), children: [_jsx("option", { value: "", children: t('titlePage.styleDefault').replace('{name}', defaultStyleName) }), styles.map(s => _jsx("option", { value: s.id, children: s.name }, s.id)), form.style && !styles.some(s => s.id === form.style) && (_jsx("option", { value: form.style, children: form.style }))] }) }), _jsx(TextField, { id: "watermark", label: t('titlePage.field.watermark'), placeholder: "CONFIDENTIAL", value: form.watermark, onChange: v => set('watermark', v) })] }), _jsxs(Panel, { id: "other", active: tab, children: [form.custom.map((c, i) => (_jsxs("div", { className: "cs-tp-row", children: [_jsx("input", { "aria-label": t('titlePage.field.key'), className: "cs-tp-input cs-tp-key", value: c.key, placeholder: "key", onChange: e => set('custom', form.custom.map((x, j) => j === i ? { ...x, key: e.target.value } : x)) }), _jsx("input", { "aria-label": t('titlePage.field.value'), className: "cs-tp-input", value: c.value, onChange: e => set('custom', form.custom.map((x, j) => j === i ? { ...x, value: e.target.value } : x)) }), _jsx("button", { type: "button", className: "cs-tp-remove", "aria-label": t('titlePage.remove'), onClick: () => set('custom', form.custom.filter((_, j) => j !== i)), children: "\u00D7" })] }, i))), _jsxs("button", { type: "button", className: "cs-tp-add", onClick: () => set('custom', [...form.custom, { key: '', value: '' }]), children: ["+ ", t('titlePage.addField')] }), preservedCount > 0 && (_jsx("p", { className: "cs-tp-note", children: t('titlePage.preserved').replace('{n}', String(preservedCount)) }))] })] })] }), _jsxs("div", { className: "cs-export-actions cs-tp-actions", children: [_jsx("button", { type: "button", className: "cs-confirm-btn-cancel", onClick: onClose, children: t('titlePage.cancel') }), _jsx("button", { type: "button", className: "cs-confirm-btn-primary", onClick: save, children: t('titlePage.save') })] })] }) }));
|
|
79
|
+
}
|
|
80
|
+
function Panel({ id, active, children }) {
|
|
81
|
+
return (_jsx("div", { role: "tabpanel", id: `cs-tp-panel-${id}`, "aria-labelledby": `cs-tp-tab-${id}`, className: "cs-tp-panel", hidden: active !== id, children: children }));
|
|
82
|
+
}
|
|
83
|
+
function Field({ label, htmlFor, hint, children }) {
|
|
84
|
+
return (_jsxs("div", { className: "cs-tp-field", children: [_jsxs("label", { className: "cs-tp-label", htmlFor: htmlFor, children: [label, hint ? _jsxs(_Fragment, { children: [" ", hint] }) : null] }), children] }));
|
|
85
|
+
}
|
|
86
|
+
function TextField({ id, label, value, placeholder, onChange }) {
|
|
87
|
+
return (_jsx(Field, { label: label, htmlFor: `cs-tp-f-${id}`, children: _jsx("input", { id: `cs-tp-f-${id}`, className: "cs-tp-input", value: value, placeholder: placeholder, onChange: e => onChange(e.target.value) }) }));
|
|
88
|
+
}
|
|
89
|
+
/** Date picker for ISO dates (YYYY-MM-DD). A date already written as free text (e.g.
|
|
90
|
+
* "June 2026") stays editable as text, with a switch to the picker. */
|
|
91
|
+
function DateInput({ id, value, onChange, usePickerLabel, useTextLabel }) {
|
|
92
|
+
const [asText, setAsText] = useState(value !== '' && !ISO_DATE.test(value));
|
|
93
|
+
return (_jsxs("div", { className: "cs-tp-date", children: [asText
|
|
94
|
+
? _jsx("input", { id: id, className: "cs-tp-input", value: value, onChange: e => onChange(e.target.value) })
|
|
95
|
+
: _jsx("input", { id: id, type: "date", className: "cs-tp-input", value: ISO_DATE.test(value) ? value : '', onChange: e => onChange(e.target.value) }), _jsx("button", { type: "button", className: "cs-tp-link", onClick: () => setAsText(!asText), children: asText ? usePickerLabel : useTextLabel })] }));
|
|
96
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Title page form ⇄ Sutra frontmatter.
|
|
3
|
+
*
|
|
4
|
+
* `readTitlePage` turns the parsed frontmatter into form state. `writeTitlePage` writes the
|
|
5
|
+
* form back into the Sutra text by editing only the frontmatter lines of keys whose
|
|
6
|
+
* value actually changed. Every other line (unknown keys, nested maps the form does not
|
|
7
|
+
* manage, comments, ordering, formatting) is kept byte for byte, so opening the form and
|
|
8
|
+
* saving without edits leaves the file unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Values are written in the subset the parser's minimal YAML reader understands: plain
|
|
11
|
+
* scalars, `key: |` block scalars, one level of nested map (multilingual `title:`), and
|
|
12
|
+
* `[a, b]` flow lists (kept as strings by the parser and split by readers).
|
|
13
|
+
*/
|
|
14
|
+
export type PageSize = '' | 'A4' | 'Letter';
|
|
15
|
+
export interface AltTitle {
|
|
16
|
+
lang: string;
|
|
17
|
+
text: string;
|
|
18
|
+
}
|
|
19
|
+
export interface CustomField {
|
|
20
|
+
key: string;
|
|
21
|
+
value: string;
|
|
22
|
+
}
|
|
23
|
+
export interface TitlePageForm {
|
|
24
|
+
title: string;
|
|
25
|
+
altTitles: AltTitle[];
|
|
26
|
+
logline: string;
|
|
27
|
+
author: string;
|
|
28
|
+
credit: string;
|
|
29
|
+
source: string;
|
|
30
|
+
story: string;
|
|
31
|
+
screenplay: string;
|
|
32
|
+
dialogue: string;
|
|
33
|
+
draft: string;
|
|
34
|
+
revision: string;
|
|
35
|
+
date: string;
|
|
36
|
+
contact: string;
|
|
37
|
+
copyright: string;
|
|
38
|
+
lang: string;
|
|
39
|
+
langSecondary: string[];
|
|
40
|
+
page: PageSize;
|
|
41
|
+
style: string;
|
|
42
|
+
watermark: string;
|
|
43
|
+
custom: CustomField[];
|
|
44
|
+
}
|
|
45
|
+
/** Keys the form edits directly (in the order new keys are appended). */
|
|
46
|
+
export declare const MANAGED_KEYS: readonly ["title", "logline", "author", "credit", "source", "story", "screenplay", "dialogue", "draft", "revision", "date", "contact", "copyright", "lang", "lang-secondary", "page", "style", "watermark"];
|
|
47
|
+
export declare function emptyForm(): TitlePageForm;
|
|
48
|
+
/** Split a list value: `[a, b]`, `a, b`, or an array. */
|
|
49
|
+
export declare function splitList(v: unknown): string[];
|
|
50
|
+
/** Build form state from parsed frontmatter data. */
|
|
51
|
+
export declare function readTitlePage(data: Record<string, unknown> | undefined): TitlePageForm;
|
|
52
|
+
/**
|
|
53
|
+
* Write `next` into `text`'s frontmatter. `original` is the form as read from the same
|
|
54
|
+
* text; only keys whose value differs between the two are rewritten.
|
|
55
|
+
*/
|
|
56
|
+
export declare function writeTitlePage(text: string, original: TitlePageForm, next: TitlePageForm): string;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Title page form ⇄ Sutra frontmatter.
|
|
3
|
+
*
|
|
4
|
+
* `readTitlePage` turns the parsed frontmatter into form state. `writeTitlePage` writes the
|
|
5
|
+
* form back into the Sutra text by editing only the frontmatter lines of keys whose
|
|
6
|
+
* value actually changed. Every other line (unknown keys, nested maps the form does not
|
|
7
|
+
* manage, comments, ordering, formatting) is kept byte for byte, so opening the form and
|
|
8
|
+
* saving without edits leaves the file unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Values are written in the subset the parser's minimal YAML reader understands: plain
|
|
11
|
+
* scalars, `key: |` block scalars, one level of nested map (multilingual `title:`), and
|
|
12
|
+
* `[a, b]` flow lists (kept as strings by the parser and split by readers).
|
|
13
|
+
*/
|
|
14
|
+
/** Keys the form edits directly (in the order new keys are appended). */
|
|
15
|
+
export const MANAGED_KEYS = [
|
|
16
|
+
'title', 'logline', 'author', 'credit', 'source', 'story', 'screenplay', 'dialogue',
|
|
17
|
+
'draft', 'revision', 'date', 'contact', 'copyright',
|
|
18
|
+
'lang', 'lang-secondary', 'page', 'style', 'watermark',
|
|
19
|
+
];
|
|
20
|
+
/** Keys kept but never shown as editable custom rows. */
|
|
21
|
+
const HIDDEN_KEYS = new Set(['format']);
|
|
22
|
+
const KEY_RE = /^([A-Za-z0-9_-]+):(.*)$/;
|
|
23
|
+
export function emptyForm() {
|
|
24
|
+
return {
|
|
25
|
+
title: '', altTitles: [], logline: '',
|
|
26
|
+
author: '', credit: '', source: '', story: '', screenplay: '', dialogue: '',
|
|
27
|
+
draft: '', revision: '', date: '',
|
|
28
|
+
contact: '', copyright: '',
|
|
29
|
+
lang: '', langSecondary: [], page: '', style: '', watermark: '',
|
|
30
|
+
custom: [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function str(v) {
|
|
34
|
+
return typeof v === 'string' ? v : v === undefined || v === null ? '' : String(v);
|
|
35
|
+
}
|
|
36
|
+
/** Split a list value: `[a, b]`, `a, b`, or an array. */
|
|
37
|
+
export function splitList(v) {
|
|
38
|
+
if (Array.isArray(v))
|
|
39
|
+
return v.map(x => str(x).trim()).filter(Boolean);
|
|
40
|
+
return str(v).replace(/^\s*\[|\]\s*$/g, '').split(',')
|
|
41
|
+
.map(x => x.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean);
|
|
42
|
+
}
|
|
43
|
+
function normalizePage(v) {
|
|
44
|
+
const p = v.trim().toLowerCase();
|
|
45
|
+
if (p === 'a4')
|
|
46
|
+
return 'A4';
|
|
47
|
+
if (p === 'letter' || p === 'us-letter' || p === 'usletter')
|
|
48
|
+
return 'Letter';
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
/** Build form state from parsed frontmatter data. */
|
|
52
|
+
export function readTitlePage(data) {
|
|
53
|
+
const f = emptyForm();
|
|
54
|
+
if (!data)
|
|
55
|
+
return f;
|
|
56
|
+
f.lang = str(data['lang']).trim();
|
|
57
|
+
const title = data['title'];
|
|
58
|
+
if (title && typeof title === 'object' && !Array.isArray(title)) {
|
|
59
|
+
const map = title;
|
|
60
|
+
const keys = Object.keys(map);
|
|
61
|
+
const primary = keys.includes(f.lang) ? f.lang : keys[0];
|
|
62
|
+
if (primary !== undefined)
|
|
63
|
+
f.title = str(map[primary]);
|
|
64
|
+
f.altTitles = keys.filter(k => k !== primary).map(k => ({ lang: k, text: str(map[k]) }));
|
|
65
|
+
// Remember which language key held the main title.
|
|
66
|
+
if (primary !== undefined && !f.lang)
|
|
67
|
+
f.lang = primary;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
f.title = str(title);
|
|
71
|
+
}
|
|
72
|
+
f.logline = str(data['logline']);
|
|
73
|
+
f.author = str(data['author']);
|
|
74
|
+
f.credit = str(data['credit']);
|
|
75
|
+
f.source = str(data['source']);
|
|
76
|
+
f.story = str(data['story']);
|
|
77
|
+
f.screenplay = str(data['screenplay']);
|
|
78
|
+
f.dialogue = str(data['dialogue']);
|
|
79
|
+
f.draft = str(data['draft']);
|
|
80
|
+
f.revision = str(data['revision']);
|
|
81
|
+
f.date = str(data['date']);
|
|
82
|
+
f.contact = str(data['contact']);
|
|
83
|
+
f.copyright = str(data['copyright']);
|
|
84
|
+
f.langSecondary = splitList(data['lang-secondary']);
|
|
85
|
+
f.page = normalizePage(str(data['page']));
|
|
86
|
+
f.style = str(data['style']);
|
|
87
|
+
f.watermark = str(data['watermark']);
|
|
88
|
+
const managed = new Set(MANAGED_KEYS);
|
|
89
|
+
for (const [key, value] of Object.entries(data)) {
|
|
90
|
+
if (managed.has(key) || HIDDEN_KEYS.has(key))
|
|
91
|
+
continue;
|
|
92
|
+
if (typeof value === 'string' && !value.includes('\n'))
|
|
93
|
+
f.custom.push({ key, value });
|
|
94
|
+
}
|
|
95
|
+
return f;
|
|
96
|
+
}
|
|
97
|
+
// ── Writing ─────────────────────────────────────────────────────────────────────────
|
|
98
|
+
function scalarLines(key, value) {
|
|
99
|
+
const v = value.replace(/\s+$/g, '');
|
|
100
|
+
if (v.trim() === '')
|
|
101
|
+
return null;
|
|
102
|
+
if (v.includes('\n')) {
|
|
103
|
+
return [`${key}: |`, ...v.split('\n').map(l => (l ? ` ${l}` : ''))];
|
|
104
|
+
}
|
|
105
|
+
return [`${key}: ${v.trim()}`];
|
|
106
|
+
}
|
|
107
|
+
function oneLine(v) {
|
|
108
|
+
return v.replace(/\s*\n\s*/g, ' ').trim();
|
|
109
|
+
}
|
|
110
|
+
/** Lines for a managed key, or null to remove it. */
|
|
111
|
+
function managedLines(key, f) {
|
|
112
|
+
switch (key) {
|
|
113
|
+
case 'title': {
|
|
114
|
+
const alts = f.altTitles.filter(a => a.lang.trim() && a.text.trim());
|
|
115
|
+
if (alts.length === 0)
|
|
116
|
+
return scalarLines('title', oneLine(f.title));
|
|
117
|
+
const primaryKey = f.lang.trim() || 'und';
|
|
118
|
+
const lines = ['title:'];
|
|
119
|
+
if (f.title.trim())
|
|
120
|
+
lines.push(` ${primaryKey}: ${oneLine(f.title)}`);
|
|
121
|
+
for (const a of alts)
|
|
122
|
+
if (a.lang.trim() !== primaryKey)
|
|
123
|
+
lines.push(` ${a.lang.trim()}: ${oneLine(a.text)}`);
|
|
124
|
+
return lines;
|
|
125
|
+
}
|
|
126
|
+
case 'lang-secondary': {
|
|
127
|
+
const list = f.langSecondary.map(l => l.trim()).filter(Boolean);
|
|
128
|
+
return list.length ? [`lang-secondary: [${list.join(', ')}]`] : null;
|
|
129
|
+
}
|
|
130
|
+
case 'page': return f.page ? [`page: ${f.page}`] : null;
|
|
131
|
+
case 'logline': return scalarLines('logline', oneLine(f.logline));
|
|
132
|
+
case 'contact': return scalarLines('contact', f.contact);
|
|
133
|
+
default: {
|
|
134
|
+
const field = {
|
|
135
|
+
author: f.author, credit: f.credit, source: f.source, story: f.story,
|
|
136
|
+
screenplay: f.screenplay, dialogue: f.dialogue, draft: f.draft, revision: f.revision,
|
|
137
|
+
date: f.date, copyright: f.copyright, lang: f.lang, style: f.style, watermark: f.watermark,
|
|
138
|
+
}[key] ?? '';
|
|
139
|
+
return scalarLines(key, oneLine(field));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Semantic value of a managed key, for change detection. */
|
|
144
|
+
function managedValue(key, f) {
|
|
145
|
+
return JSON.stringify(managedLines(key, f));
|
|
146
|
+
}
|
|
147
|
+
function splitEntries(yamlLines) {
|
|
148
|
+
const entries = [];
|
|
149
|
+
for (const line of yamlLines) {
|
|
150
|
+
const m = KEY_RE.exec(line);
|
|
151
|
+
if (m)
|
|
152
|
+
entries.push({ key: m[1], lines: [line] });
|
|
153
|
+
else if (entries.length > 0)
|
|
154
|
+
entries[entries.length - 1].lines.push(line);
|
|
155
|
+
else
|
|
156
|
+
entries.push({ key: null, lines: [line] });
|
|
157
|
+
}
|
|
158
|
+
return entries;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Write `next` into `text`'s frontmatter. `original` is the form as read from the same
|
|
162
|
+
* text; only keys whose value differs between the two are rewritten.
|
|
163
|
+
*/
|
|
164
|
+
export function writeTitlePage(text, original, next) {
|
|
165
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
166
|
+
const lines = text.split(/\r?\n/);
|
|
167
|
+
let yamlLines = [];
|
|
168
|
+
let bodyLines = lines;
|
|
169
|
+
let hadFrontmatter = false;
|
|
170
|
+
if (lines[0] === '---') {
|
|
171
|
+
const close = lines.indexOf('---', 1);
|
|
172
|
+
if (close > 0) {
|
|
173
|
+
hadFrontmatter = true;
|
|
174
|
+
yamlLines = lines.slice(1, close);
|
|
175
|
+
bodyLines = lines.slice(close + 1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const entries = splitEntries(yamlLines);
|
|
179
|
+
const present = new Set(entries.map(e => e.key).filter((k) => k !== null));
|
|
180
|
+
// Managed keys that changed → their new lines (null = remove).
|
|
181
|
+
const changes = new Map();
|
|
182
|
+
for (const key of MANAGED_KEYS) {
|
|
183
|
+
if (managedValue(key, original) !== managedValue(key, next))
|
|
184
|
+
changes.set(key, managedLines(key, next));
|
|
185
|
+
}
|
|
186
|
+
// Primary language key of a multilingual title follows `lang`.
|
|
187
|
+
if (next.altTitles.some(a => a.text.trim()) && original.lang !== next.lang) {
|
|
188
|
+
changes.set('title', managedLines('title', next));
|
|
189
|
+
}
|
|
190
|
+
// Custom rows: edited, removed, or added.
|
|
191
|
+
const origCustom = new Map(original.custom.map(c => [c.key, c.value]));
|
|
192
|
+
const nextCustom = new Map();
|
|
193
|
+
for (const c of next.custom) {
|
|
194
|
+
const k = c.key.trim();
|
|
195
|
+
if (!/^[A-Za-z0-9_-]+$/.test(k) || MANAGED_KEYS.includes(k))
|
|
196
|
+
continue;
|
|
197
|
+
nextCustom.set(k, c.value);
|
|
198
|
+
}
|
|
199
|
+
for (const [k, v] of origCustom) {
|
|
200
|
+
if (!nextCustom.has(k))
|
|
201
|
+
changes.set(k, null);
|
|
202
|
+
else if (nextCustom.get(k) !== v)
|
|
203
|
+
changes.set(k, scalarLines(k, oneLine(nextCustom.get(k))));
|
|
204
|
+
}
|
|
205
|
+
for (const [k, v] of nextCustom) {
|
|
206
|
+
if (!origCustom.has(k))
|
|
207
|
+
changes.set(k, scalarLines(k, oneLine(v)));
|
|
208
|
+
}
|
|
209
|
+
if (changes.size === 0)
|
|
210
|
+
return text;
|
|
211
|
+
const out = [];
|
|
212
|
+
for (const e of entries) {
|
|
213
|
+
if (e.key !== null && changes.has(e.key)) {
|
|
214
|
+
const replacement = changes.get(e.key);
|
|
215
|
+
if (replacement)
|
|
216
|
+
out.push(...replacement);
|
|
217
|
+
changes.delete(e.key);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
out.push(...e.lines);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Keys not present before: managed keys in canonical order, then custom keys.
|
|
224
|
+
for (const key of MANAGED_KEYS) {
|
|
225
|
+
if (!present.has(key) && changes.has(key)) {
|
|
226
|
+
const add = changes.get(key);
|
|
227
|
+
if (add)
|
|
228
|
+
out.push(...add);
|
|
229
|
+
changes.delete(key);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const [, add] of changes)
|
|
233
|
+
if (add)
|
|
234
|
+
out.push(...add);
|
|
235
|
+
const hasContent = out.some(l => l.trim() !== '');
|
|
236
|
+
if (!hasContent) {
|
|
237
|
+
// Everything removed: drop the frontmatter block and the blank line after it.
|
|
238
|
+
const rest = hadFrontmatter && bodyLines[0] === '' ? bodyLines.slice(1) : bodyLines;
|
|
239
|
+
return rest.join(eol);
|
|
240
|
+
}
|
|
241
|
+
if (!hadFrontmatter) {
|
|
242
|
+
return ['---', ...out, '---', '', ...lines].join(eol);
|
|
243
|
+
}
|
|
244
|
+
return ['---', ...out, '---', ...bodyLines].join(eol);
|
|
245
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Languages offered for a document's `lang` / `lang-secondary` (BCP-47 primary tags).
|
|
2
|
+
* Indian languages first, then others commonly mixed into Indian scripts. A code found in
|
|
3
|
+
* a file but missing here is still shown (see `languageOptions`). */
|
|
4
|
+
export interface LanguageOption {
|
|
5
|
+
code: string;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const DOCUMENT_LANGUAGES: LanguageOption[];
|
|
9
|
+
/** The list plus any codes already in the document that it does not contain. */
|
|
10
|
+
export declare function languageOptions(inUse: string[]): LanguageOption[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const DOCUMENT_LANGUAGES = [
|
|
2
|
+
{ code: 'hi', label: 'Hindi — हिन्दी' },
|
|
3
|
+
{ code: 'ta', label: 'Tamil — தமிழ்' },
|
|
4
|
+
{ code: 'te', label: 'Telugu — తెలుగు' },
|
|
5
|
+
{ code: 'ml', label: 'Malayalam — മലയാളം' },
|
|
6
|
+
{ code: 'kn', label: 'Kannada — ಕನ್ನಡ' },
|
|
7
|
+
{ code: 'bn', label: 'Bengali — বাংলা' },
|
|
8
|
+
{ code: 'mr', label: 'Marathi — मराठी' },
|
|
9
|
+
{ code: 'gu', label: 'Gujarati — ગુજરાતી' },
|
|
10
|
+
{ code: 'pa', label: 'Punjabi — ਪੰਜਾਬੀ' },
|
|
11
|
+
{ code: 'or', label: 'Odia — ଓଡ଼ିଆ' },
|
|
12
|
+
{ code: 'as', label: 'Assamese — অসমীয়া' },
|
|
13
|
+
{ code: 'ur', label: 'Urdu — اردو' },
|
|
14
|
+
{ code: 'si', label: 'Sinhala — සිංහල' },
|
|
15
|
+
{ code: 'sa', label: 'Sanskrit — संस्कृतम्' },
|
|
16
|
+
{ code: 'ne', label: 'Nepali — नेपाली' },
|
|
17
|
+
{ code: 'kok', label: 'Konkani — कोंकणी' },
|
|
18
|
+
{ code: 'mai', label: 'Maithili — मैथिली' },
|
|
19
|
+
{ code: 'bho', label: 'Bhojpuri — भोजपुरी' },
|
|
20
|
+
{ code: 'tcy', label: 'Tulu — ತುಳು' },
|
|
21
|
+
{ code: 'en', label: 'English' },
|
|
22
|
+
{ code: 'ar', label: 'Arabic — العربية' },
|
|
23
|
+
{ code: 'fr', label: 'French — Français' },
|
|
24
|
+
{ code: 'es', label: 'Spanish — Español' },
|
|
25
|
+
];
|
|
26
|
+
/** The list plus any codes already in the document that it does not contain. */
|
|
27
|
+
export function languageOptions(inUse) {
|
|
28
|
+
const known = new Set(DOCUMENT_LANGUAGES.map(l => l.code));
|
|
29
|
+
const extra = inUse.filter(c => c && !known.has(c)).map(c => ({ code: c, label: c }));
|
|
30
|
+
return [...DOCUMENT_LANGUAGES, ...extra];
|
|
31
|
+
}
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sutrata/editor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "GPL-3.0-or-later",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "node scripts/generate-providers.mjs && tsc && node scripts/copy-assets.mjs",
|
|
10
|
+
"generate:providers": "node scripts/generate-providers.mjs",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@codemirror/commands": "^6.10.3",
|
|
16
|
+
"@codemirror/language": "^6.12.3",
|
|
17
|
+
"@codemirror/state": "^6.6.0",
|
|
18
|
+
"@codemirror/theme-one-dark": "^6.1.3",
|
|
19
|
+
"@codemirror/view": "^6.43.1",
|
|
20
|
+
"@lezer/highlight": "^1.2.3",
|
|
21
|
+
"@sutrata/parser": "workspace:*",
|
|
22
|
+
"diff": "^9.0.0",
|
|
23
|
+
"docx": "^9.7.1",
|
|
24
|
+
"jszip": "^3.10.1",
|
|
25
|
+
"prosemirror-commands": "^1.5.2",
|
|
26
|
+
"prosemirror-history": "^1.4.0",
|
|
27
|
+
"prosemirror-keymap": "^1.2.2",
|
|
28
|
+
"prosemirror-model": "^1.21.0",
|
|
29
|
+
"prosemirror-schema-basic": "^1.2.2",
|
|
30
|
+
"prosemirror-state": "^1.4.3",
|
|
31
|
+
"prosemirror-transform": "^1.9.0",
|
|
32
|
+
"prosemirror-view": "^1.33.0",
|
|
33
|
+
"react": "^18.3.0",
|
|
34
|
+
"react-dom": "^18.3.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
38
|
+
"@testing-library/react": "^16.3.2",
|
|
39
|
+
"@testing-library/user-event": "^14.6.1",
|
|
40
|
+
"@types/react": "^18.3.0",
|
|
41
|
+
"@types/react-dom": "^18.3.0",
|
|
42
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
43
|
+
"jsdom": "^29.1.1",
|
|
44
|
+
"typescript": "^5.4.0",
|
|
45
|
+
"vitest": "^1.6.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { mkdirSync, copyFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
6
|
+
|
|
7
|
+
const files = [
|
|
8
|
+
['src/shell/fonts/fonts.css', 'dist/shell/fonts/fonts.css'],
|
|
9
|
+
['src/styles/screenplay.css', 'dist/styles/screenplay.css'],
|
|
10
|
+
['src/desktop-api.d.ts', 'dist/desktop-api.d.ts'],
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
for (const [from, to] of files) {
|
|
14
|
+
mkdirSync(dirname(join(root, to)), { recursive: true });
|
|
15
|
+
copyFileSync(join(root, from), join(root, to));
|
|
16
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Generates src/ai/generated-providers.json (and a copy at
|
|
2
|
+
// packages/app/public/models-config.json) from the community-maintained
|
|
3
|
+
// all-llm-provider-list registry, at build time.
|
|
4
|
+
//
|
|
5
|
+
// Only providers Sutrata can actually drive are kept: ones with a real
|
|
6
|
+
// env_variable (i.e. a "paste an API key" auth model — this drops
|
|
7
|
+
// OAuth/browser-cookie/local-only/no-auth entries) and either
|
|
8
|
+
// openai_compatible: true, or Anthropic (the one vendor ai-client.ts still
|
|
9
|
+
// calls via a bespoke request shape, since Anthropic has no OpenAI-compat
|
|
10
|
+
// endpoint). Google/Gemini is retargeted at its official OpenAI-compat base
|
|
11
|
+
// URL (https://generativelanguage.googleapis.com/v1beta/openai) so it flows
|
|
12
|
+
// through the generic branch too, rather than needing its own adapter.
|
|
13
|
+
//
|
|
14
|
+
// TODO(future): this list is currently refreshed only when this script is
|
|
15
|
+
// re-run at build time. A follow-up could instead fetch the upstream list
|
|
16
|
+
// live each time the user opens the AI settings/onboarding dialog, so the
|
|
17
|
+
// provider catalog stays current without a rebuild — intentionally not done
|
|
18
|
+
// here to keep this change scoped.
|
|
19
|
+
|
|
20
|
+
import { writeFileSync, existsSync } from 'node:fs';
|
|
21
|
+
import { dirname, join } from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
const editorOutPath = join(root, 'src/ai/generated-providers.json');
|
|
26
|
+
const appOutPath = join(root, '../app/public/models-config.json');
|
|
27
|
+
|
|
28
|
+
const SOURCE_URL = 'https://raw.githubusercontent.com/foisalislambd/all-llm-provider-list/main/data/providers.json';
|
|
29
|
+
|
|
30
|
+
// id/baseUrl overrides for vendors Sutrata treats specially. Ids double as
|
|
31
|
+
// localStorage key suffixes (sutrata_api_key_<id>) and, for anthropic, as
|
|
32
|
+
// invokeProvider's branch discriminator — they must stay stable across
|
|
33
|
+
// regenerations regardless of what the upstream slug says.
|
|
34
|
+
//
|
|
35
|
+
// `models` overrides are needed because the upstream list's popular_models
|
|
36
|
+
// sometimes holds human-readable display names (e.g. "Claude Opus 4.8")
|
|
37
|
+
// rather than API-ready model ids — fine for a dropdown label, but wrong if
|
|
38
|
+
// sent verbatim as the `model` field. These two are the ids actually used
|
|
39
|
+
// for testConnection() before listModels() has a chance to fetch live ones;
|
|
40
|
+
// everyone else relies on listModels() to correct a bad static id at
|
|
41
|
+
// first use.
|
|
42
|
+
const OVERRIDES = {
|
|
43
|
+
anthropic: {
|
|
44
|
+
id: 'anthropic',
|
|
45
|
+
baseUrl: 'https://api.anthropic.com/v1',
|
|
46
|
+
models: [
|
|
47
|
+
{ id: 'claude-opus-5', name: 'Claude Opus 5' },
|
|
48
|
+
{ id: 'claude-sonnet-5', name: 'Claude Sonnet 5' },
|
|
49
|
+
{ id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
'google-ai-studio': {
|
|
53
|
+
id: 'gemini',
|
|
54
|
+
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
55
|
+
models: [
|
|
56
|
+
{ id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash' },
|
|
57
|
+
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro' },
|
|
58
|
+
{ id: 'gemini-2.0-flash-lite', name: 'Gemini 2.0 Flash Lite' },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
OVERRIDES.gemini = OVERRIDES['google-ai-studio'];
|
|
63
|
+
OVERRIDES.google = OVERRIDES['google-ai-studio'];
|
|
64
|
+
|
|
65
|
+
const ESSENTIAL_IDS = new Set(['anthropic', 'gemini', 'groq']);
|
|
66
|
+
|
|
67
|
+
function isSupported(entry) {
|
|
68
|
+
if (!entry.env_variable || !entry.api_base_url) return false;
|
|
69
|
+
const override = OVERRIDES[entry.slug];
|
|
70
|
+
return entry.openai_compatible === true || override?.id === 'anthropic';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toProviderConfig(entry) {
|
|
74
|
+
const override = OVERRIDES[entry.slug];
|
|
75
|
+
const id = override?.id ?? entry.slug;
|
|
76
|
+
const baseUrl = override?.baseUrl ?? entry.api_base_url;
|
|
77
|
+
const config = {
|
|
78
|
+
id,
|
|
79
|
+
name: entry.name,
|
|
80
|
+
baseUrl,
|
|
81
|
+
models: override?.models ?? (entry.popular_models ?? []).map((m) => ({ id: m, name: m })),
|
|
82
|
+
};
|
|
83
|
+
if (ESSENTIAL_IDS.has(id)) config.essential = true;
|
|
84
|
+
return config;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function main() {
|
|
88
|
+
let source;
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch(SOURCE_URL);
|
|
91
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
92
|
+
source = await res.json();
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.warn(`[generate-providers] Failed to fetch ${SOURCE_URL}: ${e}. Leaving existing generated files untouched.`);
|
|
95
|
+
if (!existsSync(editorOutPath)) {
|
|
96
|
+
console.error('[generate-providers] No existing generated-providers.json to fall back to — build will use the compiled-in defaults only.');
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const entries = Array.isArray(source) ? source : source.providers ?? [];
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
const providers = [];
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (!isSupported(entry)) continue;
|
|
106
|
+
const config = toProviderConfig(entry);
|
|
107
|
+
if (seen.has(config.id)) continue; // first match wins (upstream list order)
|
|
108
|
+
seen.add(config.id);
|
|
109
|
+
providers.push(config);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const output = {
|
|
113
|
+
generatedAt: new Date().toISOString(),
|
|
114
|
+
source: 'https://github.com/foisalislambd/all-llm-provider-list',
|
|
115
|
+
providers,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const json = JSON.stringify(output, null, 2) + '\n';
|
|
119
|
+
writeFileSync(editorOutPath, json);
|
|
120
|
+
|
|
121
|
+
const appConfig = { providers: output.providers };
|
|
122
|
+
writeFileSync(appOutPath, JSON.stringify(appConfig, null, 2) + '\n');
|
|
123
|
+
|
|
124
|
+
console.log(`[generate-providers] Wrote ${providers.length} providers to ${editorOutPath} and ${appOutPath}.`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await main();
|