@sutrata/editor 0.3.0 → 0.4.1

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 (39) hide show
  1. package/dist/ai/VoiceConfirmDialog.js +3 -1
  2. package/dist/ai/VoiceToolbar.js +8 -8
  3. package/dist/context/DocumentContext.js +30 -24
  4. package/dist/editor/SelectionMenu.js +3 -1
  5. package/dist/file/builtin-exporters.js +20 -24
  6. package/dist/findreplace/FindReplace.js +31 -22
  7. package/dist/findreplace/scroll-to-match.d.ts +1 -1
  8. package/dist/findreplace/scroll-to-match.js +12 -2
  9. package/dist/i18n/useTranslation.js +1 -1
  10. package/dist/import/AIImportDialog.js +20 -18
  11. package/dist/locales/bn.json +195 -13
  12. package/dist/locales/en.json +194 -12
  13. package/dist/locales/gu.json +195 -13
  14. package/dist/locales/hi.json +195 -13
  15. package/dist/locales/kn.json +195 -13
  16. package/dist/locales/ml.json +195 -13
  17. package/dist/locales/mr.json +195 -13
  18. package/dist/locales/or.json +195 -13
  19. package/dist/locales/pa.json +194 -12
  20. package/dist/locales/si.json +195 -13
  21. package/dist/locales/ta.json +195 -13
  22. package/dist/locales/te.json +195 -13
  23. package/dist/navigator/SceneNavigator.js +118 -62
  24. package/dist/navigator/StatisticsDialog.js +4 -2
  25. package/dist/navigator/scene-list.d.ts +7 -0
  26. package/dist/navigator/scene-list.js +14 -0
  27. package/dist/shell/AppBar.js +2 -2
  28. package/dist/shell/AppShell.js +1 -1
  29. package/dist/shell/ConfirmDialog.js +4 -2
  30. package/dist/shell/ElementToolbar.js +4 -2
  31. package/dist/shell/LanguageStatus.js +3 -1
  32. package/dist/shell/StatusBar.js +1 -1
  33. package/dist/shell/VersionHistory.js +4 -2
  34. package/dist/source/find-highlight-source.d.ts +1 -1
  35. package/dist/source/find-highlight-source.js +3 -2
  36. package/dist/styles/screenplay.css +12 -0
  37. package/dist/titlepage/ExportDialog.js +6 -6
  38. package/dist/titlepage/TitlePageDialog.js +2 -2
  39. package/package.json +1 -1
@@ -3,7 +3,7 @@ import React, { useEffect, useState, useCallback } from 'react';
3
3
  import { TextSelection } from 'prosemirror-state';
4
4
  import { useDocument } from '../context/DocumentContext';
5
5
  import { useTranslation } from '../i18n/useTranslation';
6
- import { buildSceneList } from './scene-list';
6
+ import { buildSceneList, buildSectionList } from './scene-list';
7
7
  import { getEditorView, getSourceView } from '../editor/editor-bus';
8
8
  import { schema } from '../editor/schema';
9
9
  import { EditorView as CmView } from '@codemirror/view';
@@ -178,15 +178,15 @@ function generateSceneMetadata(ai, sceneText) {
178
178
  * True when the provider can take calls; otherwise points the user at its
179
179
  * setup (if it has one) and returns false.
180
180
  */
181
- async function ensureAIReady(ai, showToast) {
181
+ async function ensureAIReady(ai, showToast, t) {
182
182
  if (await ai.isConfigured())
183
183
  return true;
184
184
  if (ai.openSetup) {
185
- showToast('AI must be set up to generate synopsis & duration. Opening Setup...', 'info');
185
+ showToast(t('navigator.toast.aiSetup'), 'info');
186
186
  ai.openSetup();
187
187
  }
188
188
  else {
189
- showToast(`${ai.displayName} is not available right now.`, 'warn');
189
+ showToast(t('ai.unavailable').replace('{name}', ai.displayName), 'warn');
190
190
  }
191
191
  return false;
192
192
  }
@@ -198,6 +198,7 @@ export function SceneNavigator() {
198
198
  const ai = useAI();
199
199
  const canEdit = useCanEdit();
200
200
  const [scenes, setScenes] = useState([]);
201
+ const [sections, setSections] = useState([]);
201
202
  const [dragIndex, setDragIndex] = useState(null);
202
203
  const [dropIndex, setDropIndex] = useState(null);
203
204
  // New States
@@ -213,6 +214,7 @@ export function SceneNavigator() {
213
214
  // Rebuild scene list whenever text changes
214
215
  useEffect(() => {
215
216
  setScenes(buildSceneList(text));
217
+ setSections(buildSectionList(text));
216
218
  }, [text]);
217
219
  // Numbers that need renumbering (missing/duplicate/out of order; §7.4).
218
220
  // Nothing is flagged until at least one scene has a number.
@@ -222,7 +224,7 @@ export function SceneNavigator() {
222
224
  setIsProcessing(true);
223
225
  setProgressText('Generating synopsis & estimating duration...');
224
226
  try {
225
- if (!ai || !(await ensureAIReady(ai, showToast)))
227
+ if (!ai || !(await ensureAIReady(ai, showToast, t)))
226
228
  return;
227
229
  const idx = scenes.findIndex(s => s.index === scene.index);
228
230
  if (idx === -1)
@@ -241,10 +243,10 @@ export function SceneNavigator() {
241
243
  nextText = updateSceneMetadataInText(nextText, scene.textOffset, 'est-duration', duration.trim());
242
244
  setText(nextText);
243
245
  }
244
- showToast(`Generated synopsis & duration for scene ${scene.id || scene.index}.`, 'success');
246
+ showToast(t('navigator.toast.generatedOne').replace('{scene}', String(scene.id || scene.index)), 'success');
245
247
  }
246
248
  catch (e) {
247
- showToast(`Scene metadata generation failed: ${e.message || String(e)}`, 'error');
249
+ showToast(t('navigator.toast.genFailed').replace('{error}', e.message || String(e)), 'error');
248
250
  }
249
251
  finally {
250
252
  setIsProcessing(false);
@@ -255,7 +257,7 @@ export function SceneNavigator() {
255
257
  setIsProcessing(true);
256
258
  setProgressText('Starting batch synopsis & duration generation...');
257
259
  try {
258
- if (!ai || !(await ensureAIReady(ai, showToast)))
260
+ if (!ai || !(await ensureAIReady(ai, showToast, t)))
259
261
  return;
260
262
  const pmView = getEditorView();
261
263
  const cmView = getSourceView();
@@ -263,7 +265,7 @@ export function SceneNavigator() {
263
265
  let currentScenes = buildSceneList(currentText);
264
266
  const missing = currentScenes.filter(s => !s.synopsis || !s.estDuration);
265
267
  if (missing.length === 0) {
266
- showToast('All scenes already have synopsis and duration.', 'info');
268
+ showToast(t('navigator.toast.allHave'), 'info');
267
269
  return;
268
270
  }
269
271
  let current = 0;
@@ -307,10 +309,10 @@ export function SceneNavigator() {
307
309
  console.warn(`Failed scene metadata for scene ${scene.index}:`, err);
308
310
  }
309
311
  }
310
- showToast(`Generated synopsis & duration for ${missing.length} scenes.`, 'success');
312
+ showToast(t('navigator.toast.generatedMany').replace('{count}', String(missing.length)), 'success');
311
313
  }
312
314
  catch (e) {
313
- showToast(`Batch generation failed: ${e.message || String(e)}`, 'error');
315
+ showToast(t('navigator.toast.batchFailed').replace('{error}', e.message || String(e)), 'error');
314
316
  }
315
317
  finally {
316
318
  setIsProcessing(false);
@@ -389,6 +391,47 @@ export function SceneNavigator() {
389
391
  /* at doc boundary — ignore */
390
392
  }
391
393
  }, [setNavVisible]);
394
+ const handleSectionJump = useCallback((section) => {
395
+ const view = getEditorView();
396
+ if (!view) {
397
+ const cmView = getSourceView();
398
+ if (cmView) {
399
+ cmView.dispatch({
400
+ selection: { anchor: section.textOffset },
401
+ effects: CmView.scrollIntoView(section.textOffset, { y: 'start' }),
402
+ });
403
+ cmView.focus();
404
+ if (typeof window !== 'undefined' && window.innerWidth <= 640)
405
+ setNavVisible(false);
406
+ }
407
+ return;
408
+ }
409
+ try {
410
+ let pos = null;
411
+ let count = 0;
412
+ view.state.doc.forEach((node, offset) => {
413
+ if (pos !== null)
414
+ return;
415
+ if (node.type.name === 'section') {
416
+ count++;
417
+ if (count === section.index)
418
+ pos = offset;
419
+ }
420
+ });
421
+ if (pos === null)
422
+ return;
423
+ view.dispatch(view.state.tr.setSelection(TextSelection.near(view.state.doc.resolve(pos + 1))));
424
+ view.focus();
425
+ const domNode = view.nodeDOM(pos);
426
+ const el = domNode instanceof Element ? domNode : domNode?.parentElement;
427
+ el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
428
+ if (typeof window !== 'undefined' && window.innerWidth <= 640)
429
+ setNavVisible(false);
430
+ }
431
+ catch {
432
+ /* at doc boundary — ignore */
433
+ }
434
+ }, [setNavVisible]);
392
435
  const handleDragStart = useCallback((idx) => {
393
436
  setDragIndex(idx);
394
437
  }, []);
@@ -484,12 +527,12 @@ export function SceneNavigator() {
484
527
  const ids = next.map((n, i) => (n === list[i].id ? undefined : n));
485
528
  const changed = ids.filter(n => n !== undefined).length;
486
529
  if (changed === 0) {
487
- showToast('Scene numbers are already in order.', 'info');
530
+ showToast(t('navigator.toast.inOrder'), 'info');
488
531
  return;
489
532
  }
490
533
  applyTextEdit(setSceneIdsInText(current, ids), setText);
491
- showToast(`Renumbered ${changed} scene${changed === 1 ? '' : 's'}.`, 'success');
492
- }, [liveText, setText, showToast]);
534
+ showToast(t(changed === 1 ? 'navigator.toast.renumberedOne' : 'navigator.toast.renumberedMany').replace('{count}', String(changed)), 'success');
535
+ }, [liveText, setText, showToast, t]);
493
536
  const handleOmitToggle = useCallback((scene) => {
494
537
  const current = liveText();
495
538
  const idx = buildSceneList(current).findIndex(s => s.index === scene.index);
@@ -498,7 +541,13 @@ export function SceneNavigator() {
498
541
  const omitted = scene.status.toLowerCase() === 'omitted';
499
542
  applyTextEdit(omitted ? restoreSceneInText(current, idx) : omitSceneInText(current, idx), setText);
500
543
  }, [liveText, setText]);
501
- return (_jsxs("div", { className: "cs-navigator", role: "navigation", "aria-label": "Scene Navigator", children: [_jsxs("div", { className: "cs-nav-mobile-bar", children: [_jsx("span", { className: "cs-nav-mobile-title", children: t('navigator.title') }), _jsx("button", { type: "button", className: "cs-nav-mobile-close", "aria-label": "Close navigator", onClick: () => setNavVisible(false), children: _jsx(CloseIcon, { size: 18 }) })] }), _jsxs("div", { className: "cs-nav-list", children: [_jsx("button", { type: "button", className: "cs-nav-section-header cs-nav-frontmatter-btn", onClick: handleFrontmatter, title: t('navigator.frontmatter'), children: _jsx("span", { className: "cs-nav-title", children: t('navigator.frontmatter') }) }), _jsxs("div", { className: "cs-nav-section-header cs-nav-scenes-header", style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx("span", { className: "cs-nav-title", children: t('navigator.title') }), _jsx("span", { className: "cs-nav-count", children: scenes.length })] }), _jsxs("div", { style: { display: 'flex', gap: '4px', alignItems: 'center' }, onClick: e => e.stopPropagation(), children: [_jsx("button", { type: "button", className: "cs-nav-action-btn", onClick: () => setShowNavSynopsis(!showNavSynopsis), title: showNavSynopsis ? 'Hide Synopsis & Duration' : 'Show Synopsis & Duration', "aria-label": showNavSynopsis ? 'Hide Synopsis & Duration' : 'Show Synopsis & Duration', children: showNavSynopsis ? _jsx(EyeIcon, { size: 14 }) : _jsx(EyeOffIcon, { size: 14 }) }), ai && (_jsx("button", { type: "button", className: "cs-nav-action-btn", onClick: handleGenerateAllSceneMetadata, disabled: isProcessing, title: "Generate synopsis & estimate duration (all scenes)", "aria-label": "Generate synopsis & estimate duration (all scenes)", style: { opacity: isProcessing ? 0.5 : 1 }, children: _jsx(SparklesIcon, { size: 14 }) })), canEdit && (_jsx("button", { type: "button", className: "cs-nav-action-btn", onClick: handleRenumber, title: t('navigator.renumber'), "aria-label": t('navigator.renumber'), children: _jsx(RenumberIcon, { size: 14 }) }))] })] }), isProcessing && progressText && (_jsxs("div", { className: "cs-nav-progress", style: {
544
+ const renderSection = (section) => (_jsx("div", { role: "button", tabIndex: 0, className: "cs-nav-item cs-nav-section-anchor", "aria-label": t('navigator.jumpSection').replace('{heading}', section.heading), onClick: () => handleSectionJump(section), onKeyDown: (e) => {
545
+ if (e.key === 'Enter' || e.key === ' ') {
546
+ e.preventDefault();
547
+ handleSectionJump(section);
548
+ }
549
+ }, children: _jsx("div", { className: "cs-nav-scene-info", children: _jsx("div", { className: "cs-nav-heading", children: section.heading || '—' }) }) }, `section:${section.index}`));
550
+ return (_jsxs("div", { className: "cs-navigator", role: "navigation", "aria-label": t('navigator.aria'), children: [_jsxs("div", { className: "cs-nav-mobile-bar", children: [_jsx("span", { className: "cs-nav-mobile-title", children: t('navigator.title') }), _jsx("button", { type: "button", className: "cs-nav-mobile-close", "aria-label": t('navigator.close'), onClick: () => setNavVisible(false), children: _jsx(CloseIcon, { size: 18 }) })] }), _jsxs("div", { className: "cs-nav-list", children: [_jsx("button", { type: "button", className: "cs-nav-section-header cs-nav-frontmatter-btn", onClick: handleFrontmatter, title: t('navigator.frontmatter'), children: _jsx("span", { className: "cs-nav-title", children: t('navigator.frontmatter') }) }), _jsxs("div", { className: "cs-nav-section-header cs-nav-scenes-header", style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx("span", { className: "cs-nav-title", children: t('navigator.title') }), _jsx("span", { className: "cs-nav-count", children: scenes.length })] }), _jsxs("div", { style: { display: 'flex', gap: '4px', alignItems: 'center' }, onClick: e => e.stopPropagation(), children: [_jsx("button", { type: "button", className: "cs-nav-action-btn", onClick: () => setShowNavSynopsis(!showNavSynopsis), title: showNavSynopsis ? t('navigator.hideSynopsis') : t('navigator.showSynopsis'), "aria-label": showNavSynopsis ? t('navigator.hideSynopsis') : t('navigator.showSynopsis'), children: showNavSynopsis ? _jsx(EyeIcon, { size: 14 }) : _jsx(EyeOffIcon, { size: 14 }) }), ai && (_jsx("button", { type: "button", className: "cs-nav-action-btn", onClick: handleGenerateAllSceneMetadata, disabled: isProcessing, title: t('navigator.genAll'), "aria-label": t('navigator.genAll'), style: { opacity: isProcessing ? 0.5 : 1 }, children: _jsx(SparklesIcon, { size: 14 }) })), canEdit && (_jsx("button", { type: "button", className: "cs-nav-action-btn", onClick: handleRenumber, title: t('navigator.renumber'), "aria-label": t('navigator.renumber'), children: _jsx(RenumberIcon, { size: 14 }) }))] })] }), isProcessing && progressText && (_jsxs("div", { className: "cs-nav-progress", style: {
502
551
  display: 'flex',
503
552
  alignItems: 'center',
504
553
  gap: '8px',
@@ -509,58 +558,65 @@ export function SceneNavigator() {
509
558
  borderBottom: '1px solid var(--cs-ui-border-light, #e2e0d6)',
510
559
  fontWeight: 500,
511
560
  }, children: [_jsx("span", { className: "cs-spinner", style: { width: '10px', height: '10px', borderWidth: '1.5px' } }), _jsx("span", { children: progressText })] })), scenes.map((scene, idx) => {
561
+ // Sections that start before this scene (and after the previous one) render as anchors above it.
562
+ const prevOffset = idx === 0 ? -1 : scenes[idx - 1].textOffset;
563
+ const leading = sections.filter(sec => sec.textOffset > prevOffset && sec.textOffset < scene.textOffset);
512
564
  const issue = numberIssues[idx] ?? null;
513
565
  const omitted = scene.status.toLowerCase() === 'omitted';
514
566
  // The id is the scene number, so a missing id is one note, not two.
515
567
  const note = issue ? issueLabel(issue) : scene.id ? null : t('navigator.missingId');
516
568
  const notes = note ? [note] : [];
517
- return (_jsxs("div", { role: "button", tabIndex: 0, "aria-label": `Jump to ${scene.heading}${notes.length ? ` (${notes.join('; ')})` : ''}`, onKeyDown: (e) => {
518
- if (e.key === 'Enter' || e.key === ' ') {
519
- e.preventDefault();
520
- handleJump(scene);
521
- }
522
- }, className: [
523
- 'cs-nav-item',
524
- // No {#id}: comments, scene-level diff, and page locking can't anchor to it.
525
- scene.id ? '' : 'cs-nav-scene-missing-id',
526
- issue ? 'cs-nav-scene-number-issue' : '',
527
- omitted ? 'cs-nav-scene-omitted' : '',
528
- dragIndex === idx ? 'cs-nav-dragging' : '',
529
- dropIndex === idx ? 'cs-nav-drop-target' : '',
530
- ]
531
- .filter(Boolean)
532
- .join(' '), draggable: canEdit, onClick: () => handleJump(scene), onDragStart: () => handleDragStart(idx), onDragOver: (e) => handleDragOver(e, idx), onDrop: (e) => handleDrop(e, idx), onDragEnd: () => {
533
- setDragIndex(null);
534
- setDropIndex(null);
535
- }, children: [_jsxs("div", { className: "cs-nav-scene-info", children: [_jsx("div", { className: "cs-nav-heading", children: scene.heading }), showNavSynopsis && (_jsxs("div", { className: "cs-nav-synopsis-row", style: { marginTop: '4px' }, children: [_jsx("div", { style: { flex: 1, minWidth: 0 }, children: !canEdit ? (scene.synopsis ? _jsx("div", { className: "cs-nav-scene-synopsis", children: scene.synopsis }) : null) : editingSceneIdx === idx ? (_jsx("input", { type: "text", className: "cs-nav-synopsis-input", value: editVal, onChange: e => setEditVal(e.target.value), onBlur: () => commitSynopsis(idx, scene), onKeyDown: e => {
536
- if (e.key === 'Enter')
537
- commitSynopsis(idx, scene);
538
- if (e.key === 'Escape')
539
- setEditingSceneIdx(null);
540
- }, autoFocus: true, onClick: e => e.stopPropagation() })) : scene.synopsis ? (_jsx("div", { className: "cs-nav-scene-synopsis", onClick: e => {
541
- e.stopPropagation();
542
- setEditingSceneIdx(idx);
543
- setEditVal(scene.synopsis);
544
- }, title: "Click to edit synopsis", children: scene.synopsis })) : (_jsx("div", { className: "cs-nav-scene-synopsis cs-nav-synopsis-placeholder", onClick: e => {
545
- e.stopPropagation();
546
- setEditingSceneIdx(idx);
547
- setEditVal('');
548
- }, role: "button", tabIndex: 0, "aria-label": "Add synopsis", onKeyDown: e => {
549
- if (e.key === 'Enter' || e.key === ' ') {
550
- e.stopPropagation();
551
- e.preventDefault();
552
- setEditingSceneIdx(idx);
553
- setEditVal('');
554
- }
555
- }, children: "+ Synopsis" })) }), ai && _jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); void handleSceneMetadata(scene); }, disabled: isProcessing, title: "Generate synopsis & estimate duration", "aria-label": "Generate synopsis & estimate duration", className: "cs-nav-action-btn cs-nav-synopsis-ai-btn", style: { color: 'var(--cs-ui-accent, #c4760a)', ...(isProcessing ? { opacity: 0.5 } : {}) }, children: _jsx(SparklesIcon, { size: 12 }) })] })), (scene.status || (showNavSynopsis && scene.estDuration)) && (_jsxs("div", { style: { display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center', marginTop: '6px' }, children: [scene.status && (_jsx("span", { className: `cs-nav-status cs-status-${scene.status
556
- .toLowerCase()
557
- .replace(/\s+/g, '-')}`, children: scene.status })), showNavSynopsis && scene.estDuration && (_jsxs("span", { className: "cs-nav-status", title: "Estimated Onscreen Duration", style: {
558
- background: 'rgba(217, 119, 6, 0.1)',
559
- color: 'var(--cs-ui-accent, #c4760a)',
560
- border: '1px solid rgba(217, 119, 6, 0.25)',
561
- fontWeight: 600,
562
- }, children: ["\u23F1 ", scene.estDuration] }))] }))] }), _jsxs("div", { className: "cs-nav-scene-side", children: [scene.id ? (_jsx("div", { className: `cs-nav-scene-num${issue ? ' cs-nav-num-issue' : ''}`, title: issue ? issueLabel(issue) : undefined, children: scene.id })) : (_jsx("div", { className: "cs-nav-missing-id-icon", title: issue ? issueLabel(issue) : t('navigator.missingId'), "aria-hidden": "true", children: "\u26A0" })), canEdit && (_jsx("button", { type: "button", className: "cs-nav-action-btn cs-nav-omit-btn", title: omitted ? t('navigator.restoreScene') : t('navigator.omitScene'), "aria-label": `${omitted ? t('navigator.restoreScene') : t('navigator.omitScene')}: ${scene.heading}`, onClick: e => { e.stopPropagation(); handleOmitToggle(scene); }, children: omitted ? _jsx(RestoreIcon, { size: 12 }) : _jsx(OmitIcon, { size: 12 }) }))] })] }, `${idx}:${scene.id ?? ''}`));
563
- }), scenes.length === 0 && (_jsx("div", { className: "cs-nav-empty", children: t('navigator.empty') })), _jsx("div", { style: { marginTop: '20px', padding: '0 8px 10px', borderTop: '1px solid #eee', paddingTop: '15px' }, children: _jsxs("button", { type: "button", className: "cs-nav-section-header cs-nav-frontmatter-btn", onClick: () => setStatsOpen(true), style: { width: '100%', textAlign: 'left', display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }, children: [_jsx(StatsIcon, { size: 14 }), " ", _jsx("span", { className: "cs-nav-title", children: "Statistics" })] }) })] }), statsOpen && _jsx(StatisticsDialog, { onClose: () => setStatsOpen(false) })] }));
569
+ return (_jsxs(React.Fragment, { children: [leading.map(renderSection), _jsxs("div", {
570
+ // Ids can repeat until the writer renumbers (§7.4), so the index keeps keys unique.
571
+ role: "button", tabIndex: 0, "aria-label": t('navigator.jumpScene').replace('{heading}', scene.heading) + (notes.length ? ` (${notes.join('; ')})` : ''), onKeyDown: (e) => {
572
+ if (e.key === 'Enter' || e.key === ' ') {
573
+ e.preventDefault();
574
+ handleJump(scene);
575
+ }
576
+ }, className: [
577
+ 'cs-nav-item',
578
+ // No {#id}: comments, scene-level diff, and page locking can't anchor to it.
579
+ scene.id ? '' : 'cs-nav-scene-missing-id',
580
+ issue ? 'cs-nav-scene-number-issue' : '',
581
+ omitted ? 'cs-nav-scene-omitted' : '',
582
+ dragIndex === idx ? 'cs-nav-dragging' : '',
583
+ dropIndex === idx ? 'cs-nav-drop-target' : '',
584
+ ]
585
+ .filter(Boolean)
586
+ .join(' '), draggable: canEdit, onClick: () => handleJump(scene), onDragStart: () => handleDragStart(idx), onDragOver: (e) => handleDragOver(e, idx), onDrop: (e) => handleDrop(e, idx), onDragEnd: () => {
587
+ setDragIndex(null);
588
+ setDropIndex(null);
589
+ }, children: [_jsxs("div", { className: "cs-nav-scene-info", children: [_jsx("div", { className: "cs-nav-heading", children: scene.heading }), showNavSynopsis && (_jsxs("div", { className: "cs-nav-synopsis-row", style: { marginTop: '4px' }, children: [_jsx("div", { style: { flex: 1, minWidth: 0 }, children: !canEdit ? (scene.synopsis ? _jsx("div", { className: "cs-nav-scene-synopsis", children: scene.synopsis }) : null) : editingSceneIdx === idx ? (_jsx("input", { type: "text", className: "cs-nav-synopsis-input", value: editVal, onChange: e => setEditVal(e.target.value), onBlur: () => commitSynopsis(idx, scene), onKeyDown: e => {
590
+ if (e.key === 'Enter')
591
+ commitSynopsis(idx, scene);
592
+ if (e.key === 'Escape')
593
+ setEditingSceneIdx(null);
594
+ }, autoFocus: true, onClick: e => e.stopPropagation() })) : scene.synopsis ? (_jsx("div", { className: "cs-nav-scene-synopsis", onClick: e => {
595
+ e.stopPropagation();
596
+ setEditingSceneIdx(idx);
597
+ setEditVal(scene.synopsis);
598
+ }, title: t('navigator.editSynopsis'), children: scene.synopsis })) : (_jsx("div", { className: "cs-nav-scene-synopsis cs-nav-synopsis-placeholder", onClick: e => {
599
+ e.stopPropagation();
600
+ setEditingSceneIdx(idx);
601
+ setEditVal('');
602
+ }, role: "button", tabIndex: 0, "aria-label": t('navigator.addSynopsis'), onKeyDown: e => {
603
+ if (e.key === 'Enter' || e.key === ' ') {
604
+ e.stopPropagation();
605
+ e.preventDefault();
606
+ setEditingSceneIdx(idx);
607
+ setEditVal('');
608
+ }
609
+ }, children: "+ Synopsis" })) }), ai && _jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); void handleSceneMetadata(scene); }, disabled: isProcessing, title: t('navigator.genOne'), "aria-label": t('navigator.genOne'), className: "cs-nav-action-btn cs-nav-synopsis-ai-btn", style: { color: 'var(--cs-ui-accent, #c4760a)', ...(isProcessing ? { opacity: 0.5 } : {}) }, children: _jsx(SparklesIcon, { size: 12 }) })] })), (scene.status || (showNavSynopsis && scene.estDuration)) && (_jsxs("div", { style: { display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center', marginTop: '6px' }, children: [scene.status && (_jsx("span", { className: `cs-nav-status cs-status-${scene.status
610
+ .toLowerCase()
611
+ .replace(/\s+/g, '-')}`, children: scene.status })), showNavSynopsis && scene.estDuration && (_jsxs("span", { className: "cs-nav-status", title: t('navigator.duration'), style: {
612
+ background: 'rgba(217, 119, 6, 0.1)',
613
+ color: 'var(--cs-ui-accent, #c4760a)',
614
+ border: '1px solid rgba(217, 119, 6, 0.25)',
615
+ fontWeight: 600,
616
+ }, children: ["\u23F1 ", scene.estDuration] }))] }))] }), _jsxs("div", { className: "cs-nav-scene-side", children: [scene.id ? (_jsx("div", { className: `cs-nav-scene-num${issue ? ' cs-nav-num-issue' : ''}`, title: issue ? issueLabel(issue) : undefined, children: scene.id })) : (_jsx("div", { className: "cs-nav-missing-id-icon", title: issue ? issueLabel(issue) : t('navigator.missingId'), "aria-hidden": "true", children: "\u26A0" })), canEdit && (_jsx("button", { type: "button", className: "cs-nav-action-btn cs-nav-omit-btn", title: omitted ? t('navigator.restoreScene') : t('navigator.omitScene'), "aria-label": `${omitted ? t('navigator.restoreScene') : t('navigator.omitScene')}: ${scene.heading}`, onClick: e => { e.stopPropagation(); handleOmitToggle(scene); }, children: omitted ? _jsx(RestoreIcon, { size: 12 }) : _jsx(OmitIcon, { size: 12 }) }))] })] })] }, `${idx}:${scene.id ?? ''}`));
617
+ }), sections
618
+ .filter(sec => scenes.length === 0 || sec.textOffset > scenes[scenes.length - 1].textOffset)
619
+ .map(renderSection), scenes.length === 0 && sections.length === 0 && (_jsx("div", { className: "cs-nav-empty", children: t('navigator.empty') })), _jsx("div", { style: { marginTop: '20px', padding: '0 8px 10px', borderTop: '1px solid #eee', paddingTop: '15px' }, children: _jsxs("button", { type: "button", className: "cs-nav-section-header cs-nav-frontmatter-btn", onClick: () => setStatsOpen(true), style: { width: '100%', textAlign: 'left', display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }, children: [_jsx(StatsIcon, { size: 14 }), " ", _jsx("span", { className: "cs-nav-title", children: t('navigator.stats') })] }) })] }), statsOpen && _jsx(StatisticsDialog, { onClose: () => setStatsOpen(false) })] }));
564
620
  }
565
621
  /**
566
622
  * Reorder scene blocks in the source text.
@@ -4,8 +4,10 @@ import { extractWorkflowData } from '../file/workflow-reports';
4
4
  import { getEditorView } from '../editor/editor-bus';
5
5
  import { schema } from '../editor/schema';
6
6
  import { useCanEdit } from '../extensions/session';
7
+ import { useTranslation } from '../i18n/useTranslation';
7
8
  export function StatisticsDialog({ onClose }) {
8
9
  const { ast, text, setText } = useDocument();
10
+ const { t } = useTranslation();
9
11
  const canEdit = useCanEdit();
10
12
  const { scenes, characters } = extractWorkflowData(ast);
11
13
  const handleUpdateActor = (charName, actorName) => {
@@ -82,13 +84,13 @@ export function StatisticsDialog({ onClose }) {
82
84
  return (_jsx("div", { className: "cs-dialog-overlay", role: "presentation", onClick: e => {
83
85
  if (e.target === e.currentTarget)
84
86
  onClose();
85
- }, children: _jsxs("div", { className: "cs-dialog", role: "dialog", "aria-modal": "true", "aria-labelledby": "cs-stats-dialog-title", style: { maxWidth: '640px', width: '92%', maxHeight: '85vh', display: 'flex', flexDirection: 'column' }, children: [_jsxs("div", { className: "cs-panel-header", children: [_jsx("span", { id: "cs-stats-dialog-title", className: "cs-panel-title", children: "Script Statistics" }), _jsx("button", { className: "cs-panel-close", onClick: onClose, "aria-label": "Close", children: "\u00D7" })] }), _jsxs("div", { className: "cs-panel-body", style: { overflowY: 'auto', flex: 1, padding: '20px' }, children: [_jsxs("div", { className: "cs-stats-summary-grid", children: [_jsxs("div", { className: "cs-stats-card", children: [_jsx("div", { className: "cs-stats-card-val", children: scenes.length }), _jsx("div", { className: "cs-stats-card-lbl", children: "Scenes" })] }), _jsxs("div", { className: "cs-stats-card", children: [_jsx("div", { className: "cs-stats-card-val", children: wordCount.toLocaleString() }), _jsx("div", { className: "cs-stats-card-lbl", children: "Words" })] }), _jsxs("div", { className: "cs-stats-card", children: [_jsx("div", { className: "cs-stats-card-val", children: totalDurationLabel }), _jsx("div", { className: "cs-stats-card-lbl", children: "Est. Duration" })] })] }), _jsxs("div", { className: "cs-stats-section-hdr", children: ["Cast List & Occurrences (", characters.length, ")"] }), characters.length === 0 ? (_jsx("div", { style: { color: 'var(--cs-ui-text-light-muted, #8c897f)', fontStyle: 'italic', fontSize: '12px' }, children: "No character cues detected in the screenplay yet." })) : (_jsxs("table", { className: "cs-stats-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: { textAlign: 'left', width: '35%' }, children: "Character" }), _jsx("th", { style: { textAlign: 'left' }, children: "Actor" }), _jsx("th", { style: { textAlign: 'center', width: '15%' }, children: "Scenes" })] }) }), _jsx("tbody", { children: characters.map((c, i) => (_jsxs("tr", { children: [_jsx("td", { style: { fontWeight: '600' }, children: c.name }), _jsx("td", { children: _jsx("input", { type: "text", className: "cs-stats-actor-input", value: c.actor, onChange: (e) => handleUpdateActor(c.name, e.target.value), readOnly: !canEdit, placeholder: "Assign actor...", "aria-label": `Actor for ${c.name}` }) }), _jsx("td", { style: { textAlign: 'center', fontWeight: '500' }, children: c.sceneCount })] }, i))) })] }))] }), _jsx("div", { style: {
87
+ }, children: _jsxs("div", { className: "cs-dialog", role: "dialog", "aria-modal": "true", "aria-labelledby": "cs-stats-dialog-title", style: { maxWidth: '640px', width: '92%', maxHeight: '85vh', display: 'flex', flexDirection: 'column' }, children: [_jsxs("div", { className: "cs-panel-header", children: [_jsx("span", { id: "cs-stats-dialog-title", className: "cs-panel-title", children: t('stats.title') }), _jsx("button", { className: "cs-panel-close", onClick: onClose, "aria-label": t('common.close'), children: "\u00D7" })] }), _jsxs("div", { className: "cs-panel-body", style: { overflowY: 'auto', flex: 1, padding: '20px' }, children: [_jsxs("div", { className: "cs-stats-summary-grid", children: [_jsxs("div", { className: "cs-stats-card", children: [_jsx("div", { className: "cs-stats-card-val", children: scenes.length }), _jsx("div", { className: "cs-stats-card-lbl", children: t('stats.scenes') })] }), _jsxs("div", { className: "cs-stats-card", children: [_jsx("div", { className: "cs-stats-card-val", children: wordCount.toLocaleString() }), _jsx("div", { className: "cs-stats-card-lbl", children: t('stats.words') })] }), _jsxs("div", { className: "cs-stats-card", children: [_jsx("div", { className: "cs-stats-card-val", children: totalDurationLabel }), _jsx("div", { className: "cs-stats-card-lbl", children: t('stats.duration') })] })] }), _jsxs("div", { className: "cs-stats-section-hdr", children: [t('stats.cast'), " (", characters.length, ")"] }), characters.length === 0 ? (_jsx("div", { style: { color: 'var(--cs-ui-text-light-muted, #8c897f)', fontStyle: 'italic', fontSize: '12px' }, children: t('stats.noCharacters') })) : (_jsxs("table", { className: "cs-stats-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: { textAlign: 'left', width: '35%' }, children: t('stats.character') }), _jsx("th", { style: { textAlign: 'left' }, children: t('stats.actor') }), _jsx("th", { style: { textAlign: 'center', width: '15%' }, children: t('stats.scenes') })] }) }), _jsx("tbody", { children: characters.map((c, i) => (_jsxs("tr", { children: [_jsx("td", { style: { fontWeight: '600' }, children: c.name }), _jsx("td", { children: _jsx("input", { type: "text", className: "cs-stats-actor-input", value: c.actor, onChange: (e) => handleUpdateActor(c.name, e.target.value), readOnly: !canEdit, placeholder: t('stats.assignActor'), "aria-label": t('stats.actorFor').replace('{name}', c.name) }) }), _jsx("td", { style: { textAlign: 'center', fontWeight: '500' }, children: c.sceneCount })] }, i))) })] }))] }), _jsx("div", { style: {
86
88
  borderTop: '1px solid var(--cs-ui-border-light-subtle, #eeece4)',
87
89
  display: 'flex',
88
90
  justifyContent: 'flex-end',
89
91
  padding: '12px 20px',
90
92
  background: 'var(--cs-ui-bg-panel-subtle, #f4f2ea)',
91
- }, children: _jsx("button", { type: "button", className: "cs-confirm-btn-primary", onClick: onClose, children: "Close" }) })] }) }));
93
+ }, children: _jsx("button", { type: "button", className: "cs-confirm-btn-primary", onClick: onClose, children: t('common.close') }) })] }) }));
92
94
  }
93
95
  function updateActorInText(text, charName, actorName) {
94
96
  const lines = text.split('\n');
@@ -14,3 +14,10 @@ export interface SceneEntry {
14
14
  * Scenes are identified by scene heading lines (## prefix).
15
15
  */
16
16
  export declare function buildSceneList(text: string): SceneEntry[];
17
+ export interface SectionEntry {
18
+ heading: string;
19
+ index: number;
20
+ textOffset: number;
21
+ }
22
+ /** Derive the `# ` section headings (acts, sequences, …) from Sutra source text. */
23
+ export declare function buildSectionList(text: string): SectionEntry[];
@@ -68,3 +68,17 @@ export function buildSceneList(text) {
68
68
  }
69
69
  return scenes;
70
70
  }
71
+ /** Derive the `# ` section headings (acts, sequences, …) from Sutra source text. */
72
+ export function buildSectionList(text) {
73
+ const sections = [];
74
+ let offset = 0;
75
+ for (const line of text.split('\n')) {
76
+ const trimmed = line.trim();
77
+ if (trimmed === '#' || trimmed.startsWith('# ') || trimmed.startsWith('#\t')) {
78
+ const { body } = splitAttributeBlock(trimmed.slice(1).trim());
79
+ sections.push({ heading: body.trim(), index: sections.length + 1, textOffset: offset });
80
+ }
81
+ offset += line.length + 1;
82
+ }
83
+ return sections;
84
+ }
@@ -48,8 +48,8 @@ export function AppBar() {
48
48
  }
49
49
  }
50
50
  catch (err) {
51
- showToast(err instanceof Error ? err.message : 'Failed to import the Word document.', 'error');
51
+ showToast(err instanceof Error ? err.message : t('appbar.importFailed'), 'error');
52
52
  }
53
53
  };
54
- 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: name, 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: [localFiles && (_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 }) }), localFiles && (_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: [appbarPanels.map(p => (_jsx("span", { className: "cs-ab-panel", "aria-label": p.title, children: p.render(panelContext) }, p.id))), 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') })] }), localFiles && _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { handleNew(); setMobileMenuOpen(false); }, children: [_jsx(NewIcon, { size: 18 }), _jsx("span", { children: t('appbar.new') })] }), localFiles && _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') })] }), localFiles && _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) })] }));
54
+ return (_jsxs("header", { className: "cs-app-bar", role: "banner", "aria-label": t('appbar.header'), children: [_jsxs("div", { className: "cs-ab-group cs-ab-left", children: [_jsxs("div", { className: "cs-ab-brand", title: t('appbar.brandTitle'), 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: name, children: [_jsx("span", { className: "cs-ab-file-name", children: name }), isDirty && _jsx("span", { className: "cs-ab-dirty", title: t('appbar.unsaved') })] })] }), _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: t('appbar.voice'), shortcut: "Ctrl+Shift+V", active: voiceActive, onClick: () => setVoiceActive(!voiceActive), children: _jsx(MicIcon, { size: 18 }) })), _jsx(AbBtn, { label: metadataVisible ? t('appbar.hideMetadata') : t('appbar.showMetadata'), 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: [localFiles && (_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: t('appbar.importAI'), 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 }) }), localFiles && (_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: [appbarPanels.map(p => (_jsx("span", { className: "cs-ab-panel", "aria-label": p.title, children: p.render(panelContext) }, p.id))), canEdit && (_jsx(AbBtn, { label: t('appbar.save'), variant: isDirty ? 'primary' : undefined, onClick: () => { void saveFile(); }, children: _jsx(SaveIcon, { size: 17 }) })), _jsx(AbBtn, { label: t('appbar.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": t('appbar.appMenu'), 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": t('appbar.closeMenu'), 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: t('appbar.unsaved') })] }), _jsxs("div", { className: "cs-mm-section", children: [_jsx("div", { className: "cs-mm-section-title", children: t('appbar.viewsTools') }), _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: t('appbar.voice') })] }), _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 ? t('appbar.hideMetadata') : t('appbar.showMetadata') })] })] }), canEdit && _jsxs("div", { className: "cs-mm-section", children: [_jsx("div", { className: "cs-mm-section-title", children: t('appbar.fileOps') }), _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { openTitlePageForm(); setMobileMenuOpen(false); }, children: [_jsx(TitlePageIcon, { size: 18 }), _jsx("span", { children: t('appbar.titlePage') })] }), localFiles && _jsxs("button", { type: "button", className: "cs-mm-item", onClick: () => { handleNew(); setMobileMenuOpen(false); }, children: [_jsx(NewIcon, { size: 18 }), _jsx("span", { children: t('appbar.new') })] }), localFiles && _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: t('appbar.importAI') })] }), _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') })] }), localFiles && _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: t('appbar.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) })] }));
55
55
  }
@@ -40,5 +40,5 @@ export function AppShell() {
40
40
  window.addEventListener(OPEN_TITLE_PAGE_EVENT, open);
41
41
  return () => window.removeEventListener(OPEN_TITLE_PAGE_EVENT, open);
42
42
  }, []);
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, {})] }));
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": t('shell.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": t('shell.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, {})] }));
44
44
  }
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useTranslation } from '../i18n/useTranslation';
2
3
  import { useEffect, useRef } from 'react';
3
4
  export function ConfirmDialog({ options, onClose }) {
5
+ const { t } = useTranslation();
4
6
  const confirmBtnRef = useRef(null);
5
7
  useEffect(() => {
6
8
  confirmBtnRef.current?.focus();
@@ -16,8 +18,8 @@ export function ConfirmDialog({ options, onClose }) {
16
18
  return (_jsx("div", { className: "cs-confirm-overlay", role: "presentation", onClick: () => { options.onCancel?.(); onClose(); }, children: _jsxs("div", { className: "cs-confirm-dialog", role: "alertdialog", "aria-modal": "true", "aria-labelledby": "cs-confirm-title", "aria-describedby": "cs-confirm-desc", onClick: (e) => e.stopPropagation(), children: [_jsx("div", { id: "cs-confirm-title", className: "cs-confirm-title", children: options.title }), _jsx("div", { id: "cs-confirm-desc", className: "cs-confirm-msg", children: options.message }), _jsxs("div", { className: "cs-confirm-actions", children: [_jsx("button", { type: "button", className: "cs-confirm-btn-cancel", onClick: () => {
17
19
  options.onCancel?.();
18
20
  onClose();
19
- }, children: options.cancelLabel ?? 'Cancel' }), _jsx("button", { ref: confirmBtnRef, type: "button", className: options.destructive ? 'cs-confirm-btn-danger' : 'cs-confirm-btn-primary', onClick: () => {
21
+ }, children: options.cancelLabel ?? t('common.cancel') }), _jsx("button", { ref: confirmBtnRef, type: "button", className: options.destructive ? 'cs-confirm-btn-danger' : 'cs-confirm-btn-primary', onClick: () => {
20
22
  options.onConfirm();
21
23
  onClose();
22
- }, children: options.confirmLabel ?? 'Confirm' })] })] }) }));
24
+ }, children: options.confirmLabel ?? t('common.confirm') })] })] }) }));
23
25
  }
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React, { useEffect, useState } from 'react';
3
+ import { useTranslation } from '../i18n/useTranslation';
3
4
  import { ToolbarButton } from './ToolbarButton';
4
5
  import { subscribe } from '../editor/editor-bus';
5
6
  import { OverflowIcon, CloseIcon } from './icons';
@@ -21,6 +22,7 @@ function groupsOf(commands) {
21
22
  * separator between groups.
22
23
  */
23
24
  export function ElementToolbar() {
25
+ const { t } = useTranslation();
24
26
  const [, setTick] = useState(0);
25
27
  const [overflowOpen, setOverflowOpen] = useState(false);
26
28
  const commands = useCommands().filter(c => c.menu === 'toolbar');
@@ -33,8 +35,8 @@ export function ElementToolbar() {
33
35
  const elements = commands.filter(c => c.group === 'elements');
34
36
  const others = commands.filter(c => !['marks', 'history', 'elements'].includes(c.group ?? ''));
35
37
  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 => {
38
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "cs-element-toolbar", role: "toolbar", "aria-label": t('tb.editing'), 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": t('tb.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": t('tb.sheetLabel'), children: [_jsxs("div", { className: "cs-tb-overflow-header", children: [_jsx("span", { className: "cs-tb-overflow-title", children: t('tb.sheetTitle') }), _jsx("button", { type: "button", className: "cs-tb-overflow-close", "aria-label": t('common.close'), onClick: () => setOverflowOpen(false), children: _jsx(CloseIcon, { size: 18 }) })] }), _jsx("div", { className: "cs-tb-overflow-section-label", children: t('tb.inline') }), _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: t('tb.elements') }), _jsx("div", { className: "cs-tb-overflow-list", children: [...elements, ...others].map(c => {
37
39
  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));
40
+ 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: t('tb.active') })] }, c.id));
39
41
  }) })] })] }))] }));
40
42
  }
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useRef } from 'react';
3
3
  import { useLanguage } from '../i18n/LanguageContext';
4
+ import { useTranslation } from '../i18n/useTranslation';
4
5
  import { useDocument } from '../context/DocumentContext';
5
6
  import { getEditorView } from '../editor/editor-bus';
6
7
  import { useCanEdit } from '../extensions/session';
@@ -18,6 +19,7 @@ const LANG_NAMES = {
18
19
  si: 'Sinhala',
19
20
  };
20
21
  export function LanguageStatus() {
22
+ const { t } = useTranslation();
21
23
  const { currentLanguage } = useLanguage();
22
24
  const { text, setText } = useDocument();
23
25
  const canEdit = useCanEdit();
@@ -61,7 +63,7 @@ export function LanguageStatus() {
61
63
  }
62
64
  setMenuOpen(false);
63
65
  };
64
- return (_jsxs("div", { className: "cs-lang-status", ref: menuRef, children: [_jsxs("button", { type: "button", className: "cs-lang-status-btn cs-lang-pill", title: `Script language at caret: ${langName} (${currentLanguage.toUpperCase()})`, "aria-label": `Script language: ${langName}`, "aria-haspopup": "menu", "aria-expanded": menuOpen, onClick: () => setMenuOpen(!menuOpen), children: [_jsx("span", { className: "cs-lang-pill-script", children: "\u0905/A" }), _jsx("span", { className: "cs-lang-code", children: currentLanguage.toUpperCase() })] }), menuOpen && (_jsx("div", { className: "cs-lang-menu", role: "menu", "aria-label": "Script languages", children: Object.entries(LANG_NAMES).map(([code, name]) => {
66
+ return (_jsxs("div", { className: "cs-lang-status", ref: menuRef, children: [_jsxs("button", { type: "button", className: "cs-lang-status-btn cs-lang-pill", title: t('lang.tooltip').replace('{name}', langName).replace('{code}', currentLanguage.toUpperCase()), "aria-label": t('lang.aria').replace('{name}', langName), "aria-haspopup": "menu", "aria-expanded": menuOpen, onClick: () => setMenuOpen(!menuOpen), children: [_jsx("span", { className: "cs-lang-pill-script", children: "\u0905/A" }), _jsx("span", { className: "cs-lang-code", children: currentLanguage.toUpperCase() })] }), menuOpen && (_jsx("div", { className: "cs-lang-menu", role: "menu", "aria-label": t('lang.menu'), children: Object.entries(LANG_NAMES).map(([code, name]) => {
65
67
  const isActive = currentLanguage === code;
66
68
  return (_jsxs("button", { type: "button", role: "menuitem", className: `cs-lang-option${isActive ? ' cs-lang-active' : ''}`, onClick: (e) => {
67
69
  e.stopPropagation();
@@ -55,5 +55,5 @@ export function StatusBar() {
55
55
  const estPages = Math.max(1, Math.round(lines / 54));
56
56
  return { wordCount: words, pageEstimate: estPages };
57
57
  }, [text]);
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) }))] }));
58
+ return (_jsxs(_Fragment, { children: [_jsxs("footer", { className: "cs-status-bar", role: "contentinfo", "aria-label": t('statusbar.label'), 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) }))] }));
59
59
  }