/** * Minimal imperative handle every text-editor surface implements so * an external tool (voice dictation, command palette, AI suggestion) * can read/write its text content without traversing React. * * Methods are optional so a host can register a partial handle * (e.g. only `getValue` + `setValue`), and the caller checks before use. */ interface ComposerHandle { /** Move keyboard focus into the composer's editable surface. */ focus: () => void; /** Move the caret to the very end of the input. */ moveCursorToEnd?: () => void; /** Read the current draft text. Voice dictation anchors partial * transcripts onto the user's already-typed prefix via this. */ getValue?: () => string; /** Replace the current draft text. Voice dictation pushes interim * and final transcripts through this without owning a controlled * binding. */ setValue?: (value: string) => void; } /** * `@djangocfg/ui-tools/composer-registry` * * Cross-tool bridge: the currently-active text composer's handle. * * Producer side (`@djangocfg/ui-tools/chat` and TipTap hosts): * register their composer's imperative handle via `attachComposer`. * * Consumer side (`@djangocfg/ui-tools/speech-recognition`): * reads the active handle via `useActiveComposer`/`getActiveComposer` * and pipes voice transcripts into it. * * Why this lives in its own subpath (not inside `chat`) * ---------------------------------------------------- * `chat` and `speech-recognition` are sibling subpath exports. If the * registry lived inside `chat`, then `speech-recognition` would have * to reach into it via a cross-tool relative import — and under Vite * dev's dependency optimizer that file ends up loaded TWICE (once via * the `./chat` URL, once via the `./speech-recognition` relative-up * URL), giving the producer and the consumer two separate `let active` * slots. The active handle registered by chat would be invisible to * speech-recognition (and vice versa). * * Putting the registry in its own dedicated subpath (a single tool * that NEITHER chat nor speech-recognition cross-import — they both * import this one as their dependency) means Vite resolves it from a * single URL across the whole graph. One module instance, one shared * `active` slot. * * Semantics: one active composer per realm. The most recent * `registerComposer(handle)` wins; `registerComposer(null)` clears it. */ type Listener = (handle: ComposerHandle | null) => void; /** Set or replace the active composer handle. Pass `null` to clear. */ declare function registerComposer(handle: ComposerHandle | null): void; /** * Convenience for components: register on mount, unregister on * unmount. Returns a cleanup function suitable for `useEffect`. */ declare function attachComposer(handle: ComposerHandle): () => void; /** Read the current active handle (no subscription). */ declare function getActiveComposer(): ComposerHandle | null; /** Subscribe to handle changes; returns an unsubscribe fn. */ declare function subscribeComposer(listener: Listener): () => void; /** * React hook: re-renders the caller whenever the active composer * changes. Built on `useSyncExternalStore` so concurrent rendering, * SSR, and dev-mode strict-effects all behave correctly. */ declare function useActiveComposer(): ComposerHandle | null; export { type ComposerHandle, attachComposer, getActiveComposer, registerComposer, subscribeComposer, useActiveComposer };