@sutrata/editor 0.1.2 → 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.
Files changed (118) hide show
  1. package/dist/ai/VoiceConfirmDialog.d.ts +1 -0
  2. package/dist/ai/VoiceConfirmDialog.js +6 -6
  3. package/dist/ai/VoiceToolbar.d.ts +1 -0
  4. package/dist/ai/VoiceToolbar.js +22 -16
  5. package/dist/ai/clean-response.d.ts +7 -0
  6. package/dist/ai/clean-response.js +58 -0
  7. package/dist/ai/format-transcript.d.ts +3 -0
  8. package/dist/ai/format-transcript.js +7 -0
  9. package/dist/ai/prompts.d.ts +8 -1
  10. package/dist/ai/prompts.js +34 -7
  11. package/dist/autocomplete/suggestions.js +1 -1
  12. package/dist/context/DocumentContext.d.ts +28 -4
  13. package/dist/context/DocumentContext.js +67 -37
  14. package/dist/editor/EditorView.js +32 -4
  15. package/dist/editor/SelectionMenu.js +5 -3
  16. package/dist/editor/external-decorations-plugin.d.ts +23 -0
  17. package/dist/editor/external-decorations-plugin.js +151 -0
  18. package/dist/editor/omitted-scene-plugin.d.ts +9 -0
  19. package/dist/editor/omitted-scene-plugin.js +44 -0
  20. package/dist/editor/plugins.js +4 -0
  21. package/dist/editor/private-metadata-plugin.d.ts +10 -0
  22. package/dist/editor/private-metadata-plugin.js +60 -0
  23. package/dist/editor/prosemirror-to-sutra.js +179 -74
  24. package/dist/editor/scene-heading-view.js +2 -0
  25. package/dist/editor/schema.js +69 -12
  26. package/dist/editor/sutra-to-prosemirror.d.ts +2 -0
  27. package/dist/editor/sutra-to-prosemirror.js +32 -34
  28. package/dist/extensions/ai-provider.d.ts +44 -0
  29. package/dist/extensions/ai-provider.js +27 -0
  30. package/dist/extensions/collab.d.ts +21 -0
  31. package/dist/extensions/collab.js +6 -0
  32. package/dist/extensions/command-registry.d.ts +49 -0
  33. package/dist/extensions/command-registry.js +28 -0
  34. package/dist/extensions/decorations.d.ts +48 -0
  35. package/dist/extensions/decorations.js +6 -0
  36. package/dist/extensions/export-registry.d.ts +59 -0
  37. package/dist/extensions/export-registry.js +10 -0
  38. package/dist/extensions/panel-registry.d.ts +30 -0
  39. package/dist/extensions/panel-registry.js +16 -0
  40. package/dist/extensions/registries.d.ts +15 -0
  41. package/dist/extensions/registries.js +12 -0
  42. package/dist/extensions/registry-store.d.ts +26 -0
  43. package/dist/extensions/registry-store.js +35 -0
  44. package/dist/extensions/session.d.ts +29 -0
  45. package/dist/extensions/session.js +20 -0
  46. package/dist/extensions/speech-provider.d.ts +35 -0
  47. package/dist/extensions/speech-provider.js +17 -0
  48. package/dist/file/builtin-exporters.d.ts +9 -0
  49. package/dist/file/builtin-exporters.js +72 -0
  50. package/dist/file/docx-exporter.js +7 -4
  51. package/dist/file/docx-importer.d.ts +9 -0
  52. package/dist/file/docx-importer.js +47 -19
  53. package/dist/file/scene-number.d.ts +5 -0
  54. package/dist/file/scene-number.js +8 -0
  55. package/dist/file/workflow-reports.js +13 -7
  56. package/dist/findreplace/FindReplace.js +5 -2
  57. package/dist/import/AIImportDialog.d.ts +11 -0
  58. package/dist/import/AIImportDialog.js +112 -0
  59. package/dist/import/ai-import.d.ts +51 -0
  60. package/dist/import/ai-import.js +200 -0
  61. package/dist/import/apply-import.d.ts +7 -0
  62. package/dist/import/apply-import.js +35 -0
  63. package/dist/import/extract.d.ts +17 -0
  64. package/dist/import/extract.js +18 -0
  65. package/dist/index.d.ts +17 -2
  66. package/dist/index.js +7 -0
  67. package/dist/locales/bn.json +8 -0
  68. package/dist/locales/en.json +8 -0
  69. package/dist/locales/gu.json +8 -0
  70. package/dist/locales/hi.json +8 -0
  71. package/dist/locales/kn.json +8 -0
  72. package/dist/locales/ml.json +8 -0
  73. package/dist/locales/mr.json +8 -0
  74. package/dist/locales/or.json +8 -0
  75. package/dist/locales/pa.json +8 -0
  76. package/dist/locales/si.json +8 -0
  77. package/dist/locales/ta.json +8 -0
  78. package/dist/locales/te.json +8 -0
  79. package/dist/navigator/SceneNavigator.js +125 -131
  80. package/dist/navigator/StatisticsDialog.js +5 -1
  81. package/dist/navigator/scene-edits.d.ts +18 -0
  82. package/dist/navigator/scene-edits.js +143 -0
  83. package/dist/navigator/scene-list.js +4 -4
  84. package/dist/navigator/scene-numbering.d.ts +37 -0
  85. package/dist/navigator/scene-numbering.js +88 -0
  86. package/dist/settings/SettingsDialog.js +8 -120
  87. package/dist/shell/AppBar.js +21 -2
  88. package/dist/shell/AppShell.js +18 -4
  89. package/dist/shell/ElementToolbar.d.ts +5 -0
  90. package/dist/shell/ElementToolbar.js +35 -25
  91. package/dist/shell/LanguageStatus.js +6 -0
  92. package/dist/shell/StatusBar.js +11 -6
  93. package/dist/shell/builtin-commands.d.ts +10 -0
  94. package/dist/shell/builtin-commands.js +43 -0
  95. package/dist/shell/icons.d.ts +6 -0
  96. package/dist/shell/icons.js +6 -0
  97. package/dist/shell/use-commands.d.ts +7 -0
  98. package/dist/shell/use-commands.js +24 -0
  99. package/dist/shell/use-panel-context.d.ts +3 -0
  100. package/dist/shell/use-panel-context.js +39 -0
  101. package/dist/source/SourceView.js +18 -1
  102. package/dist/styles/screenplay.css +113 -1
  103. package/dist/titlepage/ExportDialog.d.ts +5 -0
  104. package/dist/titlepage/ExportDialog.js +59 -96
  105. package/package.json +4 -4
  106. package/dist/ai/OnboardingDialog.d.ts +0 -6
  107. package/dist/ai/OnboardingDialog.js +0 -115
  108. package/dist/ai/ai-client.d.ts +0 -71
  109. package/dist/ai/ai-client.js +0 -494
  110. package/dist/ai/generated-providers.json +0 -3696
  111. package/dist/ai/voice-service.d.ts +0 -15
  112. package/dist/ai/voice-service.js +0 -81
  113. package/dist/editor/character-view.d.ts +0 -35
  114. package/dist/editor/character-view.js +0 -112
  115. package/dist/editor/transliterate-input.d.ts +0 -10
  116. package/dist/editor/transliterate-input.js +0 -88
  117. package/dist/extensions/types.d.ts +0 -73
  118. package/dist/extensions/types.js +0 -12
@@ -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, useState } from 'react';
2
+ import { useEffect, useRef } from 'react';
3
3
  import { useTranslation } from '../i18n/useTranslation';
4
- import { useDocument } from '../context/DocumentContext';
5
- import { getAIConfig, saveAIConfig, fetchProviders, getApiKey, setApiKey, deleteApiKey, testConnection, listModels, } from '../ai/ai-client';
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 { setAIOnboardingVisible } = useDocument();
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
- const handleProviderChange = (e) => {
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
  }
@@ -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
  }
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
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 { OnboardingDialog } from '../ai/OnboardingDialog';
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, aiOnboardingVisible, setAIOnboardingVisible, confirmModal, closeConfirm, toasts, dismissToast, } = useDocument();
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 && _jsx(StyleDialog, { onClose: () => setStyleVisible(false) }), aiOnboardingVisible && _jsx(OnboardingDialog, { onClose: () => setAIOnboardingVisible(false) }), confirmModal && _jsx(ConfirmDialog, { options: confirmModal, onClose: closeConfirm }), _jsx(ToastContainer, { toasts: toasts, onDismiss: dismissToast }), _jsx(SelectionMenu, {})] }));
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 { makeSetBlock, makeSetOrToggleNote, makeToggleMark, undo, redo } from './toolbar-actions';
5
- import { runCommand, activeBlockType, subscribe } from '../editor/editor-bus';
6
- import { useTranslation } from '../i18n/useTranslation';
7
- import { UndoIcon, RedoIcon, SceneIcon, ActionIcon, CharacterIcon, DialogueIcon, ParentheticalIcon, TransitionIcon, CenteredIcon, LyricsIcon, NoteIcon, PageBreakIcon, SectionIcon, BoldIcon, ItalicIcon, UnderlineIcon, OverflowIcon, CloseIcon, } from './icons';
8
- const ELEMENTS = [
9
- { type: 'scene_heading', label: 'Scene Heading', sigil: '##', shortcut: 'Alt+Shift+H', Icon: SceneIcon, translationKey: 'toolbar.scene' },
10
- { type: 'action', label: 'Action', sigil: '(plain)', shortcut: 'Alt+Shift+A', Icon: ActionIcon, translationKey: 'toolbar.action' },
11
- { type: 'character', label: 'Character', sigil: '@', shortcut: 'Alt+Shift+C', Icon: CharacterIcon, translationKey: 'toolbar.character' },
12
- { type: 'dialogue', label: 'Dialogue', sigil: '(after @)', shortcut: 'Alt+Shift+D', Icon: DialogueIcon, translationKey: 'toolbar.dialogue' },
13
- { type: 'parenthetical', label: 'Parenthetical', sigil: '( )', shortcut: 'Alt+Shift+P', Icon: ParentheticalIcon, translationKey: 'toolbar.parenthetical' },
14
- { type: 'transition', label: 'Transition', sigil: '>>', shortcut: 'Alt+Shift+X', Icon: TransitionIcon, translationKey: 'toolbar.transition' },
15
- { type: 'centered', label: 'Centered', sigil: '>> <<', shortcut: '', Icon: CenteredIcon, translationKey: 'toolbar.centered' },
16
- { type: 'lyrics', label: 'Lyrics', sigil: '~', shortcut: 'Alt+Shift+L', Icon: LyricsIcon, translationKey: 'toolbar.lyrics' },
17
- { type: 'note', label: 'Note', sigil: '[[ ]]', shortcut: 'Alt+Shift+N', Icon: NoteIcon, translationKey: 'toolbar.note' },
18
- { type: 'page_break', label: 'Page Break', sigil: '===', shortcut: '', Icon: PageBreakIcon, translationKey: 'toolbar.pageBreak' },
19
- { type: 'section', label: 'Section', sigil: '#', shortcut: '', Icon: SectionIcon, translationKey: 'toolbar.section' },
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 [activeType, setActiveType] = useState(null);
24
+ const [, setTick] = useState(0);
23
25
  const [overflowOpen, setOverflowOpen] = useState(false);
24
- const { t } = useTranslation();
25
- useEffect(() => subscribe(() => setActiveType(activeBlockType())), []);
26
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "cs-element-toolbar", role: "toolbar", "aria-label": "Editing toolbar", children: [_jsx(ToolbarButton, { label: t('toolbar.undo'), shortcut: "Mod-Z", onClick: () => runCommand(undo), children: _jsx(UndoIcon, { size: 20 }) }), _jsx(ToolbarButton, { label: t('toolbar.redo'), shortcut: "Mod-Shift-Z", onClick: () => runCommand(redo), children: _jsx(RedoIcon, { size: 20 }) }), _jsx("span", { className: "cs-tb-sep" }), ELEMENTS.map(el => (_jsx(ToolbarButton, { label: t(el.translationKey), sigil: el.sigil, shortcut: el.shortcut || undefined, active: activeType === el.type, onClick: () => runCommand(el.type === 'note' ? makeSetOrToggleNote() : makeSetBlock(el.type)), children: _jsx(el.Icon, { size: 20 }) }, el.type))), _jsx("span", { className: "cs-tb-sep" }), _jsx(ToolbarButton, { label: t('toolbar.bold'), shortcut: "Mod-B", onClick: () => runCommand(makeToggleMark('bold')), children: _jsx(BoldIcon, { size: 20 }) }), _jsx(ToolbarButton, { label: t('toolbar.italic'), shortcut: "Mod-I", onClick: () => runCommand(makeToggleMark('italic')), children: _jsx(ItalicIcon, { size: 20 }) }), _jsx(ToolbarButton, { label: t('toolbar.underline'), shortcut: "Mod-U", onClick: () => runCommand(makeToggleMark('underline')), children: _jsx(UnderlineIcon, { size: 20 }) }), _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" }), _jsxs("div", { className: "cs-tb-overflow-inline-grid", children: [_jsxs("button", { type: "button", className: "cs-tb-overflow-item", onClick: () => { runCommand(makeToggleMark('bold')); setOverflowOpen(false); }, children: [_jsx(BoldIcon, { size: 18 }), _jsx("span", { children: t('toolbar.bold') })] }), _jsxs("button", { type: "button", className: "cs-tb-overflow-item", onClick: () => { runCommand(makeToggleMark('italic')); setOverflowOpen(false); }, children: [_jsx(ItalicIcon, { size: 18 }), _jsx("span", { children: t('toolbar.italic') })] }), _jsxs("button", { type: "button", className: "cs-tb-overflow-item", onClick: () => { runCommand(makeToggleMark('underline')); setOverflowOpen(false); }, children: [_jsx(UnderlineIcon, { size: 18 }), _jsx("span", { children: t('toolbar.underline') })] }), _jsxs("button", { type: "button", className: "cs-tb-overflow-item", onClick: () => { runCommand(undo); setOverflowOpen(false); }, children: [_jsx(UndoIcon, { size: 18 }), _jsx("span", { children: t('toolbar.undo') })] }), _jsxs("button", { type: "button", className: "cs-tb-overflow-item", onClick: () => { runCommand(redo); setOverflowOpen(false); }, children: [_jsx(RedoIcon, { size: 18 }), _jsx("span", { children: t('toolbar.redo') })] })] }), _jsx("div", { className: "cs-tb-overflow-section-label", children: "Screenplay Elements" }), _jsx("div", { className: "cs-tb-overflow-list", children: ELEMENTS.map(el => (_jsxs("button", { type: "button", className: `cs-tb-overflow-list-item${activeType === el.type ? ' cs-tb-overflow-item-active' : ''}`, onClick: () => {
27
- runCommand(el.type === 'note' ? makeSetOrToggleNote() : makeSetBlock(el.type));
28
- setOverflowOpen(false);
29
- }, children: [_jsx("div", { className: "cs-tb-overflow-list-icon", children: _jsx(el.Icon, { size: 18 }) }), _jsxs("div", { className: "cs-tb-overflow-list-text", children: [_jsx("span", { className: "cs-tb-overflow-list-name", children: t(el.translationKey) }), _jsx("span", { className: "cs-tb-overflow-list-sigil", children: el.sigil })] }), activeType === el.type && (_jsx("span", { className: "cs-tb-overflow-current-badge", children: "Active" }))] }, el.type))) })] })] }))] }));
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;
@@ -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
- return subscribeToAICalls(setActiveCalls);
15
- }, []);
16
- if (activeCalls.length === 0)
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: formatActiveCalls(activeCalls) })] }));
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 {};