@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.
- package/dist/ai/VoiceConfirmDialog.d.ts +1 -0
- package/dist/ai/VoiceConfirmDialog.js +6 -6
- package/dist/ai/VoiceToolbar.d.ts +1 -0
- package/dist/ai/VoiceToolbar.js +22 -16
- package/dist/ai/clean-response.d.ts +7 -0
- package/dist/ai/clean-response.js +58 -0
- package/dist/ai/format-transcript.d.ts +3 -0
- package/dist/ai/format-transcript.js +7 -0
- package/dist/ai/prompts.d.ts +8 -1
- package/dist/ai/prompts.js +34 -7
- package/dist/autocomplete/suggestions.js +1 -1
- package/dist/context/DocumentContext.d.ts +28 -4
- package/dist/context/DocumentContext.js +67 -37
- package/dist/editor/EditorView.js +32 -4
- package/dist/editor/SelectionMenu.js +5 -3
- package/dist/editor/external-decorations-plugin.d.ts +23 -0
- package/dist/editor/external-decorations-plugin.js +151 -0
- package/dist/editor/omitted-scene-plugin.d.ts +9 -0
- package/dist/editor/omitted-scene-plugin.js +44 -0
- package/dist/editor/plugins.js +4 -0
- package/dist/editor/private-metadata-plugin.d.ts +10 -0
- package/dist/editor/private-metadata-plugin.js +60 -0
- package/dist/editor/prosemirror-to-sutra.js +179 -74
- package/dist/editor/scene-heading-view.js +2 -0
- package/dist/editor/schema.js +69 -12
- package/dist/editor/sutra-to-prosemirror.d.ts +2 -0
- package/dist/editor/sutra-to-prosemirror.js +32 -34
- package/dist/extensions/ai-provider.d.ts +44 -0
- package/dist/extensions/ai-provider.js +27 -0
- package/dist/extensions/collab.d.ts +21 -0
- package/dist/extensions/collab.js +6 -0
- package/dist/extensions/command-registry.d.ts +49 -0
- package/dist/extensions/command-registry.js +28 -0
- package/dist/extensions/decorations.d.ts +48 -0
- package/dist/extensions/decorations.js +6 -0
- package/dist/extensions/export-registry.d.ts +59 -0
- package/dist/extensions/export-registry.js +10 -0
- package/dist/extensions/panel-registry.d.ts +30 -0
- package/dist/extensions/panel-registry.js +16 -0
- package/dist/extensions/registries.d.ts +15 -0
- package/dist/extensions/registries.js +12 -0
- package/dist/extensions/registry-store.d.ts +26 -0
- package/dist/extensions/registry-store.js +35 -0
- package/dist/extensions/session.d.ts +29 -0
- package/dist/extensions/session.js +20 -0
- package/dist/extensions/speech-provider.d.ts +35 -0
- package/dist/extensions/speech-provider.js +17 -0
- package/dist/file/builtin-exporters.d.ts +9 -0
- package/dist/file/builtin-exporters.js +72 -0
- package/dist/file/docx-exporter.js +7 -4
- package/dist/file/docx-importer.d.ts +9 -0
- package/dist/file/docx-importer.js +47 -19
- package/dist/file/scene-number.d.ts +5 -0
- package/dist/file/scene-number.js +8 -0
- package/dist/file/workflow-reports.js +13 -7
- package/dist/findreplace/FindReplace.js +5 -2
- package/dist/import/AIImportDialog.d.ts +11 -0
- package/dist/import/AIImportDialog.js +112 -0
- package/dist/import/ai-import.d.ts +51 -0
- package/dist/import/ai-import.js +200 -0
- package/dist/import/apply-import.d.ts +7 -0
- package/dist/import/apply-import.js +35 -0
- package/dist/import/extract.d.ts +17 -0
- package/dist/import/extract.js +18 -0
- package/dist/index.d.ts +17 -2
- package/dist/index.js +7 -0
- package/dist/locales/bn.json +8 -0
- package/dist/locales/en.json +8 -0
- package/dist/locales/gu.json +8 -0
- package/dist/locales/hi.json +8 -0
- package/dist/locales/kn.json +8 -0
- package/dist/locales/ml.json +8 -0
- package/dist/locales/mr.json +8 -0
- package/dist/locales/or.json +8 -0
- package/dist/locales/pa.json +8 -0
- package/dist/locales/si.json +8 -0
- package/dist/locales/ta.json +8 -0
- package/dist/locales/te.json +8 -0
- package/dist/navigator/SceneNavigator.js +125 -131
- package/dist/navigator/StatisticsDialog.js +5 -1
- package/dist/navigator/scene-edits.d.ts +18 -0
- package/dist/navigator/scene-edits.js +143 -0
- package/dist/navigator/scene-list.js +4 -4
- package/dist/navigator/scene-numbering.d.ts +37 -0
- package/dist/navigator/scene-numbering.js +88 -0
- package/dist/settings/SettingsDialog.js +8 -120
- package/dist/shell/AppBar.js +21 -2
- package/dist/shell/AppShell.js +18 -4
- package/dist/shell/ElementToolbar.d.ts +5 -0
- package/dist/shell/ElementToolbar.js +35 -25
- package/dist/shell/LanguageStatus.js +6 -0
- package/dist/shell/StatusBar.js +11 -6
- package/dist/shell/builtin-commands.d.ts +10 -0
- package/dist/shell/builtin-commands.js +43 -0
- package/dist/shell/icons.d.ts +6 -0
- package/dist/shell/icons.js +6 -0
- package/dist/shell/use-commands.d.ts +7 -0
- package/dist/shell/use-commands.js +24 -0
- package/dist/shell/use-panel-context.d.ts +3 -0
- package/dist/shell/use-panel-context.js +39 -0
- package/dist/source/SourceView.js +18 -1
- package/dist/styles/screenplay.css +113 -1
- package/dist/titlepage/ExportDialog.d.ts +5 -0
- package/dist/titlepage/ExportDialog.js +59 -96
- package/package.json +4 -4
- package/dist/ai/OnboardingDialog.d.ts +0 -6
- package/dist/ai/OnboardingDialog.js +0 -115
- package/dist/ai/ai-client.d.ts +0 -71
- package/dist/ai/ai-client.js +0 -494
- package/dist/ai/generated-providers.json +0 -3696
- package/dist/ai/voice-service.d.ts +0 -15
- package/dist/ai/voice-service.js +0 -81
- package/dist/editor/character-view.d.ts +0 -35
- package/dist/editor/character-view.js +0 -112
- package/dist/editor/transliterate-input.d.ts +0 -10
- package/dist/editor/transliterate-input.js +0 -88
- package/dist/extensions/types.d.ts +0 -73
- package/dist/extensions/types.js +0 -12
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { isFeatureEnabled, useSession } from './session';
|
|
3
|
+
const AIProviderContext = createContext(null);
|
|
4
|
+
export const AIProviderProvider = AIProviderContext.Provider;
|
|
5
|
+
/**
|
|
6
|
+
* The AI provider, or null when AI is unavailable in this session: no
|
|
7
|
+
* provider was injected, the session cannot edit, or `featureFlags.ai` is off.
|
|
8
|
+
* Components hide their AI UI when this is null.
|
|
9
|
+
*/
|
|
10
|
+
export function useAI() {
|
|
11
|
+
const provider = useContext(AIProviderContext);
|
|
12
|
+
const session = useSession();
|
|
13
|
+
if (!provider || session.permission !== 'edit' || !isFeatureEnabled(session, 'ai'))
|
|
14
|
+
return null;
|
|
15
|
+
return provider;
|
|
16
|
+
}
|
|
17
|
+
/** `Calling <label>...` text for the status bar; '' when idle. */
|
|
18
|
+
export function formatActivity(calls) {
|
|
19
|
+
if (calls.length === 0)
|
|
20
|
+
return '';
|
|
21
|
+
if (calls.length === 1)
|
|
22
|
+
return `Calling ${calls[0].label}...`;
|
|
23
|
+
const unique = Array.from(new Set(calls.map(c => c.label)));
|
|
24
|
+
if (unique.length === 1)
|
|
25
|
+
return `Calling ${unique[0]} (${calls.length} calls)...`;
|
|
26
|
+
return `Calling ${unique.join(', ')}...`;
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Schema } from 'prosemirror-model';
|
|
2
|
+
import type { Plugin } from 'prosemirror-state';
|
|
3
|
+
import type { EditorView } from 'prosemirror-view';
|
|
4
|
+
/**
|
|
5
|
+
* Binds the formatted editor to a real-time sync provider (e.g. a Yjs
|
|
6
|
+
* document via y-prosemirror) (OSS spec §11.5).
|
|
7
|
+
*
|
|
8
|
+
* `plugins` are added whenever the editor creates its state; `attach` is
|
|
9
|
+
* called once the view exists and returns a detach function, called when
|
|
10
|
+
* the view is destroyed.
|
|
11
|
+
*
|
|
12
|
+
* The editor serializes the shared ProseMirror document back to Sutra text
|
|
13
|
+
* with the AST-lossless serializer (packages/editor/tests/lossless-round-trip.test.ts),
|
|
14
|
+
* so collaborators see the same document the file contains.
|
|
15
|
+
*/
|
|
16
|
+
export interface CollabBinding {
|
|
17
|
+
plugins(schema: Schema): Plugin[];
|
|
18
|
+
attach(view: EditorView): () => void;
|
|
19
|
+
}
|
|
20
|
+
export declare const CollabBindingProvider: import("react").Provider<CollabBinding | null>;
|
|
21
|
+
export declare function useCollabBinding(): CollabBinding | null;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { EditorView } from 'prosemirror-view';
|
|
3
|
+
import type { EditorView as SourceEditorView } from '@codemirror/view';
|
|
4
|
+
import type { EditorMode } from '../types';
|
|
5
|
+
import type { RegistryStore } from './registry-store';
|
|
6
|
+
export interface CommandContext {
|
|
7
|
+
/** The formatted editor, when it is mounted. */
|
|
8
|
+
view: EditorView | null;
|
|
9
|
+
/** The source editor, when it is mounted. */
|
|
10
|
+
sourceView: SourceEditorView | null;
|
|
11
|
+
mode: EditorMode;
|
|
12
|
+
text: string;
|
|
13
|
+
setText(next: string): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A command contributed by the embedder or built into the editor (OSS spec §11.5).
|
|
17
|
+
*
|
|
18
|
+
* - `shortcut`: a key binding such as `Mod-Shift-K` or `Alt-Shift-H` (`Mod` is
|
|
19
|
+
* Ctrl, or Cmd on macOS; `-` or `+` separate parts). Handled globally, in
|
|
20
|
+
* both editing modes.
|
|
21
|
+
* - `shortcutHint`: shown in tooltips only — for keys something else already
|
|
22
|
+
* binds (e.g. ProseMirror's Mod-B).
|
|
23
|
+
* - `menu`: `toolbar` places it in the element toolbar (in `group`),
|
|
24
|
+
* `appbar` in the app bar.
|
|
25
|
+
* - `readOnly: true` keeps it available in read/comment sessions; otherwise
|
|
26
|
+
* it is hidden and its shortcut inactive there.
|
|
27
|
+
*/
|
|
28
|
+
export interface CommandContribution {
|
|
29
|
+
id: string;
|
|
30
|
+
label: string;
|
|
31
|
+
run(ctx: CommandContext): void;
|
|
32
|
+
shortcut?: string;
|
|
33
|
+
shortcutHint?: string;
|
|
34
|
+
menu?: 'toolbar' | 'appbar';
|
|
35
|
+
group?: string;
|
|
36
|
+
/** Icon at the requested pixel size. */
|
|
37
|
+
icon?: (size: number) => ReactNode;
|
|
38
|
+
/** Short syntax hint shown under toolbar labels (e.g. the `@` sigil). */
|
|
39
|
+
sigil?: string;
|
|
40
|
+
/** Highlights the toolbar button (e.g. the block type at the caret). */
|
|
41
|
+
isActive?(ctx: CommandContext): boolean;
|
|
42
|
+
readOnly?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export type CommandRegistry = RegistryStore<CommandContribution>;
|
|
45
|
+
export declare function createCommandRegistry(initial?: CommandContribution[]): CommandRegistry;
|
|
46
|
+
export declare const CommandRegistryProvider: import("react").Provider<CommandRegistry>;
|
|
47
|
+
export declare function useCommandRegistry(): CommandRegistry;
|
|
48
|
+
/** True when `e` matches a binding like `Mod-Shift-K` / `Alt+Shift+H`. */
|
|
49
|
+
export declare function matchesShortcut(shortcut: string, e: KeyboardEvent): boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { createRegistryStore } from './registry-store';
|
|
3
|
+
export function createCommandRegistry(initial = []) {
|
|
4
|
+
return createRegistryStore(initial);
|
|
5
|
+
}
|
|
6
|
+
const CommandRegistryContext = createContext(createCommandRegistry());
|
|
7
|
+
export const CommandRegistryProvider = CommandRegistryContext.Provider;
|
|
8
|
+
export function useCommandRegistry() {
|
|
9
|
+
return useContext(CommandRegistryContext);
|
|
10
|
+
}
|
|
11
|
+
const IS_MAC = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform);
|
|
12
|
+
/** True when `e` matches a binding like `Mod-Shift-K` / `Alt+Shift+H`. */
|
|
13
|
+
export function matchesShortcut(shortcut, e) {
|
|
14
|
+
const parts = shortcut.split(/[-+](?=.)/);
|
|
15
|
+
const key = parts.pop();
|
|
16
|
+
const mods = new Set(parts.map(p => p.toLowerCase()));
|
|
17
|
+
const wantCtrl = mods.has('ctrl') || (mods.has('mod') && !IS_MAC);
|
|
18
|
+
const wantMeta = mods.has('cmd') || mods.has('meta') || (mods.has('mod') && IS_MAC);
|
|
19
|
+
if (e.ctrlKey !== wantCtrl || e.metaKey !== wantMeta)
|
|
20
|
+
return false;
|
|
21
|
+
if (e.altKey !== mods.has('alt') || e.shiftKey !== mods.has('shift'))
|
|
22
|
+
return false;
|
|
23
|
+
if (/^[a-z]$/i.test(key))
|
|
24
|
+
return e.code === `Key${key.toUpperCase()}`;
|
|
25
|
+
if (/^[0-9]$/.test(key))
|
|
26
|
+
return e.code === `Digit${key}`;
|
|
27
|
+
return e.key.toLowerCase() === key.toLowerCase();
|
|
28
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { DocumentNode } from '@sutrata/parser';
|
|
2
|
+
/**
|
|
3
|
+
* Where a decoration sits. Anchors are scene-relative so they survive edits
|
|
4
|
+
* elsewhere in the document: `sceneId` is the scene's `{#id}` (scenes
|
|
5
|
+
* without one cannot be anchored — the navigator flags them), and
|
|
6
|
+
* `from`/`to` are character offsets into that scene's text as returned by
|
|
7
|
+
* `ctx.sceneText(sceneId)`.
|
|
8
|
+
*
|
|
9
|
+
* Scene text is the scene's blocks (heading through the last block before
|
|
10
|
+
* the next heading) joined with '\n', with line breaks as '\n'.
|
|
11
|
+
*/
|
|
12
|
+
export interface Anchor {
|
|
13
|
+
sceneId: string;
|
|
14
|
+
from?: number;
|
|
15
|
+
to?: number;
|
|
16
|
+
}
|
|
17
|
+
export type DecorationSpec = {
|
|
18
|
+
/** `inline`: a text range (default: the whole scene). `node`: the block containing `from` (default: the heading). */
|
|
19
|
+
kind: 'inline' | 'node';
|
|
20
|
+
anchor: Anchor;
|
|
21
|
+
className?: string;
|
|
22
|
+
attrs?: Record<string, string>;
|
|
23
|
+
/** Called on click, including in read-only and comment sessions. */
|
|
24
|
+
onClick?(e: MouseEvent): void;
|
|
25
|
+
} | {
|
|
26
|
+
/** A DOM element inserted at `anchor.from` (default: the start of the scene). */
|
|
27
|
+
kind: 'widget';
|
|
28
|
+
anchor: Anchor;
|
|
29
|
+
side?: -1 | 1;
|
|
30
|
+
render(): HTMLElement;
|
|
31
|
+
};
|
|
32
|
+
export interface DecorationContext {
|
|
33
|
+
ast: DocumentNode;
|
|
34
|
+
/** The anchorable text of a scene, or null if no scene has that id. */
|
|
35
|
+
sceneText(sceneId: string): string | null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* External annotations — comment threads, breakdown highlights, revision
|
|
39
|
+
* marks — rendered in the formatted editor (OSS spec §11.5).
|
|
40
|
+
* `subscribe` tells the editor to call `getDecorations` again.
|
|
41
|
+
*/
|
|
42
|
+
export interface DecorationProvider {
|
|
43
|
+
id: string;
|
|
44
|
+
subscribe(onChange: () => void): () => void;
|
|
45
|
+
getDecorations(ctx: DecorationContext): DecorationSpec[];
|
|
46
|
+
}
|
|
47
|
+
export declare const DecorationProvidersProvider: import("react").Provider<readonly DecorationProvider[]>;
|
|
48
|
+
export declare function useDecorationProviders(): readonly DecorationProvider[];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
const DecorationProvidersContext = createContext([]);
|
|
3
|
+
export const DecorationProvidersProvider = DecorationProvidersContext.Provider;
|
|
4
|
+
export function useDecorationProviders() {
|
|
5
|
+
return useContext(DecorationProvidersContext);
|
|
6
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { DocumentNode, RomanizeVariant } from '@sutrata/parser';
|
|
2
|
+
import type { ScreenplayStyleDefinition } from '../styles/types';
|
|
3
|
+
import type { RegistryStore } from './registry-store';
|
|
4
|
+
export interface ExportOptions {
|
|
5
|
+
/** Style chosen in the dialog (screenplay exports), or undefined for the document's own. */
|
|
6
|
+
styleId?: string;
|
|
7
|
+
skipNotes: boolean;
|
|
8
|
+
/** Set when `text`/`ast` are a romanized copy (Indic → ISO 15919). */
|
|
9
|
+
romanized: {
|
|
10
|
+
scope: 'all' | 'dialogue';
|
|
11
|
+
variant: RomanizeVariant;
|
|
12
|
+
} | null;
|
|
13
|
+
}
|
|
14
|
+
export interface ExportContext {
|
|
15
|
+
/** The document — or its romanized copy, see options.romanized. */
|
|
16
|
+
text: string;
|
|
17
|
+
ast: DocumentNode;
|
|
18
|
+
/** The document's resolved style. */
|
|
19
|
+
style: ScreenplayStyleDefinition;
|
|
20
|
+
customStyles: ScreenplayStyleDefinition[];
|
|
21
|
+
options: ExportOptions;
|
|
22
|
+
/** File name without extension, from the open document. */
|
|
23
|
+
baseName: string;
|
|
24
|
+
}
|
|
25
|
+
/** What `run` produced: a file to download (optionally with warnings), or nothing if it handled output itself. */
|
|
26
|
+
export type ExportOutput = Blob | {
|
|
27
|
+
blob?: Blob;
|
|
28
|
+
warnings?: string[];
|
|
29
|
+
} | void;
|
|
30
|
+
/**
|
|
31
|
+
* An export format or report contributed by the embedder or built into the
|
|
32
|
+
* editor (OSS spec §11.5). The Export dialog renders every registered
|
|
33
|
+
* exporter: `screenplay` ones as cards, `report` ones grouped by `family`
|
|
34
|
+
* with `formatLabel` chips.
|
|
35
|
+
*/
|
|
36
|
+
export interface ExportContribution {
|
|
37
|
+
id: string;
|
|
38
|
+
label: string;
|
|
39
|
+
group: 'screenplay' | 'report';
|
|
40
|
+
description?: string;
|
|
41
|
+
/** Reports: the report this format belongs to (e.g. "Shot List"). */
|
|
42
|
+
family?: string;
|
|
43
|
+
/** Reports: the chip label (e.g. "Word (.docx)"). */
|
|
44
|
+
formatLabel?: string;
|
|
45
|
+
/** Which dialog options apply. */
|
|
46
|
+
supports?: {
|
|
47
|
+
style?: boolean;
|
|
48
|
+
skipNotes?: boolean;
|
|
49
|
+
romanize?: boolean;
|
|
50
|
+
};
|
|
51
|
+
run(ctx: ExportContext): Promise<ExportOutput>;
|
|
52
|
+
fileName(ctx: ExportContext): string;
|
|
53
|
+
/** Toast shown after a successful export. */
|
|
54
|
+
successMessage?: string;
|
|
55
|
+
}
|
|
56
|
+
export type ExportRegistry = RegistryStore<ExportContribution>;
|
|
57
|
+
export declare function createExportRegistry(initial?: ExportContribution[]): ExportRegistry;
|
|
58
|
+
export declare const ExportRegistryProvider: import("react").Provider<ExportRegistry>;
|
|
59
|
+
export declare function useExportRegistry(): ExportRegistry;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { createRegistryStore } from './registry-store';
|
|
3
|
+
export function createExportRegistry(initial = []) {
|
|
4
|
+
return createRegistryStore(initial);
|
|
5
|
+
}
|
|
6
|
+
const ExportRegistryContext = createContext(createExportRegistry());
|
|
7
|
+
export const ExportRegistryProvider = ExportRegistryContext.Provider;
|
|
8
|
+
export function useExportRegistry() {
|
|
9
|
+
return useContext(ExportRegistryContext);
|
|
10
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { DocumentNode } from '@sutrata/parser';
|
|
3
|
+
import type { RegistryStore } from './registry-store';
|
|
4
|
+
/**
|
|
5
|
+
* - `sidebar`: tabs beside the scene navigator (the built-in first tab).
|
|
6
|
+
* - `inspector`: sections in a panel on the right of the editor.
|
|
7
|
+
* - `appbar`: small items in the app bar, before Settings.
|
|
8
|
+
* - `settings`: sections in the Settings dialog.
|
|
9
|
+
*/
|
|
10
|
+
export type PanelLocation = 'sidebar' | 'inspector' | 'appbar' | 'settings';
|
|
11
|
+
export interface PanelRenderContext {
|
|
12
|
+
ast: DocumentNode;
|
|
13
|
+
/** Ids of scenes that have one, in document order. */
|
|
14
|
+
sceneIds: string[];
|
|
15
|
+
/** Id of the scene containing the caret, if it has one. */
|
|
16
|
+
activeSceneId: string | null;
|
|
17
|
+
}
|
|
18
|
+
/** A panel contributed by the embedder (OSS spec §11.5). */
|
|
19
|
+
export interface PanelContribution {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
location: PanelLocation;
|
|
23
|
+
render(ctx: PanelRenderContext): ReactNode;
|
|
24
|
+
}
|
|
25
|
+
export type PanelRegistry = RegistryStore<PanelContribution>;
|
|
26
|
+
export declare function createPanelRegistry(initial?: PanelContribution[]): PanelRegistry;
|
|
27
|
+
export declare const PanelRegistryProvider: import("react").Provider<PanelRegistry>;
|
|
28
|
+
export declare function usePanelRegistry(): PanelRegistry;
|
|
29
|
+
/** Registered panels for `location`, re-rendering when registrations change. */
|
|
30
|
+
export declare function usePanels(location: PanelLocation): PanelContribution[];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo } from 'react';
|
|
2
|
+
import { createRegistryStore, useRegistryList } from './registry-store';
|
|
3
|
+
export function createPanelRegistry(initial = []) {
|
|
4
|
+
return createRegistryStore(initial);
|
|
5
|
+
}
|
|
6
|
+
const EMPTY_REGISTRY = createPanelRegistry();
|
|
7
|
+
const PanelRegistryContext = createContext(EMPTY_REGISTRY);
|
|
8
|
+
export const PanelRegistryProvider = PanelRegistryContext.Provider;
|
|
9
|
+
export function usePanelRegistry() {
|
|
10
|
+
return useContext(PanelRegistryContext);
|
|
11
|
+
}
|
|
12
|
+
/** Registered panels for `location`, re-rendering when registrations change. */
|
|
13
|
+
export function usePanels(location) {
|
|
14
|
+
const all = useRegistryList(usePanelRegistry());
|
|
15
|
+
return useMemo(() => all.filter(p => p.location === location), [all, location]);
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PanelRegistry } from './panel-registry';
|
|
2
|
+
import type { CommandRegistry } from './command-registry';
|
|
3
|
+
import type { ExportRegistry } from './export-registry';
|
|
4
|
+
export interface Registries {
|
|
5
|
+
panels: PanelRegistry;
|
|
6
|
+
commands: CommandRegistry;
|
|
7
|
+
exporters: ExportRegistry;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Live registries for DocumentProvider's `panels` / `commands` / `exporters`
|
|
11
|
+
* props. Register up front or at runtime; each `register()` returns an
|
|
12
|
+
* unregister function. The editor's built-ins are not in these registries —
|
|
13
|
+
* registering an entry with a built-in's id replaces it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createRegistries(): Registries;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createPanelRegistry } from './panel-registry';
|
|
2
|
+
import { createCommandRegistry } from './command-registry';
|
|
3
|
+
import { createExportRegistry } from './export-registry';
|
|
4
|
+
/**
|
|
5
|
+
* Live registries for DocumentProvider's `panels` / `commands` / `exporters`
|
|
6
|
+
* props. Register up front or at runtime; each `register()` returns an
|
|
7
|
+
* unregister function. The editor's built-ins are not in these registries —
|
|
8
|
+
* registering an entry with a built-in's id replaces it.
|
|
9
|
+
*/
|
|
10
|
+
export function createRegistries() {
|
|
11
|
+
return { panels: createPanelRegistry(), commands: createCommandRegistry(), exporters: createExportRegistry() };
|
|
12
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live list behind every registry (panels, commands, exporters).
|
|
3
|
+
* `register` replaces an entry with the same id and returns an unregister
|
|
4
|
+
* function; `list` returns the same array until something changes, so it can
|
|
5
|
+
* back useSyncExternalStore.
|
|
6
|
+
*/
|
|
7
|
+
export interface RegistryStore<T extends {
|
|
8
|
+
id: string;
|
|
9
|
+
}> {
|
|
10
|
+
register(entry: T): () => void;
|
|
11
|
+
list(): readonly T[];
|
|
12
|
+
subscribe(listener: () => void): () => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function createRegistryStore<T extends {
|
|
15
|
+
id: string;
|
|
16
|
+
}>(initial?: readonly T[]): RegistryStore<T>;
|
|
17
|
+
export declare function useRegistryList<T extends {
|
|
18
|
+
id: string;
|
|
19
|
+
}>(store: RegistryStore<T>): readonly T[];
|
|
20
|
+
/**
|
|
21
|
+
* Built-in entries followed by registered ones; a registered entry with a
|
|
22
|
+
* built-in's id replaces it in place.
|
|
23
|
+
*/
|
|
24
|
+
export declare function withBuiltins<T extends {
|
|
25
|
+
id: string;
|
|
26
|
+
}>(builtins: readonly T[], registered: readonly T[]): T[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
export function createRegistryStore(initial = []) {
|
|
3
|
+
let entries = [...initial];
|
|
4
|
+
const listeners = new Set();
|
|
5
|
+
const set = (next) => {
|
|
6
|
+
entries = next;
|
|
7
|
+
for (const l of listeners)
|
|
8
|
+
l();
|
|
9
|
+
};
|
|
10
|
+
return {
|
|
11
|
+
register(entry) {
|
|
12
|
+
set([...entries.filter(e => e.id !== entry.id), entry]);
|
|
13
|
+
return () => set(entries.filter(e => e !== entry));
|
|
14
|
+
},
|
|
15
|
+
list: () => entries,
|
|
16
|
+
subscribe(listener) {
|
|
17
|
+
listeners.add(listener);
|
|
18
|
+
return () => { listeners.delete(listener); };
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function useRegistryList(store) {
|
|
23
|
+
return useSyncExternalStore(store.subscribe, store.list, store.list);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Built-in entries followed by registered ones; a registered entry with a
|
|
27
|
+
* built-in's id replaces it in place.
|
|
28
|
+
*/
|
|
29
|
+
export function withBuiltins(builtins, registered) {
|
|
30
|
+
const byId = new Map(registered.map(e => [e.id, e]));
|
|
31
|
+
return [
|
|
32
|
+
...builtins.map(b => byId.get(b.id) ?? b),
|
|
33
|
+
...registered.filter(e => !builtins.some(b => b.id === e.id)),
|
|
34
|
+
];
|
|
35
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who is using the editor and what they may do (OSS spec §11.5). Optional:
|
|
3
|
+
* without one, the editor behaves as a single local user with full edit
|
|
4
|
+
* rights and no feature flags.
|
|
5
|
+
*
|
|
6
|
+
* - `edit`: everything.
|
|
7
|
+
* - `read`: the document cannot be changed. The formatted and source editors
|
|
8
|
+
* are read-only; save/open/import, AI, voice, the element toolbar, and
|
|
9
|
+
* structural navigator actions (reorder, synopsis edits, scene-number
|
|
10
|
+
* locking) are unavailable.
|
|
11
|
+
* - `comment`: like `read`; decoration providers (comment threads) may still
|
|
12
|
+
* handle clicks.
|
|
13
|
+
*
|
|
14
|
+
* `featureFlags`: `ai: false` hides AI features, `voice: false` hides voice
|
|
15
|
+
* dictation. Unset flags count as enabled.
|
|
16
|
+
*/
|
|
17
|
+
export interface SessionContext {
|
|
18
|
+
userId: string | null;
|
|
19
|
+
displayName: string | null;
|
|
20
|
+
permission: 'read' | 'comment' | 'edit';
|
|
21
|
+
featureFlags: Record<string, boolean>;
|
|
22
|
+
}
|
|
23
|
+
export declare const DEFAULT_SESSION: SessionContext;
|
|
24
|
+
export declare const SessionProvider: import("react").Provider<SessionContext>;
|
|
25
|
+
export declare function useSession(): SessionContext;
|
|
26
|
+
/** True when the current session may change the document. */
|
|
27
|
+
export declare function useCanEdit(): boolean;
|
|
28
|
+
/** A feature is on unless the session's featureFlags turn it off explicitly. */
|
|
29
|
+
export declare function isFeatureEnabled(session: SessionContext, flag: string): boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const DEFAULT_SESSION = {
|
|
3
|
+
userId: null,
|
|
4
|
+
displayName: null,
|
|
5
|
+
permission: 'edit',
|
|
6
|
+
featureFlags: {},
|
|
7
|
+
};
|
|
8
|
+
const SessionReactContext = createContext(DEFAULT_SESSION);
|
|
9
|
+
export const SessionProvider = SessionReactContext.Provider;
|
|
10
|
+
export function useSession() {
|
|
11
|
+
return useContext(SessionReactContext);
|
|
12
|
+
}
|
|
13
|
+
/** True when the current session may change the document. */
|
|
14
|
+
export function useCanEdit() {
|
|
15
|
+
return useSession().permission === 'edit';
|
|
16
|
+
}
|
|
17
|
+
/** A feature is on unless the session's featureFlags turn it off explicitly. */
|
|
18
|
+
export function isFeatureEnabled(session, flag) {
|
|
19
|
+
return session.featureFlags[flag] !== false;
|
|
20
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface SpeechRecognitionCallbacks {
|
|
2
|
+
onResult(text: string, isFinal: boolean): void;
|
|
3
|
+
onError(error: string): void;
|
|
4
|
+
onEnd(): void;
|
|
5
|
+
}
|
|
6
|
+
/** A running live-recognition session. */
|
|
7
|
+
export interface SpeechRecognitionSession {
|
|
8
|
+
/** Stop listening; pending results are still delivered. */
|
|
9
|
+
stop(): void;
|
|
10
|
+
/** Stop listening and drop pending results. */
|
|
11
|
+
abort(): void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Speech-to-text, injected by the embedder (OSS spec §11.5). Voice dictation
|
|
15
|
+
* uses live recognition (`listen`) and formats the transcript with the
|
|
16
|
+
* AIProvider, so the voice UI appears only when both are available.
|
|
17
|
+
*/
|
|
18
|
+
export interface SpeechProvider {
|
|
19
|
+
id: string;
|
|
20
|
+
displayName: string;
|
|
21
|
+
/** Whether recognition can run here (e.g. the browser has the Web Speech API). */
|
|
22
|
+
isSupported(): boolean;
|
|
23
|
+
/** Start live recognition for a BCP-47 language (e.g. `hi-IN`). */
|
|
24
|
+
listen(language: string, callbacks: SpeechRecognitionCallbacks): SpeechRecognitionSession;
|
|
25
|
+
/** Optional batch transcription of recorded audio. */
|
|
26
|
+
transcribe?(audio: Blob, opts?: {
|
|
27
|
+
language?: string;
|
|
28
|
+
}): Promise<string>;
|
|
29
|
+
}
|
|
30
|
+
export declare const SpeechProviderProvider: import("react").Provider<SpeechProvider | null>;
|
|
31
|
+
/**
|
|
32
|
+
* The speech provider, or null when voice dictation is unavailable: no speech
|
|
33
|
+
* or AI provider, a session that cannot edit, or `featureFlags.voice` off.
|
|
34
|
+
*/
|
|
35
|
+
export declare function useSpeech(): SpeechProvider | null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { isFeatureEnabled, useSession } from './session';
|
|
3
|
+
import { useAI } from './ai-provider';
|
|
4
|
+
const SpeechProviderContext = createContext(null);
|
|
5
|
+
export const SpeechProviderProvider = SpeechProviderContext.Provider;
|
|
6
|
+
/**
|
|
7
|
+
* The speech provider, or null when voice dictation is unavailable: no speech
|
|
8
|
+
* or AI provider, a session that cannot edit, or `featureFlags.voice` off.
|
|
9
|
+
*/
|
|
10
|
+
export function useSpeech() {
|
|
11
|
+
const provider = useContext(SpeechProviderContext);
|
|
12
|
+
const ai = useAI();
|
|
13
|
+
const session = useSession();
|
|
14
|
+
if (!provider || !ai || !isFeatureEnabled(session, 'voice'))
|
|
15
|
+
return null;
|
|
16
|
+
return provider;
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ExportContribution } from '../extensions/export-registry';
|
|
2
|
+
type Translate = (key: string) => string;
|
|
3
|
+
/**
|
|
4
|
+
* The editor's own export formats and reports, registered like an
|
|
5
|
+
* embedder's. PDF and print views open a print preview window instead of
|
|
6
|
+
* returning a file; their run() must open it synchronously (popup blockers).
|
|
7
|
+
*/
|
|
8
|
+
export declare function builtinExporters(t: Translate): ExportContribution[];
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { exportFountain } from '@sutrata/parser';
|
|
2
|
+
import { exportToDocx } from './docx-exporter';
|
|
3
|
+
import { extractWorkflowData, exportOneLinerCsv, exportShotListCsv, exportCastReportCsv, exportOneLinerDocx, exportShotListDocx, exportCastReportDocx, openPrintPreview, openScreenplayPrintPreview, } from './workflow-reports';
|
|
4
|
+
/** Short descriptions of the built-in report families, shown under each report's title. */
|
|
5
|
+
const REPORT_DESCRIPTIONS = {
|
|
6
|
+
'One-Liner Schedule': 'A scene-by-scene production stripboard breakdown with locations, cast, and timings.',
|
|
7
|
+
'Shot List': 'Camera shot specifications extracted from scene headings and production notes.',
|
|
8
|
+
'Cast Breakdown': 'Character occurrences, dialogue counts, and assigned actors list.',
|
|
9
|
+
};
|
|
10
|
+
const suffix = (ctx) => (ctx.options.romanized ? '_romanized' : '');
|
|
11
|
+
const titleSuffix = (ctx) => (ctx.options.romanized ? ' (ROMANIZED)' : '');
|
|
12
|
+
/** CSV with a BOM so Excel reads UTF-8 (Indic text) correctly. */
|
|
13
|
+
function csv(content) {
|
|
14
|
+
return new Blob(['\uFEFF', content], { type: 'text/csv;charset=utf-8' });
|
|
15
|
+
}
|
|
16
|
+
function report(id, family, formatLabel, ext, stem, run, successMessage) {
|
|
17
|
+
return {
|
|
18
|
+
id, label: `${family} — ${formatLabel}`, group: 'report', family, formatLabel,
|
|
19
|
+
description: REPORT_DESCRIPTIONS[family],
|
|
20
|
+
supports: { romanize: true },
|
|
21
|
+
run,
|
|
22
|
+
fileName: ctx => `${ctx.baseName}_${stem}${suffix(ctx)}.${ext ?? 'html'}`,
|
|
23
|
+
successMessage,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The editor's own export formats and reports, registered like an
|
|
28
|
+
* embedder's. PDF and print views open a print preview window instead of
|
|
29
|
+
* returning a file; their run() must open it synchronously (popup blockers).
|
|
30
|
+
*/
|
|
31
|
+
export function builtinExporters(t) {
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
id: 'pdf', label: 'PDF / Print Preview', group: 'screenplay',
|
|
35
|
+
description: 'Print-ready industry screenplay layout styled according to your active visual template.',
|
|
36
|
+
supports: { style: true, skipNotes: true, romanize: true },
|
|
37
|
+
run: async (ctx) => {
|
|
38
|
+
const title = `${ctx.baseName.toUpperCase()} - SCREENPLAY${titleSuffix(ctx)}`;
|
|
39
|
+
await openScreenplayPrintPreview(ctx.ast, title, ctx.options.styleId, ctx.customStyles, ctx.options.skipNotes);
|
|
40
|
+
},
|
|
41
|
+
fileName: ctx => `${ctx.baseName}${suffix(ctx)}.pdf`,
|
|
42
|
+
successMessage: 'Opened Screenplay Print Preview',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'docx', label: 'Microsoft Word (.docx)', group: 'screenplay',
|
|
46
|
+
description: t('export.docxDesc'),
|
|
47
|
+
supports: { style: true, skipNotes: true, romanize: true },
|
|
48
|
+
run: ctx => exportToDocx(ctx.ast, ctx.options.styleId, ctx.customStyles, ctx.options.skipNotes),
|
|
49
|
+
fileName: ctx => `${ctx.baseName}${suffix(ctx)}.docx`,
|
|
50
|
+
successMessage: 'Exported Word document (.docx)',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'fountain', label: 'Fountain (.fountain)', group: 'screenplay',
|
|
54
|
+
description: t('export.fountainDesc'),
|
|
55
|
+
run: async (ctx) => {
|
|
56
|
+
const result = exportFountain(ctx.ast);
|
|
57
|
+
return { blob: new Blob([result.text], { type: 'text/plain;charset=utf-8' }), warnings: result.warnings };
|
|
58
|
+
},
|
|
59
|
+
fileName: ctx => `${ctx.baseName}.fountain`,
|
|
60
|
+
successMessage: 'Exported Fountain screenplay',
|
|
61
|
+
},
|
|
62
|
+
report('oneliner-print', 'One-Liner Schedule', 'Print View', null, 'one_liner', async (ctx) => {
|
|
63
|
+
await openPrintPreview(extractWorkflowData(ctx.ast).scenes, `${ctx.baseName.toUpperCase()} - ONE-LINER SCHEDULE${titleSuffix(ctx)}`);
|
|
64
|
+
}, 'Opened One-Liner Schedule'),
|
|
65
|
+
report('oneliner-docx', 'One-Liner Schedule', 'Word (.docx)', 'docx', 'one_liner', ctx => exportOneLinerDocx(extractWorkflowData(ctx.ast).scenes), 'Exported One-Liner DOCX'),
|
|
66
|
+
report('oneliner-csv', 'One-Liner Schedule', 'Spreadsheet (.csv)', 'csv', 'one_liner', async (ctx) => csv(exportOneLinerCsv(extractWorkflowData(ctx.ast).scenes)), 'Exported One-Liner CSV'),
|
|
67
|
+
report('shotlist-docx', 'Shot List', 'Word (.docx)', 'docx', 'shot_list', ctx => exportShotListDocx(extractWorkflowData(ctx.ast).scenes), 'Exported Shot List DOCX'),
|
|
68
|
+
report('shotlist-csv', 'Shot List', 'Spreadsheet (.csv)', 'csv', 'shot_list', async (ctx) => csv(exportShotListCsv(extractWorkflowData(ctx.ast).scenes)), 'Exported Shot List CSV'),
|
|
69
|
+
report('cast-docx', 'Cast Breakdown', 'Word (.docx)', 'docx', 'cast_report', ctx => exportCastReportDocx(extractWorkflowData(ctx.ast).characters), 'Exported Cast Report DOCX'),
|
|
70
|
+
report('cast-csv', 'Cast Breakdown', 'Spreadsheet (.csv)', 'csv', 'cast_report', async (ctx) => csv(exportCastReportCsv(extractWorkflowData(ctx.ast).characters)), 'Exported Cast Report CSV'),
|
|
71
|
+
];
|
|
72
|
+
}
|
|
@@ -3,6 +3,7 @@ import { resolveStyle } from '../styles/registry';
|
|
|
3
3
|
import { styleToDocxParagraphStyles } from '../styles/docx-adapter';
|
|
4
4
|
import { SCRIPT_UNICODE_RANGES } from '../styles/script-ranges';
|
|
5
5
|
import { DEFAULT_COVER_LAYOUT } from '../styles/types';
|
|
6
|
+
import { sceneNumberOf, isOmitted } from './scene-number';
|
|
6
7
|
/** Flatten inline span trees into flat formatting runs. `skipNotes` drops inline
|
|
7
8
|
* `[[ ]]` note spans entirely (see the "Skip notes" export option). */
|
|
8
9
|
function flattenSpans(spans, props = {}, skipNotes = false) {
|
|
@@ -418,13 +419,15 @@ export async function exportToDocx(ast, styleOverride, customStyles = [], skipNo
|
|
|
418
419
|
}
|
|
419
420
|
}
|
|
420
421
|
else if (node.type === 'scene-heading') {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
422
|
+
// Scene number: the {#id} (§6.1); an omitted scene prints OMITTED (§7.4).
|
|
423
|
+
const sceneNumber = sceneNumberOf(node);
|
|
424
|
+
const headingRuns = runsFor([{ text: isOmitted(node) ? 'OMITTED' : node.text.toUpperCase(), bold: true }]);
|
|
425
|
+
if (sceneNumber) {
|
|
426
|
+
headingRuns.push(new TextRun({ text: '\t' }), ...runsFor([{ text: sceneNumber, bold: true }]));
|
|
424
427
|
}
|
|
425
428
|
childrenNodes.push(new Paragraph({
|
|
426
429
|
style: 'CineSceneHeading',
|
|
427
|
-
tabStops:
|
|
430
|
+
tabStops: sceneNumber
|
|
428
431
|
? [{ type: TabStopType.RIGHT, position: contentRightEdgeTwips }]
|
|
429
432
|
: undefined,
|
|
430
433
|
children: headingRuns,
|
|
@@ -3,6 +3,15 @@ export interface DocxImportResult {
|
|
|
3
3
|
bodyText: string | null;
|
|
4
4
|
warnings: string[];
|
|
5
5
|
}
|
|
6
|
+
/** True when the document carries the CineXxx paragraph styles exportToDocx applies. */
|
|
7
|
+
export declare function isSutrataDocx(buffer: ArrayBuffer): Promise<boolean>;
|
|
8
|
+
/**
|
|
9
|
+
* Plain text of any Word document, one line per paragraph (empty paragraphs
|
|
10
|
+
* become blank lines), with bold/italic/underline runs as Sutra emphasis
|
|
11
|
+
* markup. Table cells are read row by row, left to right. Used by the
|
|
12
|
+
* AI-assisted import for documents Sutrata did not write.
|
|
13
|
+
*/
|
|
14
|
+
export declare function readDocxText(buffer: ArrayBuffer): Promise<string>;
|
|
6
15
|
/** Parses a .docx file (as produced by exportToDocx, possibly re-saved from Word with
|
|
7
16
|
* corrections and/or font changes) back into Sutra body text. The title page, if any,
|
|
8
17
|
* is not reimported — only content carrying the CineXxx paragraph styles the exporter
|