@djangocfg/ui-tools 2.1.409 → 2.1.411

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 (54) hide show
  1. package/package.json +13 -13
  2. package/src/{tools/Chat/highlight → lib/browser-bridge}/README.md +46 -18
  3. package/src/lib/browser-bridge/commands/chat.ts +42 -0
  4. package/src/lib/browser-bridge/commands/highlight.ts +70 -0
  5. package/src/lib/browser-bridge/commands/index.ts +15 -0
  6. package/src/lib/browser-bridge/commands/inspect.ts +31 -0
  7. package/src/lib/browser-bridge/commands/scroll.ts +31 -0
  8. package/src/lib/browser-bridge/commands/write.ts +45 -0
  9. package/src/lib/browser-bridge/directive-bus.ts +120 -0
  10. package/src/lib/browser-bridge/index.ts +56 -0
  11. package/src/lib/browser-bridge/logger.ts +27 -0
  12. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/HighlightOverlay.tsx +14 -0
  13. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/__tests__/HighlightOverlay.test.tsx +52 -0
  14. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/__tests__/resolveRef.test.ts +39 -0
  15. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/index.ts +8 -5
  16. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/resolveRef.ts +5 -0
  17. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/useHighlightTargets.ts +58 -27
  18. package/src/lib/browser-bridge/overlay/waitForVisible.ts +70 -0
  19. package/src/lib/browser-bridge/registry.ts +41 -0
  20. package/src/lib/browser-bridge/setBridgeResolver.ts +42 -0
  21. package/src/lib/browser-bridge/window.ts +76 -0
  22. package/src/lib/page-snapshot/capture/walk.ts +13 -5
  23. package/src/lib/page-snapshot/engine.ts +9 -4
  24. package/src/lib/page-snapshot/index.ts +5 -0
  25. package/src/lib/page-snapshot/react/provider.tsx +70 -3
  26. package/src/lib/page-snapshot/react/use-page-snapshot.ts +10 -0
  27. package/src/lib/page-snapshot/refs/__tests__/locator.test.ts +94 -0
  28. package/src/lib/page-snapshot/refs/__tests__/registry.test.ts +59 -3
  29. package/src/lib/page-snapshot/refs/locator.ts +218 -0
  30. package/src/lib/page-snapshot/refs/registry.ts +29 -14
  31. package/src/tools/Chat/README.md +1 -1
  32. package/src/tools/Chat/constants.ts +24 -1
  33. package/src/tools/Chat/context/ChatProvider.tsx +17 -2
  34. package/src/tools/Chat/core/logger.ts +15 -2
  35. package/src/tools/Chat/index.ts +34 -2
  36. package/src/tools/Chat/launcher/ChatDock.tsx +13 -3
  37. package/src/tools/Chat/launcher/ChatFAB.tsx +4 -2
  38. package/src/tools/Chat/launcher/ChatGreeting.tsx +3 -2
  39. package/src/tools/Chat/launcher/ChatLauncher.tsx +42 -7
  40. package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +3 -2
  41. package/src/tools/Chat/launcher/header/ChatHeader.tsx +2 -0
  42. package/src/tools/Chat/launcher/header/ChatHeaderActionButton.tsx +2 -0
  43. package/src/tools/Chat/launcher/header/ChatHeaderLanguageButton.tsx +2 -2
  44. package/src/tools/Chat/launcher/header/HeaderSlots.tsx +16 -9
  45. package/src/tools/Chat/lazy.tsx +34 -2
  46. package/src/tools/Chat/public.ts +16 -0
  47. package/src/tools/Chat/settings/README.md +87 -0
  48. package/src/tools/Chat/settings/__tests__/useChatSettings.test.tsx +84 -0
  49. package/src/tools/Chat/settings/__tests__/useLocalStorage.test.tsx +138 -0
  50. package/src/tools/Chat/settings/index.ts +23 -0
  51. package/src/tools/Chat/settings/types.ts +108 -0
  52. package/src/tools/Chat/settings/useChatSettings.ts +168 -0
  53. /package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/SpotlightCanvas.tsx +0 -0
  54. /package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/types.ts +0 -0
@@ -0,0 +1,84 @@
1
+ // @vitest-environment jsdom
2
+ import { act, createElement } from 'react';
3
+ import { createRoot, type Root } from 'react-dom/client';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+
6
+ import { CHAT_SETTINGS_STORAGE_KEY } from '../types';
7
+ import { useChatSettings, type UseChatSettingsReturn } from '../useChatSettings';
8
+
9
+ (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
10
+
11
+ let container: HTMLDivElement;
12
+ let root: Root;
13
+ let api: UseChatSettingsReturn | null = null;
14
+
15
+ function Probe() {
16
+ api = useChatSettings();
17
+ return null;
18
+ }
19
+
20
+ beforeEach(() => {
21
+ window.localStorage.clear();
22
+ container = document.createElement('div');
23
+ document.body.appendChild(container);
24
+ root = createRoot(container);
25
+ });
26
+
27
+ afterEach(() => {
28
+ act(() => root.unmount());
29
+ container.remove();
30
+ window.localStorage.clear();
31
+ api = null;
32
+ });
33
+
34
+ async function flush() {
35
+ await act(async () => {
36
+ await Promise.resolve();
37
+ });
38
+ }
39
+
40
+ describe('useChatSettings', () => {
41
+ it('exposes the default settings before anything is stored', async () => {
42
+ await act(async () => root.render(createElement(Probe)));
43
+ expect(api!.settings.pageContext.linked).toBe(false);
44
+ expect(api!.settings.audio.muted).toBe(false);
45
+ expect(api!.settings.dock.side).toBe('right');
46
+ });
47
+
48
+ it('updates only the targeted slice, leaving siblings untouched', async () => {
49
+ await act(async () => root.render(createElement(Probe)));
50
+ const audioBefore = api!.settings.audio;
51
+
52
+ await act(async () => api!.updateDock({ side: 'left' }));
53
+
54
+ expect(api!.settings.dock.side).toBe('left');
55
+ // Audio slice identity is unchanged — a feature never touches another's.
56
+ expect(api!.settings.audio).toBe(audioBefore);
57
+ });
58
+
59
+ it('setPageContextLinked persists the opt-in under the central key', async () => {
60
+ await act(async () => root.render(createElement(Probe)));
61
+ await act(async () => api!.setPageContextLinked(true));
62
+ await flush();
63
+
64
+ expect(api!.settings.pageContext.linked).toBe(true);
65
+ const stored = JSON.parse(
66
+ window.localStorage.getItem(CHAT_SETTINGS_STORAGE_KEY)!,
67
+ );
68
+ expect(stored.pageContext.linked).toBe(true);
69
+ });
70
+
71
+ it('reset restores every slice to defaults', async () => {
72
+ await act(async () => root.render(createElement(Probe)));
73
+ await act(async () => {
74
+ api!.setAudioMuted(true);
75
+ api!.updateDock({ side: 'left' });
76
+ });
77
+ await flush();
78
+ expect(api!.settings.audio.muted).toBe(true);
79
+
80
+ await act(async () => api!.reset());
81
+ expect(api!.settings.audio.muted).toBe(false);
82
+ expect(api!.settings.dock.side).toBe('right');
83
+ });
84
+ });
@@ -0,0 +1,138 @@
1
+ // @vitest-environment jsdom
2
+ //
3
+ // Covers the Part-1 additions to `@djangocfg/ui-core`'s `useLocalStorage`:
4
+ // partial-merge `patch`, no-op skip, write coalescing, and same-tab
5
+ // cross-instance sync. ui-core ships no test runner, so the test lives
6
+ // here (ui-tools has vitest + jsdom and resolves ui-core via workspace).
7
+ import { act, createElement } from 'react';
8
+ import { createRoot, type Root } from 'react-dom/client';
9
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
10
+
11
+ import { useLocalStorage } from '@djangocfg/ui-core/hooks';
12
+
13
+ (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
14
+
15
+ interface Prefs {
16
+ a: number;
17
+ b: number;
18
+ c: string;
19
+ }
20
+
21
+ let container: HTMLDivElement;
22
+ let root: Root;
23
+
24
+ beforeEach(() => {
25
+ window.localStorage.clear();
26
+ container = document.createElement('div');
27
+ document.body.appendChild(container);
28
+ root = createRoot(container);
29
+ });
30
+
31
+ afterEach(() => {
32
+ act(() => root.unmount());
33
+ container.remove();
34
+ window.localStorage.clear();
35
+ });
36
+
37
+ /** Flush pending microtask-coalesced writes. */
38
+ async function flushMicrotasks() {
39
+ await act(async () => {
40
+ await Promise.resolve();
41
+ });
42
+ }
43
+
44
+ describe('useLocalStorage — patch / coalescing / sync', () => {
45
+ it('patch shallow-merges a subset of keys', async () => {
46
+ let api: ReturnType<typeof useLocalStorage<Prefs>> | null = null;
47
+ function Probe() {
48
+ api = useLocalStorage<Prefs>('t.patch', { a: 1, b: 2, c: 'x' });
49
+ return null;
50
+ }
51
+ await act(async () => root.render(createElement(Probe)));
52
+
53
+ await act(async () => {
54
+ api![3]({ b: 99 });
55
+ });
56
+ expect(api![0]).toEqual({ a: 1, b: 99, c: 'x' });
57
+
58
+ await flushMicrotasks();
59
+ expect(JSON.parse(window.localStorage.getItem('t.patch')!)).toEqual({
60
+ a: 1,
61
+ b: 99,
62
+ c: 'x',
63
+ });
64
+ });
65
+
66
+ it('patch is a no-op when nothing changes', async () => {
67
+ let renders = 0;
68
+ let api: ReturnType<typeof useLocalStorage<Prefs>> | null = null;
69
+ function Probe() {
70
+ renders += 1;
71
+ api = useLocalStorage<Prefs>('t.noop', { a: 1, b: 2, c: 'x' });
72
+ return null;
73
+ }
74
+ await act(async () => root.render(createElement(Probe)));
75
+ await flushMicrotasks();
76
+ const rendersAfterMount = renders;
77
+
78
+ // Patching with the same values must not trigger a write/re-render.
79
+ await act(async () => {
80
+ api![3]({ a: 1, b: 2 });
81
+ });
82
+ await flushMicrotasks();
83
+
84
+ expect(renders).toBe(rendersAfterMount);
85
+ // No write happened — key stays absent.
86
+ expect(window.localStorage.getItem('t.noop')).toBeNull();
87
+ });
88
+
89
+ it('coalesces several patches in one tick into a single write', async () => {
90
+ let api: ReturnType<typeof useLocalStorage<Prefs>> | null = null;
91
+ function Probe() {
92
+ api = useLocalStorage<Prefs>('t.coalesce', { a: 0, b: 0, c: '' });
93
+ return null;
94
+ }
95
+ await act(async () => root.render(createElement(Probe)));
96
+
97
+ // Spy on the prototype — jsdom's per-instance `setItem` is read-only.
98
+ let writes = 0;
99
+ const realSetItem = Storage.prototype.setItem;
100
+ Storage.prototype.setItem = function patched(this: Storage, k, v) {
101
+ if (k === 't.coalesce') writes += 1;
102
+ return realSetItem.call(this, k, v);
103
+ };
104
+
105
+ await act(async () => {
106
+ api![3]({ a: 1 });
107
+ api![3]({ b: 2 });
108
+ api![3]({ c: 'z' });
109
+ });
110
+ await flushMicrotasks();
111
+ Storage.prototype.setItem = realSetItem;
112
+
113
+ // State reflects all three patches...
114
+ expect(api![0]).toEqual({ a: 1, b: 2, c: 'z' });
115
+ // ...but they collapsed into exactly one localStorage write.
116
+ expect(writes).toBe(1);
117
+ });
118
+
119
+ it('two hook instances on the same key stay in sync (same tab)', async () => {
120
+ let a: ReturnType<typeof useLocalStorage<Prefs>> | null = null;
121
+ let b: ReturnType<typeof useLocalStorage<Prefs>> | null = null;
122
+ function Probe() {
123
+ a = useLocalStorage<Prefs>('t.sync', { a: 1, b: 1, c: 'x' });
124
+ b = useLocalStorage<Prefs>('t.sync', { a: 1, b: 1, c: 'x' });
125
+ return null;
126
+ }
127
+ await act(async () => root.render(createElement(Probe)));
128
+
129
+ await act(async () => {
130
+ a![3]({ b: 42 });
131
+ });
132
+ await flushMicrotasks();
133
+
134
+ // The sibling instance saw the write without its own setter being called.
135
+ expect(a![0]).toEqual({ a: 1, b: 42, c: 'x' });
136
+ expect(b![0]).toEqual({ a: 1, b: 42, c: 'x' });
137
+ });
138
+ });
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * @djangocfg/ui-tools — Chat / settings
5
+ *
6
+ * The one place every chat-owned persisted setting lives. See README.md.
7
+ */
8
+
9
+ export {
10
+ CHAT_SETTINGS_STORAGE_KEY,
11
+ DEFAULT_CHAT_SETTINGS,
12
+ type ChatSettings,
13
+ type ChatDockSettings,
14
+ type ChatAudioSettings,
15
+ type ChatSpeechSettings,
16
+ type ChatPageContextSettings,
17
+ } from './types';
18
+
19
+ export {
20
+ useChatSettings,
21
+ type UseChatSettingsOptions,
22
+ type UseChatSettingsReturn,
23
+ } from './useChatSettings';
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+
3
+ import type { ChatDisplayMode } from '../types';
4
+
5
+ /**
6
+ * Dock / layout preferences.
7
+ *
8
+ * Mirrors the fields the existing `useChatDockPrefs` / `useChatLayout`
9
+ * persist today. Kept as a nested slice so a feature can `updateDock({...})`
10
+ * without touching audio / speech / page-context state.
11
+ */
12
+ export interface ChatDockSettings {
13
+ /** Floating popover vs. side-docked panel vs. embedded, etc. */
14
+ mode: ChatDisplayMode;
15
+ /** Which edge a side dock attaches to. */
16
+ side: 'left' | 'right';
17
+ /** Width in px when side-docked. */
18
+ width: number;
19
+ }
20
+
21
+ /**
22
+ * Audio / notification-sound preferences.
23
+ *
24
+ * `eventsMuted` is a sparse map: only events the user explicitly toggled
25
+ * off appear here. An absent key means "use the default for that event".
26
+ */
27
+ export interface ChatAudioSettings {
28
+ /** Master mute — silences every chat sound. */
29
+ muted: boolean;
30
+ /** Master volume, 0..1. */
31
+ volume: number;
32
+ /** Per-event opt-outs, keyed by chat audio event name. */
33
+ eventsMuted: Record<string, boolean>;
34
+ }
35
+
36
+ /**
37
+ * Speech-recognition / language-picker preferences.
38
+ *
39
+ * `language` is a BCP-47 tag the user explicitly picked, or `null` for
40
+ * "no override" — callers then fall back to the app locale.
41
+ */
42
+ export interface ChatSpeechSettings {
43
+ /** Explicit language override, or `null` to follow the app locale. */
44
+ language: string | null;
45
+ /** Preferred capture device id, or `null` for the system default. */
46
+ deviceId: string | null;
47
+ /** Preferred STT engine id, or `null` for auto-select. */
48
+ engineId: string | null;
49
+ /** Earcon (audio cue) feedback on dictation start/stop. */
50
+ earcons: boolean;
51
+ }
52
+
53
+ /** Page-context (screen-sharing) opt-in. */
54
+ export interface ChatPageContextSettings {
55
+ /**
56
+ * Whether the user has opted in to sharing a snapshot of the page they
57
+ * are viewing with the assistant. Persisted so the choice survives
58
+ * reloads — the user enables it once.
59
+ */
60
+ linked: boolean;
61
+ }
62
+
63
+ /**
64
+ * The single, typed shape for every chat-owned persisted setting.
65
+ *
66
+ * Centralization rationale: chat features used to each call
67
+ * `useLocalStorage` with their own ad-hoc key. That made it impossible to
68
+ * reason about (or export/import) "the chat's settings" as one thing, and
69
+ * scattered the SSR/migration concerns. One object, one storage key, one
70
+ * hook — every feature reads/writes its own slice through it.
71
+ */
72
+ export interface ChatSettings {
73
+ dock: ChatDockSettings;
74
+ audio: ChatAudioSettings;
75
+ speech: ChatSpeechSettings;
76
+ pageContext: ChatPageContextSettings;
77
+ }
78
+
79
+ /** Baseline defaults — used for SSR and first-run (nothing stored yet). */
80
+ export const DEFAULT_CHAT_SETTINGS: ChatSettings = {
81
+ dock: {
82
+ mode: 'closed',
83
+ side: 'right',
84
+ width: 420,
85
+ },
86
+ audio: {
87
+ muted: false,
88
+ volume: 1,
89
+ eventsMuted: {},
90
+ },
91
+ speech: {
92
+ language: null,
93
+ deviceId: null,
94
+ engineId: null,
95
+ earcons: false,
96
+ },
97
+ pageContext: {
98
+ linked: false,
99
+ },
100
+ };
101
+
102
+ /**
103
+ * Single localStorage key for the whole chat-settings object.
104
+ *
105
+ * One key (vs. one-per-feature) so the entire chat configuration is a
106
+ * single read/write/coalesce unit — see {@link useChatSettings}.
107
+ */
108
+ export const CHAT_SETTINGS_STORAGE_KEY = 'djc.chat.settings';
@@ -0,0 +1,168 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useMemo } from 'react';
4
+
5
+ import { useLocalStorage } from '@djangocfg/ui-core/hooks';
6
+
7
+ import {
8
+ CHAT_SETTINGS_STORAGE_KEY,
9
+ DEFAULT_CHAT_SETTINGS,
10
+ type ChatAudioSettings,
11
+ type ChatDockSettings,
12
+ type ChatPageContextSettings,
13
+ type ChatSettings,
14
+ type ChatSpeechSettings,
15
+ } from './types';
16
+
17
+ /** Options for {@link useChatSettings}. */
18
+ export interface UseChatSettingsOptions {
19
+ /**
20
+ * Override the storage key (per-product scoping). Defaults to the
21
+ * shared {@link CHAT_SETTINGS_STORAGE_KEY}.
22
+ */
23
+ storageKey?: string;
24
+ /**
25
+ * Override baseline defaults — merged over {@link DEFAULT_CHAT_SETTINGS}
26
+ * at the slice level (per-product branding, host opt-ins, etc.).
27
+ */
28
+ defaults?: Partial<ChatSettings>;
29
+ }
30
+
31
+ /** Everything {@link useChatSettings} exposes. */
32
+ export interface UseChatSettingsReturn {
33
+ /** The full, typed settings object (each slice always present). */
34
+ settings: ChatSettings;
35
+
36
+ /**
37
+ * Top-level grouped patch — shallow-merges whole slices. Use this for
38
+ * cross-slice updates; for a single slice prefer the typed helpers.
39
+ */
40
+ patch: (partial: Partial<ChatSettings>) => void;
41
+
42
+ /** Merge a subset of dock fields. */
43
+ updateDock: (partial: Partial<ChatDockSettings>) => void;
44
+ /** Merge a subset of audio fields. */
45
+ updateAudio: (partial: Partial<ChatAudioSettings>) => void;
46
+ /** Merge a subset of speech fields. */
47
+ updateSpeech: (partial: Partial<ChatSpeechSettings>) => void;
48
+ /** Merge a subset of page-context fields. */
49
+ updatePageContext: (partial: Partial<ChatPageContextSettings>) => void;
50
+
51
+ /** Master audio mute shortcut. */
52
+ setAudioMuted: (muted: boolean) => void;
53
+ /** Page-context opt-in shortcut. */
54
+ setPageContextLinked: (linked: boolean) => void;
55
+
56
+ /** Reset every slice back to defaults. */
57
+ reset: () => void;
58
+ }
59
+
60
+ /** Shallow-merge `defaults` slices over the baseline. */
61
+ function resolveDefaults(defaults?: Partial<ChatSettings>): ChatSettings {
62
+ if (!defaults) return DEFAULT_CHAT_SETTINGS;
63
+ return {
64
+ dock: { ...DEFAULT_CHAT_SETTINGS.dock, ...defaults.dock },
65
+ audio: { ...DEFAULT_CHAT_SETTINGS.audio, ...defaults.audio },
66
+ speech: { ...DEFAULT_CHAT_SETTINGS.speech, ...defaults.speech },
67
+ pageContext: {
68
+ ...DEFAULT_CHAT_SETTINGS.pageContext,
69
+ ...defaults.pageContext,
70
+ },
71
+ };
72
+ }
73
+
74
+ /**
75
+ * The single entry point for chat-owned persisted settings.
76
+ *
77
+ * Every chat feature that wants to remember a preference goes through
78
+ * here instead of calling `useLocalStorage` with its own key. Why:
79
+ *
80
+ * - One typed object, one storage key — "the chat's settings" is a single
81
+ * thing that can be reasoned about, exported, or reset wholesale.
82
+ * - Coalesced writes: `useLocalStorage` (Part 1) updates React state
83
+ * synchronously but batches the actual `localStorage` write to one per
84
+ * tick — so several features patching different slices in the same
85
+ * render commit collapse into a single write, no thrash.
86
+ * - Cross-instance sync: two `useChatSettings()` consumers (e.g. the
87
+ * header audio toggle and the settings panel) stay in lockstep within
88
+ * the same tab and across tabs.
89
+ *
90
+ * Each helper updates only its own slice, leaving sibling slices
91
+ * untouched — a feature never has to know about the others.
92
+ *
93
+ * SSR-safe: returns defaults on the server, hydrates on mount.
94
+ */
95
+ export function useChatSettings(
96
+ opts: UseChatSettingsOptions = {},
97
+ ): UseChatSettingsReturn {
98
+ const key = opts.storageKey ?? CHAT_SETTINGS_STORAGE_KEY;
99
+ // Memoized so the resolved defaults keep a stable identity — passing a
100
+ // fresh object to `useLocalStorage` each render is harmless but wasteful.
101
+ const initial = useMemo(
102
+ () => resolveDefaults(opts.defaults),
103
+ [opts.defaults],
104
+ );
105
+
106
+ const [settings, setSettings, , patch] = useLocalStorage<ChatSettings>(
107
+ key,
108
+ initial,
109
+ );
110
+
111
+ // Slice updaters: read the current slice, shallow-merge the partial,
112
+ // write back via the coalescing top-level `patch`. The inner
113
+ // `useLocalStorage` `patch` already skips no-op writes.
114
+ const updateDock = useCallback(
115
+ (partial: Partial<ChatDockSettings>) => {
116
+ patch({ dock: { ...settings.dock, ...partial } });
117
+ },
118
+ [patch, settings.dock],
119
+ );
120
+
121
+ const updateAudio = useCallback(
122
+ (partial: Partial<ChatAudioSettings>) => {
123
+ patch({ audio: { ...settings.audio, ...partial } });
124
+ },
125
+ [patch, settings.audio],
126
+ );
127
+
128
+ const updateSpeech = useCallback(
129
+ (partial: Partial<ChatSpeechSettings>) => {
130
+ patch({ speech: { ...settings.speech, ...partial } });
131
+ },
132
+ [patch, settings.speech],
133
+ );
134
+
135
+ const updatePageContext = useCallback(
136
+ (partial: Partial<ChatPageContextSettings>) => {
137
+ patch({ pageContext: { ...settings.pageContext, ...partial } });
138
+ },
139
+ [patch, settings.pageContext],
140
+ );
141
+
142
+ const setAudioMuted = useCallback(
143
+ (muted: boolean) => updateAudio({ muted }),
144
+ [updateAudio],
145
+ );
146
+
147
+ const setPageContextLinked = useCallback(
148
+ (linked: boolean) => updatePageContext({ linked }),
149
+ [updatePageContext],
150
+ );
151
+
152
+ const reset = useCallback(
153
+ () => setSettings(initial),
154
+ [setSettings, initial],
155
+ );
156
+
157
+ return {
158
+ settings,
159
+ patch,
160
+ updateDock,
161
+ updateAudio,
162
+ updateSpeech,
163
+ updatePageContext,
164
+ setAudioMuted,
165
+ setPageContextLinked,
166
+ reset,
167
+ };
168
+ }