@sutrata/editor 0.1.1 → 0.2.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/README.md +87 -0
- package/dist/ai/VoiceConfirmDialog.d.ts +1 -0
- package/dist/ai/VoiceConfirmDialog.js +6 -6
- package/dist/ai/VoiceToolbar.d.ts +1 -0
- package/dist/ai/VoiceToolbar.js +22 -16
- package/dist/ai/clean-response.d.ts +7 -0
- package/dist/ai/clean-response.js +58 -0
- package/dist/ai/format-transcript.d.ts +3 -0
- package/dist/ai/format-transcript.js +7 -0
- package/dist/ai/prompts.d.ts +8 -1
- package/dist/ai/prompts.js +34 -7
- package/dist/autocomplete/suggestions.js +1 -1
- package/dist/context/DocumentContext.d.ts +28 -4
- package/dist/context/DocumentContext.js +67 -37
- package/dist/editor/EditorView.js +32 -4
- package/dist/editor/SelectionMenu.js +5 -3
- package/dist/editor/external-decorations-plugin.d.ts +23 -0
- package/dist/editor/external-decorations-plugin.js +151 -0
- package/dist/editor/omitted-scene-plugin.d.ts +9 -0
- package/dist/editor/omitted-scene-plugin.js +44 -0
- package/dist/editor/plugins.js +4 -0
- package/dist/editor/private-metadata-plugin.d.ts +10 -0
- package/dist/editor/private-metadata-plugin.js +60 -0
- package/dist/editor/prosemirror-to-sutra.js +179 -74
- package/dist/editor/scene-heading-view.js +2 -0
- package/dist/editor/schema.js +69 -12
- package/dist/editor/sutra-to-prosemirror.d.ts +2 -0
- package/dist/editor/sutra-to-prosemirror.js +32 -34
- package/dist/extensions/ai-provider.d.ts +44 -0
- package/dist/extensions/ai-provider.js +27 -0
- package/dist/extensions/collab.d.ts +21 -0
- package/dist/extensions/collab.js +6 -0
- package/dist/extensions/command-registry.d.ts +49 -0
- package/dist/extensions/command-registry.js +28 -0
- package/dist/extensions/decorations.d.ts +48 -0
- package/dist/extensions/decorations.js +6 -0
- package/dist/extensions/export-registry.d.ts +59 -0
- package/dist/extensions/export-registry.js +10 -0
- package/dist/extensions/panel-registry.d.ts +30 -0
- package/dist/extensions/panel-registry.js +16 -0
- package/dist/extensions/registries.d.ts +15 -0
- package/dist/extensions/registries.js +12 -0
- package/dist/extensions/registry-store.d.ts +26 -0
- package/dist/extensions/registry-store.js +35 -0
- package/dist/extensions/session.d.ts +29 -0
- package/dist/extensions/session.js +20 -0
- package/dist/extensions/speech-provider.d.ts +35 -0
- package/dist/extensions/speech-provider.js +17 -0
- package/dist/file/builtin-exporters.d.ts +9 -0
- package/dist/file/builtin-exporters.js +72 -0
- package/dist/file/docx-exporter.js +7 -4
- package/dist/file/docx-importer.d.ts +9 -0
- package/dist/file/docx-importer.js +47 -19
- package/dist/file/scene-number.d.ts +5 -0
- package/dist/file/scene-number.js +8 -0
- package/dist/file/workflow-reports.js +13 -7
- package/dist/findreplace/FindReplace.js +5 -2
- package/dist/import/AIImportDialog.d.ts +11 -0
- package/dist/import/AIImportDialog.js +112 -0
- package/dist/import/ai-import.d.ts +51 -0
- package/dist/import/ai-import.js +200 -0
- package/dist/import/apply-import.d.ts +7 -0
- package/dist/import/apply-import.js +35 -0
- package/dist/import/extract.d.ts +17 -0
- package/dist/import/extract.js +18 -0
- package/dist/index.d.ts +17 -2
- package/dist/index.js +7 -0
- package/dist/locales/bn.json +8 -0
- package/dist/locales/en.json +8 -0
- package/dist/locales/gu.json +8 -0
- package/dist/locales/hi.json +8 -0
- package/dist/locales/kn.json +8 -0
- package/dist/locales/ml.json +8 -0
- package/dist/locales/mr.json +8 -0
- package/dist/locales/or.json +8 -0
- package/dist/locales/pa.json +8 -0
- package/dist/locales/si.json +8 -0
- package/dist/locales/ta.json +8 -0
- package/dist/locales/te.json +8 -0
- package/dist/navigator/SceneNavigator.js +125 -131
- package/dist/navigator/StatisticsDialog.js +5 -1
- package/dist/navigator/scene-edits.d.ts +18 -0
- package/dist/navigator/scene-edits.js +143 -0
- package/dist/navigator/scene-list.js +4 -4
- package/dist/navigator/scene-numbering.d.ts +37 -0
- package/dist/navigator/scene-numbering.js +88 -0
- package/dist/settings/SettingsDialog.js +8 -120
- package/dist/shell/AppBar.js +21 -2
- package/dist/shell/AppShell.js +18 -4
- package/dist/shell/ElementToolbar.d.ts +5 -0
- package/dist/shell/ElementToolbar.js +35 -25
- package/dist/shell/LanguageStatus.js +6 -0
- package/dist/shell/StatusBar.js +11 -6
- package/dist/shell/builtin-commands.d.ts +10 -0
- package/dist/shell/builtin-commands.js +43 -0
- package/dist/shell/icons.d.ts +6 -0
- package/dist/shell/icons.js +6 -0
- package/dist/shell/use-commands.d.ts +7 -0
- package/dist/shell/use-commands.js +24 -0
- package/dist/shell/use-panel-context.d.ts +3 -0
- package/dist/shell/use-panel-context.js +39 -0
- package/dist/source/SourceView.js +18 -1
- package/dist/styles/screenplay.css +113 -1
- package/dist/titlepage/ExportDialog.d.ts +5 -0
- package/dist/titlepage/ExportDialog.js +59 -96
- package/package.json +32 -5
- package/dist/ai/OnboardingDialog.d.ts +0 -6
- package/dist/ai/OnboardingDialog.js +0 -115
- package/dist/ai/ai-client.d.ts +0 -71
- package/dist/ai/ai-client.js +0 -494
- package/dist/ai/generated-providers.json +0 -3696
- package/dist/ai/voice-service.d.ts +0 -15
- package/dist/ai/voice-service.js +0 -81
- package/dist/editor/character-view.d.ts +0 -35
- package/dist/editor/character-view.js +0 -112
- package/dist/editor/transliterate-input.d.ts +0 -10
- package/dist/editor/transliterate-input.js +0 -88
- package/dist/extensions/types.d.ts +0 -73
- package/dist/extensions/types.js +0 -12
- 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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene numbers (Sutra format spec §7.4). A scene's `{#id}` is its number:
|
|
3
|
+
* digits plus an optional letter suffix (`12`, `12A`). Numbers change only
|
|
4
|
+
* when the writer asks to renumber; until then they may be missing,
|
|
5
|
+
* duplicated or out of order, and the navigator flags them.
|
|
6
|
+
*/
|
|
7
|
+
export interface SceneNumber {
|
|
8
|
+
base: number;
|
|
9
|
+
/** Uppercase letters, '' for a plain number. */
|
|
10
|
+
suffix: string;
|
|
11
|
+
}
|
|
12
|
+
export type SceneNumberIssue = 'missing' | 'invalid' | 'duplicate' | 'order';
|
|
13
|
+
export declare function parseSceneNumber(value: string | null | undefined): SceneNumber | null;
|
|
14
|
+
export declare function formatSceneNumber(n: SceneNumber): string;
|
|
15
|
+
export declare function compareSceneNumbers(a: SceneNumber, b: SceneNumber): number;
|
|
16
|
+
/** '' → A, A → B, Z → AA, AZ → BA. */
|
|
17
|
+
export declare function nextLetter(suffix: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Per scene: why its number needs renumbering, or null when it is fine.
|
|
20
|
+
* Nothing is flagged while no scene has a number (the script was never
|
|
21
|
+
* numbered). A repeat or a number that doesn't come after the previous good
|
|
22
|
+
* one is flagged on the later scene.
|
|
23
|
+
*/
|
|
24
|
+
export declare function sceneNumberIssues(numbers: readonly (string | null | undefined)[]): (SceneNumberIssue | null)[];
|
|
25
|
+
/**
|
|
26
|
+
* New numbers for every scene, per §7.4. Position decides everything; a
|
|
27
|
+
* scene's old number only says whether it is a main scene (`12`) or a
|
|
28
|
+
* sub-scene (`12A`) of the main scene before it, so repeats and gaps need
|
|
29
|
+
* no special handling:
|
|
30
|
+
* - main scenes are numbered 1, 2, 3, … in order, closing any gaps;
|
|
31
|
+
* - sub-scenes take their main scene's number and the next letter (A, B, …);
|
|
32
|
+
* - a scene with no usable number is inserted: it is a sub-scene when the
|
|
33
|
+
* next scene with a number is one (2A, new, 2B → 2A, 2B, 2C), otherwise
|
|
34
|
+
* a main scene;
|
|
35
|
+
* - a sub-scene with no main scene before it becomes a main scene.
|
|
36
|
+
*/
|
|
37
|
+
export declare function renumberScenes(numbers: readonly (string | null | undefined)[]): string[];
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene numbers (Sutra format spec §7.4). A scene's `{#id}` is its number:
|
|
3
|
+
* digits plus an optional letter suffix (`12`, `12A`). Numbers change only
|
|
4
|
+
* when the writer asks to renumber; until then they may be missing,
|
|
5
|
+
* duplicated or out of order, and the navigator flags them.
|
|
6
|
+
*/
|
|
7
|
+
export function parseSceneNumber(value) {
|
|
8
|
+
const m = /^(\d+)([A-Za-z]*)$/.exec((value ?? '').trim());
|
|
9
|
+
return m ? { base: Number(m[1]), suffix: m[2].toUpperCase() } : null;
|
|
10
|
+
}
|
|
11
|
+
export function formatSceneNumber(n) {
|
|
12
|
+
return `${n.base}${n.suffix}`;
|
|
13
|
+
}
|
|
14
|
+
/** Letter suffixes in order: '' < A < … < Z < AA < AB … */
|
|
15
|
+
function compareSuffix(a, b) {
|
|
16
|
+
return a.length !== b.length ? a.length - b.length : a < b ? -1 : a > b ? 1 : 0;
|
|
17
|
+
}
|
|
18
|
+
export function compareSceneNumbers(a, b) {
|
|
19
|
+
return a.base !== b.base ? a.base - b.base : compareSuffix(a.suffix, b.suffix);
|
|
20
|
+
}
|
|
21
|
+
/** '' → A, A → B, Z → AA, AZ → BA. */
|
|
22
|
+
export function nextLetter(suffix) {
|
|
23
|
+
const letters = suffix.split('');
|
|
24
|
+
let i = letters.length - 1;
|
|
25
|
+
while (i >= 0 && letters[i] === 'Z')
|
|
26
|
+
letters[i--] = 'A';
|
|
27
|
+
if (i < 0)
|
|
28
|
+
return 'A' + letters.join('');
|
|
29
|
+
letters[i] = String.fromCharCode(letters[i].charCodeAt(0) + 1);
|
|
30
|
+
return letters.join('');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Per scene: why its number needs renumbering, or null when it is fine.
|
|
34
|
+
* Nothing is flagged while no scene has a number (the script was never
|
|
35
|
+
* numbered). A repeat or a number that doesn't come after the previous good
|
|
36
|
+
* one is flagged on the later scene.
|
|
37
|
+
*/
|
|
38
|
+
export function sceneNumberIssues(numbers) {
|
|
39
|
+
if (!numbers.some(n => parseSceneNumber(n)))
|
|
40
|
+
return numbers.map(() => null);
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
let last = null;
|
|
43
|
+
return numbers.map(value => {
|
|
44
|
+
if (!value || !value.trim())
|
|
45
|
+
return 'missing';
|
|
46
|
+
const n = parseSceneNumber(value);
|
|
47
|
+
if (!n)
|
|
48
|
+
return 'invalid';
|
|
49
|
+
const key = formatSceneNumber(n);
|
|
50
|
+
if (seen.has(key))
|
|
51
|
+
return 'duplicate';
|
|
52
|
+
if (last && compareSceneNumbers(n, last) <= 0)
|
|
53
|
+
return 'order';
|
|
54
|
+
seen.add(key);
|
|
55
|
+
last = n;
|
|
56
|
+
return null;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* New numbers for every scene, per §7.4. Position decides everything; a
|
|
61
|
+
* scene's old number only says whether it is a main scene (`12`) or a
|
|
62
|
+
* sub-scene (`12A`) of the main scene before it, so repeats and gaps need
|
|
63
|
+
* no special handling:
|
|
64
|
+
* - main scenes are numbered 1, 2, 3, … in order, closing any gaps;
|
|
65
|
+
* - sub-scenes take their main scene's number and the next letter (A, B, …);
|
|
66
|
+
* - a scene with no usable number is inserted: it is a sub-scene when the
|
|
67
|
+
* next scene with a number is one (2A, new, 2B → 2A, 2B, 2C), otherwise
|
|
68
|
+
* a main scene;
|
|
69
|
+
* - a sub-scene with no main scene before it becomes a main scene.
|
|
70
|
+
*/
|
|
71
|
+
export function renumberScenes(numbers) {
|
|
72
|
+
const own = numbers.map(parseSceneNumber);
|
|
73
|
+
const out = [];
|
|
74
|
+
let base = 0;
|
|
75
|
+
let suffix = '';
|
|
76
|
+
for (let i = 0; i < numbers.length; i++) {
|
|
77
|
+
const ref = own[i] ?? own.slice(i + 1).find((n) => n !== null) ?? null;
|
|
78
|
+
if (ref && ref.suffix && base > 0) {
|
|
79
|
+
suffix = nextLetter(suffix);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
base++;
|
|
83
|
+
suffix = '';
|
|
84
|
+
}
|
|
85
|
+
out.push(formatSceneNumber({ base, suffix }));
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
3
|
import { useTranslation } from '../i18n/useTranslation';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { usePanels } from '../extensions/panel-registry';
|
|
5
|
+
import { usePanelContext } from '../shell/use-panel-context';
|
|
6
6
|
const UI_LANGUAGES = [
|
|
7
7
|
{ code: 'en', label: 'English' },
|
|
8
8
|
{ code: 'hi', label: 'हिन्दी' },
|
|
@@ -19,129 +19,17 @@ const UI_LANGUAGES = [
|
|
|
19
19
|
];
|
|
20
20
|
export function SettingsDialog({ onClose }) {
|
|
21
21
|
const { t, locale, setLocale } = useTranslation();
|
|
22
|
-
const
|
|
22
|
+
const panelContext = usePanelContext();
|
|
23
|
+
// Embedder sections (e.g. the OSS app's AI provider & key settings) — the
|
|
24
|
+
// editor itself has no provider-specific settings.
|
|
25
|
+
const sections = usePanels('settings');
|
|
23
26
|
const dialogRef = useRef(null);
|
|
24
|
-
// AI states
|
|
25
|
-
const [aiConfig, setAiConfig] = useState(getAIConfig());
|
|
26
|
-
const [providers, setProviders] = useState([]);
|
|
27
|
-
const [activeKey, setActiveKey] = useState('');
|
|
28
|
-
const [showKey, setShowKey] = useState(false);
|
|
29
|
-
const [testStatus, setTestStatus] = useState('idle');
|
|
30
|
-
const [testError, setTestError] = useState('');
|
|
31
|
-
const [dynamicModels, setDynamicModels] = useState(null);
|
|
32
|
-
const [modelsLoading, setModelsLoading] = useState(false);
|
|
33
27
|
useEffect(() => {
|
|
34
28
|
dialogRef.current?.focus();
|
|
35
|
-
void loadProviders();
|
|
36
29
|
}, []);
|
|
37
|
-
const loadProviders = async () => {
|
|
38
|
-
const p = await fetchProviders(aiConfig);
|
|
39
|
-
setProviders(p);
|
|
40
|
-
};
|
|
41
|
-
// Loads the stored key for the selected provider, then refreshes its
|
|
42
|
-
// model list from the provider's live /models endpoint (falls back to the
|
|
43
|
-
// static list on any error — see listModels). Depends on `providers` too
|
|
44
|
-
// so it re-runs once fetchProviders() resolves on mount, instead of
|
|
45
|
-
// racing it.
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
let cancelled = false;
|
|
48
|
-
const run = async () => {
|
|
49
|
-
setTestStatus('idle');
|
|
50
|
-
setTestError('');
|
|
51
|
-
const key = await getApiKey(aiConfig.preferredProvider);
|
|
52
|
-
if (cancelled)
|
|
53
|
-
return;
|
|
54
|
-
setActiveKey(key || '');
|
|
55
|
-
setDynamicModels(null);
|
|
56
|
-
const provider = providers.find(p => p.id === aiConfig.preferredProvider);
|
|
57
|
-
if (key && provider) {
|
|
58
|
-
setModelsLoading(true);
|
|
59
|
-
const models = await listModels(provider, key);
|
|
60
|
-
if (!cancelled) {
|
|
61
|
-
setDynamicModels(models);
|
|
62
|
-
setModelsLoading(false);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
void run();
|
|
67
|
-
return () => { cancelled = true; };
|
|
68
|
-
}, [aiConfig.preferredProvider, providers]);
|
|
69
30
|
const handleKeyDown = (e) => {
|
|
70
31
|
if (e.key === 'Escape')
|
|
71
32
|
onClose();
|
|
72
33
|
};
|
|
73
|
-
|
|
74
|
-
const nextProvider = e.target.value;
|
|
75
|
-
const provider = providers.find(p => p.id === nextProvider);
|
|
76
|
-
const nextModel = provider?.models[0]?.id || '';
|
|
77
|
-
const updated = {
|
|
78
|
-
...aiConfig,
|
|
79
|
-
preferredProvider: nextProvider,
|
|
80
|
-
preferredModel: nextModel,
|
|
81
|
-
};
|
|
82
|
-
setAiConfig(updated);
|
|
83
|
-
saveAIConfig(updated);
|
|
84
|
-
};
|
|
85
|
-
const handleModelChange = (e) => {
|
|
86
|
-
const updated = {
|
|
87
|
-
...aiConfig,
|
|
88
|
-
preferredModel: e.target.value,
|
|
89
|
-
};
|
|
90
|
-
setAiConfig(updated);
|
|
91
|
-
saveAIConfig(updated);
|
|
92
|
-
};
|
|
93
|
-
const handleKeyChange = async (e) => {
|
|
94
|
-
const val = e.target.value;
|
|
95
|
-
setActiveKey(val);
|
|
96
|
-
if (val.trim()) {
|
|
97
|
-
await setApiKey(aiConfig.preferredProvider, val.trim());
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
await deleteApiKey(aiConfig.preferredProvider);
|
|
101
|
-
}
|
|
102
|
-
setTestStatus('idle');
|
|
103
|
-
};
|
|
104
|
-
const handleTestKey = async () => {
|
|
105
|
-
if (!activeKey.trim()) {
|
|
106
|
-
setTestStatus('error');
|
|
107
|
-
setTestError('Please enter an API key to test.');
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
setTestStatus('testing');
|
|
111
|
-
setTestError('');
|
|
112
|
-
try {
|
|
113
|
-
const ok = await testConnection(aiConfig.preferredProvider, activeKey.trim(), aiConfig);
|
|
114
|
-
if (ok) {
|
|
115
|
-
setTestStatus('success');
|
|
116
|
-
const provider = providers.find(p => p.id === aiConfig.preferredProvider);
|
|
117
|
-
if (provider) {
|
|
118
|
-
setModelsLoading(true);
|
|
119
|
-
setDynamicModels(await listModels(provider, activeKey.trim()));
|
|
120
|
-
setModelsLoading(false);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
setTestStatus('error');
|
|
125
|
-
setTestError('Connection failed: empty or invalid response.');
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
catch (err) {
|
|
129
|
-
setTestStatus('error');
|
|
130
|
-
setTestError(err.message || String(err));
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
const currentProvider = providers.find(p => p.id === aiConfig.preferredProvider);
|
|
134
|
-
const models = dynamicModels ?? currentProvider?.models ?? [];
|
|
135
|
-
return (_jsx("div", { className: "cs-dialog-overlay", onClick: onClose, children: _jsxs("div", { ref: dialogRef, className: "cs-dialog cs-settings-dialog", role: "dialog", "aria-modal": "true", "aria-label": t('settings.title'), tabIndex: -1, onClick: e => e.stopPropagation(), onKeyDown: handleKeyDown, style: { maxWidth: '480px' }, children: [_jsxs("div", { className: "cs-dialog-header", children: [_jsx("span", { className: "cs-dialog-title", children: t('settings.title') }), _jsx("button", { className: "cs-fr-close", "aria-label": t('settings.close'), onClick: onClose, children: "\u00D7" })] }), _jsxs("div", { className: "cs-settings-body", style: { maxHeight: '80vh', overflowY: 'auto' }, children: [_jsx("div", { className: "cs-settings-section-label", children: t('settings.general') }), _jsxs("div", { className: "cs-settings-row", children: [_jsx("label", { className: "cs-settings-label", htmlFor: "cs-ui-lang-select", children: t('settings.uiLanguage') }), _jsx("select", { id: "cs-ui-lang-select", className: "cs-settings-select", value: locale, onChange: e => setLocale(e.target.value), children: UI_LANGUAGES.map(lang => (_jsx("option", { value: lang.code, children: lang.label }, lang.code))) })] }), _jsx("div", { style: { margin: '14px 0 6px 0', borderTop: '1px solid var(--cs-ui-border-light, #e8e8e4)', paddingTop: '14px' }, className: "cs-settings-section-label", children: "Intelligence & Voice Settings" }), _jsxs("div", { className: "cs-settings-row", children: [_jsx("label", { className: "cs-settings-label", htmlFor: "cs-ai-provider-select", children: "Preferred Provider" }), _jsx("select", { id: "cs-ai-provider-select", className: "cs-settings-select", value: aiConfig.preferredProvider, onChange: handleProviderChange, children: providers.map(p => (_jsx("option", { value: p.id, children: p.name }, p.id))) })] }), _jsxs("div", { className: "cs-settings-row", children: [_jsxs("label", { className: "cs-settings-label", htmlFor: "cs-ai-model-select", children: ["Preferred Model", modelsLoading ? ' (refreshing...)' : ''] }), _jsx("select", { id: "cs-ai-model-select", className: "cs-settings-select", value: aiConfig.preferredModel, onChange: handleModelChange, disabled: models.length === 0, children: models.map(m => (_jsx("option", { value: m.id, children: m.name }, m.id))) })] }), _jsxs("div", { className: "cs-settings-row", style: { flexDirection: 'column', alignItems: 'stretch', gap: '6px' }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', width: '100%' }, children: [_jsxs("label", { className: "cs-settings-label", htmlFor: "cs-ai-key-input", children: ["API Key for ", currentProvider?.name || aiConfig.preferredProvider] }), _jsx("button", { type: "button", style: { background: 'none', border: 'none', color: 'var(--cs-ui-accent, #c4760a)', cursor: 'pointer', fontSize: '11px', padding: 0 }, onClick: () => setShowKey(!showKey), children: showKey ? 'Hide' : 'Show' })] }), _jsx("input", { id: "cs-ai-key-input", type: showKey ? 'text' : 'password', style: {
|
|
136
|
-
width: '100%',
|
|
137
|
-
padding: '7px 10px',
|
|
138
|
-
border: '1px solid var(--cs-ui-border-light, #ccc)',
|
|
139
|
-
borderRadius: '4px',
|
|
140
|
-
boxSizing: 'border-box',
|
|
141
|
-
fontFamily: 'monospace',
|
|
142
|
-
fontSize: '12px',
|
|
143
|
-
}, value: activeKey, onChange: handleKeyChange, placeholder: `Enter API Key for ${aiConfig.preferredProvider}` }), _jsx("div", { style: { display: 'flex', gap: '8px', marginTop: '4px' }, children: _jsx("button", { type: "button", className: "cs-voice-btn-secondary", style: { padding: '4px 10px', fontSize: '12px' }, onClick: handleTestKey, disabled: testStatus === 'testing', children: testStatus === 'testing' ? 'Testing...' : 'Test Connection' }) }), testStatus === 'success' && (_jsx("span", { style: { color: '#10b981', fontSize: '12px', marginTop: '2px' }, children: "\u2713 Key verified! Connection succeeded." })), testStatus === 'error' && (_jsxs("span", { style: { color: '#ef4444', fontSize: '12px', marginTop: '2px', whiteSpace: 'pre-wrap' }, children: ["\u2717 Error: ", testError] }))] }), _jsx("div", { style: { marginTop: '16px', display: 'flex', justifyContent: 'flex-start' }, children: _jsx("button", { type: "button", className: "cs-voice-btn-primary", style: { padding: '6px 12px', fontSize: '12px' }, onClick: () => {
|
|
144
|
-
onClose();
|
|
145
|
-
setAIOnboardingVisible(true);
|
|
146
|
-
}, children: "Open Setup Wizard" }) })] })] }) }));
|
|
34
|
+
return (_jsx("div", { className: "cs-dialog-overlay", onClick: onClose, children: _jsxs("div", { ref: dialogRef, className: "cs-dialog cs-settings-dialog", role: "dialog", "aria-modal": "true", "aria-label": t('settings.title'), tabIndex: -1, onClick: e => e.stopPropagation(), onKeyDown: handleKeyDown, style: { maxWidth: '480px' }, children: [_jsxs("div", { className: "cs-dialog-header", children: [_jsx("span", { className: "cs-dialog-title", children: t('settings.title') }), _jsx("button", { className: "cs-fr-close", "aria-label": t('settings.close'), onClick: onClose, children: "\u00D7" })] }), _jsxs("div", { className: "cs-settings-body", style: { maxHeight: '80vh', overflowY: 'auto' }, children: [_jsx("div", { className: "cs-settings-section-label", children: t('settings.general') }), _jsxs("div", { className: "cs-settings-row", children: [_jsx("label", { className: "cs-settings-label", htmlFor: "cs-ui-lang-select", children: t('settings.uiLanguage') }), _jsx("select", { id: "cs-ui-lang-select", className: "cs-settings-select", value: locale, onChange: e => setLocale(e.target.value), children: UI_LANGUAGES.map(lang => (_jsx("option", { value: lang.code, children: lang.label }, lang.code))) })] }), sections.map(section => (_jsxs("section", { className: "cs-settings-section", "aria-label": section.title, children: [_jsx("div", { style: { margin: '14px 0 6px 0', borderTop: '1px solid var(--cs-ui-border-light, #e8e8e4)', paddingTop: '14px' }, className: "cs-settings-section-label", children: section.title }), section.render(panelContext)] }, section.id)))] })] }) }));
|
|
147
35
|
}
|
package/dist/shell/AppBar.js
CHANGED
|
@@ -2,10 +2,17 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { useDocument } from '../context/DocumentContext';
|
|
4
4
|
import { useTranslation } from '../i18n/useTranslation';
|
|
5
|
-
import { NavigatorIcon, TitlePageIcon, NewIcon, OpenIcon, SaveIcon, SaveAsIcon, SourceIcon, FormattedIcon, RibbonIcon, ExportIcon, SettingsIcon, ImportDocxIcon, StyleIcon, MicIcon, MenuIcon, CloseIcon, } from './icons';
|
|
5
|
+
import { NavigatorIcon, TitlePageIcon, NewIcon, OpenIcon, SaveIcon, SaveAsIcon, SourceIcon, FormattedIcon, RibbonIcon, ExportIcon, SettingsIcon, ImportDocxIcon, StyleIcon, MicIcon, MenuIcon, CloseIcon, SparklesIcon, } from './icons';
|
|
6
|
+
import { useAI } from '../extensions/ai-provider';
|
|
7
|
+
import { AIImportDialog } from '../import/AIImportDialog';
|
|
6
8
|
import { SutrataLogo } from './SutrataLogo';
|
|
7
9
|
import { getTemplate } from '../templates/templates';
|
|
8
10
|
import { openTitlePageForm } from '../titlepage/TitlePageDialog';
|
|
11
|
+
import { useSpeech } from '../extensions/speech-provider';
|
|
12
|
+
import { useCanEdit } from '../extensions/session';
|
|
13
|
+
import { usePanels } from '../extensions/panel-registry';
|
|
14
|
+
import { usePanelContext } from './use-panel-context';
|
|
15
|
+
import { useCommands, useCommandContext, displayShortcut } from './use-commands';
|
|
9
16
|
function AbBtn({ label, shortcut, active, variant, onClick, children }) {
|
|
10
17
|
const variantClass = variant === 'primary' ? ' cs-ab-btn-primary' : variant === 'secondary' ? ' cs-ab-btn-secondary' : '';
|
|
11
18
|
return (_jsxs("button", { type: "button", "aria-label": label, className: `cs-ab-btn${active ? ' cs-ab-active' : ''}${variantClass}`, onClick: onClick, children: [children, _jsxs("span", { className: "cs-tb-tip", role: "tooltip", children: [_jsx("span", { children: label }), shortcut ? _jsx("span", { className: "cs-tb-key", children: shortcut }) : null] })] }));
|
|
@@ -13,6 +20,18 @@ function AbBtn({ label, shortcut, active, variant, onClick, children }) {
|
|
|
13
20
|
export function AppBar() {
|
|
14
21
|
const { mode, setMode, isDirty, filePath, ribbonVisible, setRibbonVisible, navVisible, setNavVisible, exportVisible, setExportVisible, settingsVisible, setSettingsVisible, metadataVisible, setMetadataVisible, voiceActive, setVoiceActive, newDocument, openFile, saveFile, saveFileAs, setText, importDocx, styleVisible, setStyleVisible, showConfirm, showToast, } = useDocument();
|
|
15
22
|
const { t, locale } = useTranslation();
|
|
23
|
+
const speech = useSpeech();
|
|
24
|
+
// AI-assisted import needs a provider and an edit session (useAI covers both).
|
|
25
|
+
const ai = useAI();
|
|
26
|
+
const [aiImportOpen, setAiImportOpen] = useState(false);
|
|
27
|
+
// Read-only sessions (SessionContext.permission 'read'/'comment') get no
|
|
28
|
+
// file, title-page, style or voice actions.
|
|
29
|
+
const canEdit = useCanEdit();
|
|
30
|
+
// Embedder contributions: `appbar` panels and `menu: 'appbar'` commands.
|
|
31
|
+
const appbarPanels = usePanels('appbar');
|
|
32
|
+
const appbarCommands = useCommands().filter(c => c.menu === 'appbar');
|
|
33
|
+
const commandContext = useCommandContext();
|
|
34
|
+
const panelContext = usePanelContext();
|
|
16
35
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
17
36
|
const name = filePath ?? 'untitled.sutra';
|
|
18
37
|
const nextMode = mode === 'formatted' ? 'source' : 'formatted';
|
|
@@ -30,5 +49,5 @@ export function AppBar() {
|
|
|
30
49
|
showToast(err instanceof Error ? err.message : 'Failed to import the Word document.', 'error');
|
|
31
50
|
}
|
|
32
51
|
};
|
|
33
|
-
return (_jsxs("header", { className: "cs-app-bar", role: "banner", "aria-label": "Application header", children: [_jsxs("div", { className: "cs-ab-group cs-ab-left", children: [_jsxs("div", { className: "cs-ab-brand", title: "Sutrata Multilingual Screenplay Editor", children: [_jsx(SutrataLogo, { size: 20, showText: false }), _jsx("span", { className: "cs-ab-brand-title", children: "Sutrata" })] }), _jsx("span", { className: "cs-ab-sep cs-ab-desktop-only" }), _jsx(AbBtn, { label: t('appbar.navigator'), shortcut: "Ctrl+Shift+B", active: navVisible, onClick: () => setNavVisible(!navVisible), children: _jsx(NavigatorIcon, { size: 18 }) }), _jsx("span", { className: "cs-ab-desktop-only", children: _jsx(AbBtn, { label: t('appbar.titlePage'), onClick: openTitlePageForm, children: _jsx(TitlePageIcon, { size: 18 }) }) }), _jsxs("div", { className: "cs-ab-file-pill", title: filePath ?? 'untitled.sutra', children: [_jsx("span", { className: "cs-ab-file-name", children: name }), isDirty && _jsx("span", { className: "cs-ab-dirty", title: "Unsaved edits" })] })] }), _jsx("span", { className: "cs-ab-spacer cs-ab-desktop-only" }), _jsxs("div", { className: "cs-ab-group cs-ab-desktop-group", children: [_jsx(AbBtn, { label: mode === 'formatted' ? t('appbar.switchToSource') : t('appbar.switchToFormatted'), shortcut: "Ctrl+Shift+E", onClick: () => setMode(nextMode), children: mode === 'formatted' ? _jsx(SourceIcon, { size: 18 }) : _jsx(FormattedIcon, { size: 18 }) }), _jsx(AbBtn, { label: ribbonVisible ? t('appbar.hideToolbar') : t('appbar.showToolbar'), shortcut: "Ctrl+Shift+H", active: ribbonVisible, onClick: () => setRibbonVisible(!ribbonVisible), children: _jsx(RibbonIcon, { size: 18 }) }), _jsx(AbBtn, { label: "Voice Dictation", shortcut: "Ctrl+Shift+V", active: voiceActive, onClick: () => setVoiceActive(!voiceActive), children: _jsx(MicIcon, { size: 18 }) }), _jsx(AbBtn, { label: metadataVisible ? 'Hide Metadata' : 'Show Metadata', active: metadataVisible, onClick: () => setMetadataVisible(!metadataVisible), children: _jsx("span", { style: { fontWeight: 700, fontSize: '13px' }, children: "&" }) })] }), _jsx("span", { className: "cs-ab-spacer cs-ab-desktop-only" }), _jsxs("div", { className: "cs-ab-group cs-ab-desktop-group", children: [_jsx(AbBtn, { label: t('appbar.new'), onClick: handleNew, children: _jsx(NewIcon, { size: 18 }) }), _jsx(AbBtn, { label: t('appbar.open'), shortcut: "Ctrl+O", onClick: () => { void openFile(); }, children: _jsx(OpenIcon, { size: 18 }) }), _jsx(AbBtn, { label: t('appbar.importDocx'), onClick: () => { void handleImportDocx(); }, children: _jsx(ImportDocxIcon, { size: 18 }) }), _jsx("span", { className: "cs-ab-sep" }), _jsx(AbBtn, { label: t('appbar.save'), shortcut: "Ctrl+S", variant: "primary", onClick: () => { void saveFile(); }, children: _jsx(SaveIcon, { size: 17 }) }), _jsx(AbBtn, { label: t('appbar.saveAs'), shortcut: "Ctrl+Shift+S", onClick: () => { void saveFileAs(); }, children: _jsx(SaveAsIcon, { size: 17 }) }), _jsx("span", { className: "cs-ab-sep" }), _jsx(AbBtn, { label: t('appbar.export'), variant: "secondary", active: exportVisible, onClick: () => setExportVisible(!exportVisible), children: _jsx(ExportIcon, { size: 18 }) }), _jsx(AbBtn, { label: t('appbar.style'), active: styleVisible, onClick: () => setStyleVisible(!styleVisible), children: _jsx(StyleIcon, { size: 18 }) }), _jsx(AbBtn, { label: t('settings.title'), active: settingsVisible, onClick: () => setSettingsVisible(!settingsVisible), children: _jsx(SettingsIcon, { size: 18 }) })] }), _jsxs("div", { className: "cs-ab-group cs-ab-mobile-controls", children: [_jsx(AbBtn, { label: t('appbar.save'), variant: isDirty ? 'primary' : undefined, onClick: () => { void saveFile(); }, children: _jsx(SaveIcon, { size: 17 }) }), _jsx(AbBtn, { label: "Menu", active: mobileMenuOpen, onClick: () => setMobileMenuOpen(!mobileMenuOpen), children: mobileMenuOpen ? _jsx(CloseIcon, { size: 18 }) : _jsx(MenuIcon, { size: 18 }) })] }), mobileMenuOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "cs-mobile-menu-backdrop", "aria-hidden": "true", onClick: () => setMobileMenuOpen(false) }), _jsxs("div", { className: "cs-mobile-menu-drawer", role: "dialog", "aria-modal": "true", "aria-label": "Application Menu", children: [_jsxs("div", { className: "cs-mm-header", children: [_jsxs("div", { className: "cs-mm-brand", children: [_jsx(SutrataLogo, { size: 22, showText: false }), _jsx("span", { children: "Sutrata" })] }), _jsx("button", { type: "button", className: "cs-mm-close", "aria-label": "Close menu", onClick: () => setMobileMenuOpen(false), children: _jsx(CloseIcon, { size: 18 }) })] }), _jsxs("div", { className: "cs-mm-file-info", children: [_jsx("span", { className: "cs-mm-file-name", children: name }), isDirty && _jsx("span", { className: "cs-ab-dirty", title: "Unsaved edits" })] }), _jsxs("div", { className: "cs-mm-section", children: [_jsx("div", { className: "cs-mm-section-title", children: "Views & Tools" }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { setMode(nextMode); setMobileMenuOpen(false); }, children: [mode === 'formatted' ? _jsx(SourceIcon, { size: 18 }) : _jsx(FormattedIcon, { size: 18 }), _jsx("span", { children: mode === 'formatted' ? t('appbar.switchToSource') : t('appbar.switchToFormatted') })] }), _jsxs("button", { type: "button", className: `cs-mm-item${navVisible ? ' cs-mm-active' : ''}`, onClick: () => { setNavVisible(!navVisible); setMobileMenuOpen(false); }, children: [_jsx(NavigatorIcon, { size: 18 }), _jsx("span", { children: t('appbar.navigator') })] }), _jsxs("button", { type: "button", className: `cs-mm-item${ribbonVisible ? ' cs-mm-active' : ''}`, onClick: () => { setRibbonVisible(!ribbonVisible); setMobileMenuOpen(false); }, children: [_jsx(RibbonIcon, { size: 18 }), _jsx("span", { children: ribbonVisible ? t('appbar.hideToolbar') : t('appbar.showToolbar') })] }), _jsxs("button", { type: "button", className: `cs-mm-item${voiceActive ? ' cs-mm-active' : ''}`, onClick: () => { setVoiceActive(!voiceActive); setMobileMenuOpen(false); }, children: [_jsx(MicIcon, { size: 18 }), _jsx("span", { children: "Voice Dictation" })] }), _jsxs("button", { type: "button", className: `cs-mm-item${metadataVisible ? ' cs-mm-active' : ''}`, onClick: () => { setMetadataVisible(!metadataVisible); setMobileMenuOpen(false); }, children: [_jsx("span", { style: { fontWeight: 700, width: 18, textAlign: 'center' }, children: "&" }), _jsx("span", { children: metadataVisible ? 'Hide Metadata' : 'Show Metadata' })] })] }), _jsxs("div", { className: "cs-mm-section", children: [_jsx("div", { className: "cs-mm-section-title", children: "File Operations" }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { openTitlePageForm(); setMobileMenuOpen(false); }, children: [_jsx(TitlePageIcon, { size: 18 }), _jsx("span", { children: t('appbar.titlePage') })] }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { handleNew(); setMobileMenuOpen(false); }, children: [_jsx(NewIcon, { size: 18 }), _jsx("span", { children: t('appbar.new') })] }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { void openFile(); setMobileMenuOpen(false); }, children: [_jsx(OpenIcon, { size: 18 }), _jsx("span", { children: t('appbar.open') })] }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { void handleImportDocx(); setMobileMenuOpen(false); }, children: [_jsx(ImportDocxIcon, { size: 18 }), _jsx("span", { children: t('appbar.importDocx') })] }), _jsxs("button", { type: "button", className: "cs-mm-item cs-mm-primary", onClick: () => { void saveFile(); setMobileMenuOpen(false); }, children: [_jsx(SaveIcon, { size: 18 }), _jsx("span", { children: t('appbar.save') })] }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { void saveFileAs(); setMobileMenuOpen(false); }, children: [_jsx(SaveAsIcon, { size: 18 }), _jsx("span", { children: t('appbar.saveAs') })] }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { setExportVisible(true); setMobileMenuOpen(false); }, children: [_jsx(ExportIcon, { size: 18 }), _jsx("span", { children: t('appbar.export') })] })] }), _jsxs("div", { className: "cs-mm-section", children: [_jsx("div", { className: "cs-mm-section-title", children: "Preferences" }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { setStyleVisible(true); setMobileMenuOpen(false); }, children: [_jsx(StyleIcon, { size: 18 }), _jsx("span", { children: t('appbar.style') })] }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { setSettingsVisible(true); setMobileMenuOpen(false); }, children: [_jsx(SettingsIcon, { size: 18 }), _jsx("span", { children: t('settings.title') })] })] })] })] }))] }));
|
|
52
|
+
return (_jsxs("header", { className: "cs-app-bar", role: "banner", "aria-label": "Application header", children: [_jsxs("div", { className: "cs-ab-group cs-ab-left", children: [_jsxs("div", { className: "cs-ab-brand", title: "Sutrata Multilingual Screenplay Editor", children: [_jsx(SutrataLogo, { size: 20, showText: false }), _jsx("span", { className: "cs-ab-brand-title", children: "Sutrata" })] }), _jsx("span", { className: "cs-ab-sep cs-ab-desktop-only" }), _jsx(AbBtn, { label: t('appbar.navigator'), shortcut: "Ctrl+Shift+B", active: navVisible, onClick: () => setNavVisible(!navVisible), children: _jsx(NavigatorIcon, { size: 18 }) }), canEdit && (_jsx("span", { className: "cs-ab-desktop-only", children: _jsx(AbBtn, { label: t('appbar.titlePage'), onClick: openTitlePageForm, children: _jsx(TitlePageIcon, { size: 18 }) }) })), _jsxs("div", { className: "cs-ab-file-pill", title: filePath ?? 'untitled.sutra', children: [_jsx("span", { className: "cs-ab-file-name", children: name }), isDirty && _jsx("span", { className: "cs-ab-dirty", title: "Unsaved edits" })] })] }), _jsx("span", { className: "cs-ab-spacer cs-ab-desktop-only" }), _jsxs("div", { className: "cs-ab-group cs-ab-desktop-group", children: [_jsx(AbBtn, { label: mode === 'formatted' ? t('appbar.switchToSource') : t('appbar.switchToFormatted'), shortcut: "Ctrl+Shift+E", onClick: () => setMode(nextMode), children: mode === 'formatted' ? _jsx(SourceIcon, { size: 18 }) : _jsx(FormattedIcon, { size: 18 }) }), canEdit && (_jsx(AbBtn, { label: ribbonVisible ? t('appbar.hideToolbar') : t('appbar.showToolbar'), shortcut: "Ctrl+Shift+H", active: ribbonVisible, onClick: () => setRibbonVisible(!ribbonVisible), children: _jsx(RibbonIcon, { size: 18 }) })), speech && (_jsx(AbBtn, { label: "Voice Dictation", shortcut: "Ctrl+Shift+V", active: voiceActive, onClick: () => setVoiceActive(!voiceActive), children: _jsx(MicIcon, { size: 18 }) })), _jsx(AbBtn, { label: metadataVisible ? 'Hide Metadata' : 'Show Metadata', active: metadataVisible, onClick: () => setMetadataVisible(!metadataVisible), children: _jsx("span", { style: { fontWeight: 700, fontSize: '13px' }, children: "&" }) })] }), _jsx("span", { className: "cs-ab-spacer cs-ab-desktop-only" }), _jsxs("div", { className: "cs-ab-group cs-ab-desktop-group", children: [canEdit && (_jsxs(_Fragment, { children: [_jsx(AbBtn, { label: t('appbar.new'), onClick: handleNew, children: _jsx(NewIcon, { size: 18 }) }), _jsx(AbBtn, { label: t('appbar.open'), shortcut: "Ctrl+O", onClick: () => { void openFile(); }, children: _jsx(OpenIcon, { size: 18 }) }), _jsx(AbBtn, { label: t('appbar.importDocx'), onClick: () => { void handleImportDocx(); }, children: _jsx(ImportDocxIcon, { size: 18 }) }), ai && (_jsx(AbBtn, { label: "Import with AI", onClick: () => setAiImportOpen(true), children: _jsx(SparklesIcon, { size: 18 }) })), _jsx("span", { className: "cs-ab-sep" }), _jsx(AbBtn, { label: t('appbar.save'), shortcut: "Ctrl+S", variant: "primary", onClick: () => { void saveFile(); }, children: _jsx(SaveIcon, { size: 17 }) }), _jsx(AbBtn, { label: t('appbar.saveAs'), shortcut: "Ctrl+Shift+S", onClick: () => { void saveFileAs(); }, children: _jsx(SaveAsIcon, { size: 17 }) }), _jsx("span", { className: "cs-ab-sep" })] })), _jsx(AbBtn, { label: t('appbar.export'), variant: "secondary", active: exportVisible, onClick: () => setExportVisible(!exportVisible), children: _jsx(ExportIcon, { size: 18 }) }), canEdit && (_jsx(AbBtn, { label: t('appbar.style'), active: styleVisible, onClick: () => setStyleVisible(!styleVisible), children: _jsx(StyleIcon, { size: 18 }) })), (appbarCommands.length > 0 || appbarPanels.length > 0) && _jsx("span", { className: "cs-ab-sep" }), appbarCommands.map(c => (_jsx(AbBtn, { label: c.label, shortcut: displayShortcut(c.shortcut ?? c.shortcutHint), onClick: () => c.run(commandContext()), children: c.icon ? c.icon(18) : _jsx("span", { className: "cs-ab-text-label", children: c.label }) }, c.id))), appbarPanels.map(p => (_jsx("span", { className: "cs-ab-panel", "aria-label": p.title, children: p.render(panelContext) }, p.id))), _jsx(AbBtn, { label: t('settings.title'), active: settingsVisible, onClick: () => setSettingsVisible(!settingsVisible), children: _jsx(SettingsIcon, { size: 18 }) })] }), _jsxs("div", { className: "cs-ab-group cs-ab-mobile-controls", children: [canEdit && (_jsx(AbBtn, { label: t('appbar.save'), variant: isDirty ? 'primary' : undefined, onClick: () => { void saveFile(); }, children: _jsx(SaveIcon, { size: 17 }) })), _jsx(AbBtn, { label: "Menu", active: mobileMenuOpen, onClick: () => setMobileMenuOpen(!mobileMenuOpen), children: mobileMenuOpen ? _jsx(CloseIcon, { size: 18 }) : _jsx(MenuIcon, { size: 18 }) })] }), mobileMenuOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "cs-mobile-menu-backdrop", "aria-hidden": "true", onClick: () => setMobileMenuOpen(false) }), _jsxs("div", { className: "cs-mobile-menu-drawer", role: "dialog", "aria-modal": "true", "aria-label": "Application Menu", children: [_jsxs("div", { className: "cs-mm-header", children: [_jsxs("div", { className: "cs-mm-brand", children: [_jsx(SutrataLogo, { size: 22, showText: false }), _jsx("span", { children: "Sutrata" })] }), _jsx("button", { type: "button", className: "cs-mm-close", "aria-label": "Close menu", onClick: () => setMobileMenuOpen(false), children: _jsx(CloseIcon, { size: 18 }) })] }), _jsxs("div", { className: "cs-mm-file-info", children: [_jsx("span", { className: "cs-mm-file-name", children: name }), isDirty && _jsx("span", { className: "cs-ab-dirty", title: "Unsaved edits" })] }), _jsxs("div", { className: "cs-mm-section", children: [_jsx("div", { className: "cs-mm-section-title", children: "Views & Tools" }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { setMode(nextMode); setMobileMenuOpen(false); }, children: [mode === 'formatted' ? _jsx(SourceIcon, { size: 18 }) : _jsx(FormattedIcon, { size: 18 }), _jsx("span", { children: mode === 'formatted' ? t('appbar.switchToSource') : t('appbar.switchToFormatted') })] }), _jsxs("button", { type: "button", className: `cs-mm-item${navVisible ? ' cs-mm-active' : ''}`, onClick: () => { setNavVisible(!navVisible); setMobileMenuOpen(false); }, children: [_jsx(NavigatorIcon, { size: 18 }), _jsx("span", { children: t('appbar.navigator') })] }), canEdit && _jsxs("button", { type: "button", className: `cs-mm-item${ribbonVisible ? ' cs-mm-active' : ''}`, onClick: () => { setRibbonVisible(!ribbonVisible); setMobileMenuOpen(false); }, children: [_jsx(RibbonIcon, { size: 18 }), _jsx("span", { children: ribbonVisible ? t('appbar.hideToolbar') : t('appbar.showToolbar') })] }), speech && _jsxs("button", { type: "button", className: `cs-mm-item${voiceActive ? ' cs-mm-active' : ''}`, onClick: () => { setVoiceActive(!voiceActive); setMobileMenuOpen(false); }, children: [_jsx(MicIcon, { size: 18 }), _jsx("span", { children: "Voice Dictation" })] }), _jsxs("button", { type: "button", className: `cs-mm-item${metadataVisible ? ' cs-mm-active' : ''}`, onClick: () => { setMetadataVisible(!metadataVisible); setMobileMenuOpen(false); }, children: [_jsx("span", { style: { fontWeight: 700, width: 18, textAlign: 'center' }, children: "&" }), _jsx("span", { children: metadataVisible ? 'Hide Metadata' : 'Show Metadata' })] })] }), canEdit && _jsxs("div", { className: "cs-mm-section", children: [_jsx("div", { className: "cs-mm-section-title", children: "File Operations" }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { openTitlePageForm(); setMobileMenuOpen(false); }, children: [_jsx(TitlePageIcon, { size: 18 }), _jsx("span", { children: t('appbar.titlePage') })] }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { handleNew(); setMobileMenuOpen(false); }, children: [_jsx(NewIcon, { size: 18 }), _jsx("span", { children: t('appbar.new') })] }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { void openFile(); setMobileMenuOpen(false); }, children: [_jsx(OpenIcon, { size: 18 }), _jsx("span", { children: t('appbar.open') })] }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { void handleImportDocx(); setMobileMenuOpen(false); }, children: [_jsx(ImportDocxIcon, { size: 18 }), _jsx("span", { children: t('appbar.importDocx') })] }), ai && _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { setAiImportOpen(true); setMobileMenuOpen(false); }, children: [_jsx(SparklesIcon, { size: 18 }), _jsx("span", { children: "Import with AI" })] }), _jsxs("button", { type: "button", className: "cs-mm-item cs-mm-primary", onClick: () => { void saveFile(); setMobileMenuOpen(false); }, children: [_jsx(SaveIcon, { size: 18 }), _jsx("span", { children: t('appbar.save') })] }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { void saveFileAs(); setMobileMenuOpen(false); }, children: [_jsx(SaveAsIcon, { size: 18 }), _jsx("span", { children: t('appbar.saveAs') })] }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { setExportVisible(true); setMobileMenuOpen(false); }, children: [_jsx(ExportIcon, { size: 18 }), _jsx("span", { children: t('appbar.export') })] })] }), !canEdit && _jsx("div", { className: "cs-mm-section", children: _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { setExportVisible(true); setMobileMenuOpen(false); }, children: [_jsx(ExportIcon, { size: 18 }), _jsx("span", { children: t('appbar.export') })] }) }), appbarCommands.length > 0 && _jsx("div", { className: "cs-mm-section", children: appbarCommands.map(c => (_jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { c.run(commandContext()); setMobileMenuOpen(false); }, children: [c.icon?.(18), _jsx("span", { children: c.label })] }, c.id))) }), _jsxs("div", { className: "cs-mm-section", children: [_jsx("div", { className: "cs-mm-section-title", children: "Preferences" }), canEdit && _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { setStyleVisible(true); setMobileMenuOpen(false); }, children: [_jsx(StyleIcon, { size: 18 }), _jsx("span", { children: t('appbar.style') })] }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { setSettingsVisible(true); setMobileMenuOpen(false); }, children: [_jsx(SettingsIcon, { size: 18 }), _jsx("span", { children: t('settings.title') })] })] })] })] })), aiImportOpen && _jsx(AIImportDialog, { onClose: () => setAiImportOpen(false) })] }));
|
|
34
53
|
}
|
package/dist/shell/AppShell.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
import { AppBar } from './AppBar';
|
|
4
4
|
import { ElementToolbar } from './ElementToolbar';
|
|
@@ -16,15 +16,29 @@ import { Watermark } from './Watermark';
|
|
|
16
16
|
import { ConfirmDialog } from './ConfirmDialog';
|
|
17
17
|
import { ToastContainer } from './Toast';
|
|
18
18
|
import { VoiceToolbar } from '../ai/VoiceToolbar';
|
|
19
|
-
import {
|
|
19
|
+
import { useSpeech } from '../extensions/speech-provider';
|
|
20
|
+
import { useCanEdit } from '../extensions/session';
|
|
21
|
+
import { usePanels } from '../extensions/panel-registry';
|
|
22
|
+
import { usePanelContext } from './use-panel-context';
|
|
23
|
+
import { useTranslation } from '../i18n/useTranslation';
|
|
20
24
|
import { SelectionMenu } from '../editor/SelectionMenu';
|
|
21
25
|
export function AppShell() {
|
|
22
|
-
const { mode, ribbonVisible, navVisible, setNavVisible, exportVisible, settingsVisible, setSettingsVisible, styleVisible, setStyleVisible, metadataVisible, voiceActive,
|
|
26
|
+
const { mode, ribbonVisible, navVisible, setNavVisible, exportVisible, settingsVisible, setSettingsVisible, styleVisible, setStyleVisible, metadataVisible, voiceActive, confirmModal, closeConfirm, toasts, dismissToast, } = useDocument();
|
|
23
27
|
const [titlePageVisible, setTitlePageVisible] = useState(false);
|
|
28
|
+
const speech = useSpeech();
|
|
29
|
+
const canEdit = useCanEdit();
|
|
30
|
+
const { t } = useTranslation();
|
|
31
|
+
const panelContext = usePanelContext();
|
|
32
|
+
// The scene navigator is the built-in first sidebar tab; embedder sidebar
|
|
33
|
+
// panels become further tabs. Inspector panels stack on the right.
|
|
34
|
+
const sidebarPanels = usePanels('sidebar');
|
|
35
|
+
const inspectorPanels = usePanels('inspector');
|
|
36
|
+
const [sidebarTab, setSidebarTab] = useState('navigator');
|
|
37
|
+
const activeSidebar = sidebarPanels.find(p => p.id === sidebarTab);
|
|
24
38
|
useEffect(() => {
|
|
25
39
|
const open = () => setTitlePageVisible(true);
|
|
26
40
|
window.addEventListener(OPEN_TITLE_PAGE_EVENT, open);
|
|
27
41
|
return () => window.removeEventListener(OPEN_TITLE_PAGE_EVENT, open);
|
|
28
42
|
}, []);
|
|
29
|
-
return (_jsxs("div", { className: `cs-shell ${metadataVisible ? '' : 'cs-hide-metadata'}`, children: [_jsx(Watermark, {}), _jsx(AppBar, {}), voiceActive && _jsx(VoiceToolbar, {}), ribbonVisible && _jsx(ElementToolbar, {}), _jsxs("div", { className: "cs-shell-body", children: [navVisible && (_jsxs(_Fragment, { children: [_jsx("div", { className: "cs-nav-backdrop", "aria-hidden": "true", onClick: () => setNavVisible(false) }), _jsx(SceneNavigator, {})] })), _jsx("div", { className: "cs-editor-area", children: _jsx("main", { className: "cs-main", children: mode === 'formatted' ? _jsx(EditorView, {}) : _jsx(SourceView, {}) }) })] }), _jsx(StatusBar, {}), _jsx(FindReplace, {}), exportVisible && _jsx(ExportDialog, {}), titlePageVisible && _jsx(TitlePageDialog, { onClose: () => setTitlePageVisible(false) }), settingsVisible && _jsx(SettingsDialog, { onClose: () => setSettingsVisible(false) }), styleVisible &&
|
|
43
|
+
return (_jsxs("div", { className: `cs-shell ${metadataVisible ? '' : 'cs-hide-metadata'}`, children: [_jsx(Watermark, {}), _jsx(AppBar, {}), voiceActive && speech && _jsx(VoiceToolbar, {}), ribbonVisible && canEdit && _jsx(ElementToolbar, {}), _jsxs("div", { className: "cs-shell-body", children: [navVisible && (_jsxs(_Fragment, { children: [_jsx("div", { className: "cs-nav-backdrop", "aria-hidden": "true", onClick: () => setNavVisible(false) }), sidebarPanels.length === 0 ? (_jsx(SceneNavigator, {})) : (_jsxs("div", { className: "cs-sidebar", children: [_jsx("div", { className: "cs-sidebar-tabs", role: "tablist", "aria-label": "Sidebar", children: [{ id: 'navigator', title: t('navigator.title') }, ...sidebarPanels].map(p => (_jsx("button", { type: "button", role: "tab", "aria-selected": (activeSidebar?.id ?? 'navigator') === p.id, className: `cs-sidebar-tab${(activeSidebar?.id ?? 'navigator') === p.id ? ' cs-sidebar-tab-active' : ''}`, onClick: () => setSidebarTab(p.id), children: p.title }, p.id))) }), activeSidebar ? (_jsx("div", { className: "cs-sidebar-panel", role: "tabpanel", "aria-label": activeSidebar.title, children: activeSidebar.render(panelContext) })) : (_jsx(SceneNavigator, {}))] }))] })), _jsx("div", { className: "cs-editor-area", children: _jsx("main", { className: "cs-main", children: mode === 'formatted' ? _jsx(EditorView, {}) : _jsx(SourceView, {}) }) }), inspectorPanels.length > 0 && (_jsx("aside", { className: "cs-inspector", "aria-label": "Inspector", children: inspectorPanels.map(p => (_jsxs("section", { className: "cs-inspector-section", "aria-label": p.title, children: [_jsx("div", { className: "cs-inspector-title", children: p.title }), p.render(panelContext)] }, p.id))) }))] }), _jsx(StatusBar, {}), _jsx(FindReplace, {}), exportVisible && _jsx(ExportDialog, {}), titlePageVisible && canEdit && _jsx(TitlePageDialog, { onClose: () => setTitlePageVisible(false) }), settingsVisible && _jsx(SettingsDialog, { onClose: () => setSettingsVisible(false) }), styleVisible && canEdit && _jsx(StyleDialog, { onClose: () => setStyleVisible(false) }), confirmModal && _jsx(ConfirmDialog, { options: confirmModal, onClose: closeConfirm }), _jsx(ToastContainer, { toasts: toasts, onDismiss: dismissToast }), canEdit && _jsx(SelectionMenu, {})] }));
|
|
30
44
|
}
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Renders every command registered with `menu: 'toolbar'` — the editor's
|
|
4
|
+
* built-ins (shell/builtin-commands.tsx) and the embedder's — grouped, with a
|
|
5
|
+
* separator between groups.
|
|
6
|
+
*/
|
|
2
7
|
export declare function ElementToolbar(): React.JSX.Element;
|
|
@@ -1,30 +1,40 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
3
|
import { ToolbarButton } from './ToolbarButton';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
4
|
+
import { subscribe } from '../editor/editor-bus';
|
|
5
|
+
import { OverflowIcon, CloseIcon } from './icons';
|
|
6
|
+
import { useCommands, useCommandContext, displayShortcut } from './use-commands';
|
|
7
|
+
/** Toolbar groups in display order; embedder groups follow. */
|
|
8
|
+
const GROUP_ORDER = ['history', 'elements', 'marks'];
|
|
9
|
+
function groupsOf(commands) {
|
|
10
|
+
const groups = new Map();
|
|
11
|
+
for (const c of commands) {
|
|
12
|
+
const g = c.group ?? 'extensions';
|
|
13
|
+
groups.set(g, [...(groups.get(g) ?? []), c]);
|
|
14
|
+
}
|
|
15
|
+
const names = [...GROUP_ORDER.filter(g => groups.has(g)), ...[...groups.keys()].filter(g => !GROUP_ORDER.includes(g))];
|
|
16
|
+
return names.map(g => groups.get(g));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Renders every command registered with `menu: 'toolbar'` — the editor's
|
|
20
|
+
* built-ins (shell/builtin-commands.tsx) and the embedder's — grouped, with a
|
|
21
|
+
* separator between groups.
|
|
22
|
+
*/
|
|
21
23
|
export function ElementToolbar() {
|
|
22
|
-
const [
|
|
24
|
+
const [, setTick] = useState(0);
|
|
23
25
|
const [overflowOpen, setOverflowOpen] = useState(false);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
const commands = useCommands().filter(c => c.menu === 'toolbar');
|
|
27
|
+
const context = useCommandContext();
|
|
28
|
+
// Re-render on selection changes so isActive() stays current.
|
|
29
|
+
useEffect(() => subscribe(() => setTick(n => n + 1)), []);
|
|
30
|
+
const ctx = context();
|
|
31
|
+
const groups = groupsOf(commands);
|
|
32
|
+
const inline = commands.filter(c => c.group === 'marks' || c.group === 'history');
|
|
33
|
+
const elements = commands.filter(c => c.group === 'elements');
|
|
34
|
+
const others = commands.filter(c => !['marks', 'history', 'elements'].includes(c.group ?? ''));
|
|
35
|
+
const run = (c) => { c.run(context()); setOverflowOpen(false); };
|
|
36
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "cs-element-toolbar", role: "toolbar", "aria-label": "Editing toolbar", children: [groups.map((group, gi) => (_jsxs(React.Fragment, { children: [gi > 0 && _jsx("span", { className: "cs-tb-sep" }), group.map(c => (_jsx(ToolbarButton, { label: c.label, sigil: c.sigil, shortcut: displayShortcut(c.shortcut ?? c.shortcutHint), active: c.isActive?.(ctx) ?? false, onClick: () => c.run(context()), children: c.icon ? c.icon(20) : _jsx("span", { className: "cs-tb-text-label", children: c.label }) }, c.id)))] }, group[0].group ?? `g${gi}`))), _jsx("button", { type: "button", className: `cs-tb-overflow${overflowOpen ? ' cs-tb-overflow-active' : ''}`, "aria-label": "More", "aria-expanded": overflowOpen, onClick: () => setOverflowOpen(!overflowOpen), children: _jsx(OverflowIcon, { size: 18 }) })] }), overflowOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "cs-tb-overflow-backdrop", "aria-hidden": "true", onClick: () => setOverflowOpen(false) }), _jsxs("div", { className: "cs-tb-overflow-sheet", role: "dialog", "aria-modal": "true", "aria-label": "Screenplay Elements & Formatting", children: [_jsxs("div", { className: "cs-tb-overflow-header", children: [_jsx("span", { className: "cs-tb-overflow-title", children: "Formatting & Elements" }), _jsx("button", { type: "button", className: "cs-tb-overflow-close", "aria-label": "Close", onClick: () => setOverflowOpen(false), children: _jsx(CloseIcon, { size: 18 }) })] }), _jsx("div", { className: "cs-tb-overflow-section-label", children: "Inline Formatting" }), _jsx("div", { className: "cs-tb-overflow-inline-grid", children: inline.map(c => (_jsxs("button", { type: "button", className: "cs-tb-overflow-item", onClick: () => run(c), children: [c.icon?.(18), _jsx("span", { children: c.label })] }, c.id))) }), _jsx("div", { className: "cs-tb-overflow-section-label", children: "Screenplay Elements" }), _jsx("div", { className: "cs-tb-overflow-list", children: [...elements, ...others].map(c => {
|
|
37
|
+
const active = c.isActive?.(ctx) ?? false;
|
|
38
|
+
return (_jsxs("button", { type: "button", className: `cs-tb-overflow-list-item${active ? ' cs-tb-overflow-item-active' : ''}`, onClick: () => run(c), children: [_jsx("div", { className: "cs-tb-overflow-list-icon", children: c.icon?.(18) }), _jsxs("div", { className: "cs-tb-overflow-list-text", children: [_jsx("span", { className: "cs-tb-overflow-list-name", children: c.label }), c.sigil && _jsx("span", { className: "cs-tb-overflow-list-sigil", children: c.sigil })] }), active && _jsx("span", { className: "cs-tb-overflow-current-badge", children: "Active" })] }, c.id));
|
|
39
|
+
}) })] })] }))] }));
|
|
30
40
|
}
|
|
@@ -3,6 +3,7 @@ import { useState, useEffect, useRef } from 'react';
|
|
|
3
3
|
import { useLanguage } from '../i18n/LanguageContext';
|
|
4
4
|
import { useDocument } from '../context/DocumentContext';
|
|
5
5
|
import { getEditorView } from '../editor/editor-bus';
|
|
6
|
+
import { useCanEdit } from '../extensions/session';
|
|
6
7
|
const LANG_NAMES = {
|
|
7
8
|
en: 'English',
|
|
8
9
|
hi: 'Hindi (Devanagari)',
|
|
@@ -19,6 +20,7 @@ const LANG_NAMES = {
|
|
|
19
20
|
export function LanguageStatus() {
|
|
20
21
|
const { currentLanguage } = useLanguage();
|
|
21
22
|
const { text, setText } = useDocument();
|
|
23
|
+
const canEdit = useCanEdit();
|
|
22
24
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
23
25
|
const menuRef = useRef(null);
|
|
24
26
|
const langName = LANG_NAMES[currentLanguage] ?? currentLanguage;
|
|
@@ -42,6 +44,10 @@ export function LanguageStatus() {
|
|
|
42
44
|
};
|
|
43
45
|
}, [menuOpen]);
|
|
44
46
|
const handleSelectLanguage = (code) => {
|
|
47
|
+
if (!canEdit) {
|
|
48
|
+
setMenuOpen(false);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
45
51
|
const view = getEditorView();
|
|
46
52
|
if (view) {
|
|
47
53
|
const { state, dispatch } = view;
|
package/dist/shell/StatusBar.js
CHANGED
|
@@ -6,14 +6,18 @@ import { buildSceneList } from '../navigator/scene-list';
|
|
|
6
6
|
import { StatisticsDialog } from '../navigator/StatisticsDialog';
|
|
7
7
|
import { VersionHistory } from './VersionHistory';
|
|
8
8
|
import { StatsIcon, HistoryIcon } from './icons';
|
|
9
|
-
import { subscribeToAICalls, formatActiveCalls } from '../ai/ai-client';
|
|
10
9
|
import { useTranslation } from '../i18n/useTranslation';
|
|
10
|
+
import { useAI, formatActivity } from '../extensions/ai-provider';
|
|
11
|
+
import { useCanEdit } from '../extensions/session';
|
|
11
12
|
function AIStatusBarStatus() {
|
|
13
|
+
const ai = useAI();
|
|
12
14
|
const [activeCalls, setActiveCalls] = useState([]);
|
|
13
15
|
useEffect(() => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
if (!ai?.subscribeActivity)
|
|
17
|
+
return;
|
|
18
|
+
return ai.subscribeActivity(setActiveCalls);
|
|
19
|
+
}, [ai]);
|
|
20
|
+
if (!ai || activeCalls.length === 0)
|
|
17
21
|
return null;
|
|
18
22
|
return (_jsxs("div", { className: "cs-ai-status-indicator", style: {
|
|
19
23
|
display: 'inline-flex',
|
|
@@ -23,11 +27,12 @@ function AIStatusBarStatus() {
|
|
|
23
27
|
fontWeight: 600,
|
|
24
28
|
fontSize: '11px',
|
|
25
29
|
marginRight: '8px',
|
|
26
|
-
}, children: [_jsx("span", { className: "cs-spinner", style: { width: '10px', height: '10px', borderWidth: '1.5px' } }), _jsx("span", { children:
|
|
30
|
+
}, children: [_jsx("span", { className: "cs-spinner", style: { width: '10px', height: '10px', borderWidth: '1.5px' } }), _jsx("span", { children: formatActivity(activeCalls) })] }));
|
|
27
31
|
}
|
|
28
32
|
export function StatusBar() {
|
|
29
33
|
const { text, isDirty, lastSaveTarget, filePath, versions, versionHistoryVisible, setVersionHistoryVisible, restoreVersion, clearVersionHistory } = useDocument();
|
|
30
34
|
const { t } = useTranslation();
|
|
35
|
+
const canEdit = useCanEdit();
|
|
31
36
|
const [statsOpen, setStatsOpen] = useState(false);
|
|
32
37
|
// VS Code-style: a local autosave backs the content up but never counts as
|
|
33
38
|
// "saved" in the main label — only a real write to the file (saveFile/
|
|
@@ -50,5 +55,5 @@ export function StatusBar() {
|
|
|
50
55
|
const estPages = Math.max(1, Math.round(lines / 54));
|
|
51
56
|
return { wordCount: words, pageEstimate: estPages };
|
|
52
57
|
}, [text]);
|
|
53
|
-
return (_jsxs(_Fragment, { children: [_jsxs("footer", { className: "cs-status-bar", role: "contentinfo", "aria-label": "Status bar", children: [_jsxs("div", { className: "cs-sb-left", children: [_jsxs("span", { className: "cs-sb-metric cs-sb-metric-scenes", title: t('statusbar.scenesTooltip'), children: [_jsx("strong", { children: scenes.length }), " ", _jsx("span", { className: "cs-sb-metric-label", children: scenes.length === 1 ? t('statusbar.scene') : t('statusbar.scenes') })] }), _jsx("span", { className: "cs-sb-sep", children: "\u2022" }), _jsxs("span", { className: "cs-sb-metric cs-sb-metric-pages", title: t('statusbar.pagesTooltip'), children: [_jsxs("strong", { children: ["~", pageEstimate] }), " ", _jsx("span", { className: "cs-sb-metric-label", children: pageEstimate === 1 ? t('statusbar.page') : t('statusbar.pages') })] }), _jsx("span", { className: "cs-sb-sep cs-sb-words-sep", children: "\u2022" }), _jsxs("span", { className: "cs-sb-metric cs-sb-metric-words", title: t('statusbar.wordsTooltip'), children: [_jsx("strong", { children: wordCount.toLocaleString() }), " ", _jsx("span", { className: "cs-sb-metric-label", children: t('statusbar.words') })] })] }), _jsx("div", { className: "cs-sb-center", children: _jsxs("div", { className: "cs-sb-save", role: "status", "aria-live": "polite", title: saveTooltip, children: [_jsx("span", { className: `cs-sb-dot ${isDirty ? 'cs-sb-dot-dirty' : 'cs-sb-dot-clean'}` }), _jsx("span", { className: isDirty ? 'cs-sb-save-dirty' : 'cs-sb-save-clean', children: saveLabel })] }) }), _jsxs("div", { className: "cs-sb-right", children: [_jsx(AIStatusBarStatus, {}), filePath && versions.length > 0 && (_jsxs("button", { type: "button", className: "cs-nav-action-btn", onClick: () => setVersionHistoryVisible(true), title: `${versions.length} version${versions.length === 1 ? '' : 's'} saved`, style: { fontSize: '11px', gap: '4px', padding: '2px 6px' }, children: [_jsx(HistoryIcon, { size: 13 }), _jsx("span", { children: versions.length })] })), _jsxs("button", { type: "button", className: "cs-nav-action-btn", onClick: () => setStatsOpen(true), title: t('statusbar.statsTooltip'), style: { fontSize: '11px', gap: '4px', padding: '2px 6px' }, children: [_jsx(StatsIcon, { size: 13 }), _jsx("span", { children: t('statusbar.statsButton') })] }), _jsx(LanguageStatus, {})] })] }), statsOpen && _jsx(StatisticsDialog, { onClose: () => setStatsOpen(false) }), filePath && (_jsx(VersionHistory, { versions: versions, filePath: filePath, onRestore: restoreVersion, onClearAll: clearVersionHistory, visible: versionHistoryVisible, onClose: () => setVersionHistoryVisible(false) }))] }));
|
|
58
|
+
return (_jsxs(_Fragment, { children: [_jsxs("footer", { className: "cs-status-bar", role: "contentinfo", "aria-label": "Status bar", children: [_jsxs("div", { className: "cs-sb-left", children: [_jsxs("span", { className: "cs-sb-metric cs-sb-metric-scenes", title: t('statusbar.scenesTooltip'), children: [_jsx("strong", { children: scenes.length }), " ", _jsx("span", { className: "cs-sb-metric-label", children: scenes.length === 1 ? t('statusbar.scene') : t('statusbar.scenes') })] }), _jsx("span", { className: "cs-sb-sep", children: "\u2022" }), _jsxs("span", { className: "cs-sb-metric cs-sb-metric-pages", title: t('statusbar.pagesTooltip'), children: [_jsxs("strong", { children: ["~", pageEstimate] }), " ", _jsx("span", { className: "cs-sb-metric-label", children: pageEstimate === 1 ? t('statusbar.page') : t('statusbar.pages') })] }), _jsx("span", { className: "cs-sb-sep cs-sb-words-sep", children: "\u2022" }), _jsxs("span", { className: "cs-sb-metric cs-sb-metric-words", title: t('statusbar.wordsTooltip'), children: [_jsx("strong", { children: wordCount.toLocaleString() }), " ", _jsx("span", { className: "cs-sb-metric-label", children: t('statusbar.words') })] })] }), _jsx("div", { className: "cs-sb-center", children: _jsxs("div", { className: "cs-sb-save", role: "status", "aria-live": "polite", title: saveTooltip, children: [_jsx("span", { className: `cs-sb-dot ${isDirty ? 'cs-sb-dot-dirty' : 'cs-sb-dot-clean'}` }), _jsx("span", { className: isDirty ? 'cs-sb-save-dirty' : 'cs-sb-save-clean', children: saveLabel })] }) }), _jsxs("div", { className: "cs-sb-right", children: [_jsx(AIStatusBarStatus, {}), canEdit && filePath && versions.length > 0 && (_jsxs("button", { type: "button", className: "cs-nav-action-btn", onClick: () => setVersionHistoryVisible(true), title: `${versions.length} version${versions.length === 1 ? '' : 's'} saved`, style: { fontSize: '11px', gap: '4px', padding: '2px 6px' }, children: [_jsx(HistoryIcon, { size: 13 }), _jsx("span", { children: versions.length })] })), _jsxs("button", { type: "button", className: "cs-nav-action-btn", onClick: () => setStatsOpen(true), title: t('statusbar.statsTooltip'), style: { fontSize: '11px', gap: '4px', padding: '2px 6px' }, children: [_jsx(StatsIcon, { size: 13 }), _jsx("span", { children: t('statusbar.statsButton') })] }), _jsx(LanguageStatus, {})] })] }), statsOpen && _jsx(StatisticsDialog, { onClose: () => setStatsOpen(false) }), canEdit && filePath && (_jsx(VersionHistory, { versions: versions, filePath: filePath, onRestore: restoreVersion, onClearAll: clearVersionHistory, visible: versionHistoryVisible, onClose: () => setVersionHistoryVisible(false) }))] }));
|
|
54
59
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CommandContribution } from '../extensions/command-registry';
|
|
2
|
+
type Translate = (key: string) => string;
|
|
3
|
+
/**
|
|
4
|
+
* The editor's own commands, registered like an embedder's: undo/redo,
|
|
5
|
+
* element types (with their Alt+Shift shortcuts) and inline marks, all shown
|
|
6
|
+
* in the element toolbar. Mark and history keys are bound by the ProseMirror
|
|
7
|
+
* keymap, so they only carry a `shortcutHint`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function builtinCommands(t: Translate): CommandContribution[];
|
|
10
|
+
export {};
|