@djangocfg/ui-tools 2.1.384 → 2.1.387
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/README.md +25 -11
- package/dist/ChatRoot-4KM2JMGA.mjs +6 -0
- package/dist/{ChatRoot-JVR3M3H2.mjs.map → ChatRoot-4KM2JMGA.mjs.map} +1 -1
- package/dist/ChatRoot-OILWMMZ6.cjs +15 -0
- package/dist/{ChatRoot-LXIUBOXF.cjs.map → ChatRoot-OILWMMZ6.cjs.map} +1 -1
- package/dist/DictationField-AS2F33WI.cjs +13 -0
- package/dist/{DictationField-U25MEYAL.mjs.map → DictationField-AS2F33WI.cjs.map} +1 -1
- package/dist/DictationField-WPONUCYE.mjs +4 -0
- package/dist/{DictationField-XWR5VOID.cjs.map → DictationField-WPONUCYE.mjs.map} +1 -1
- package/dist/MapContainer-AKIPABJK.mjs +4 -0
- package/dist/MapContainer-AKIPABJK.mjs.map +1 -0
- package/dist/MapContainer-STVDMC36.cjs +17 -0
- package/dist/MapContainer-STVDMC36.cjs.map +1 -0
- package/dist/{MapContainer-76YL2JXL.cjs → chunk-5D2OCOPQ.cjs} +3 -2
- package/dist/chunk-5D2OCOPQ.cjs.map +1 -0
- package/dist/{MapContainer-7HXBI3OH.mjs → chunk-7CWGZPO3.mjs} +3 -3
- package/dist/chunk-7CWGZPO3.mjs.map +1 -0
- package/dist/{chunk-4PFW7MIJ.cjs → chunk-ADEN3UA4.cjs} +60 -5
- package/dist/chunk-ADEN3UA4.cjs.map +1 -0
- package/dist/chunk-BVESQTBM.mjs +1439 -0
- package/dist/chunk-BVESQTBM.mjs.map +1 -0
- package/dist/{chunk-PEKBT75W.mjs → chunk-DMX7W4XZ.mjs} +53 -1387
- package/dist/chunk-DMX7W4XZ.mjs.map +1 -0
- package/dist/chunk-HNIMIIFR.mjs +1361 -0
- package/dist/chunk-HNIMIIFR.mjs.map +1 -0
- package/dist/chunk-L25HA3TM.cjs +1478 -0
- package/dist/chunk-L25HA3TM.cjs.map +1 -0
- package/dist/{chunk-HPK3EWBF.cjs → chunk-TBSHZO5R.cjs} +50 -1409
- package/dist/chunk-TBSHZO5R.cjs.map +1 -0
- package/dist/chunk-TSNRU3UO.cjs +1387 -0
- package/dist/chunk-TSNRU3UO.cjs.map +1 -0
- package/dist/{chunk-C2YN6WEO.mjs → chunk-UNCS5V5F.mjs} +61 -7
- package/dist/chunk-UNCS5V5F.mjs.map +1 -0
- package/dist/index.cjs +1236 -1768
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +780 -780
- package/dist/index.d.ts +780 -780
- package/dist/index.mjs +853 -1423
- package/dist/index.mjs.map +1 -1
- package/dist/launcher-5WYPDPEP.mjs +7 -0
- package/dist/launcher-5WYPDPEP.mjs.map +1 -0
- package/dist/launcher-FCI3LTDY.css +7 -0
- package/dist/launcher-FCI3LTDY.css.map +1 -0
- package/dist/launcher-QAOG2NUI.cjs +60 -0
- package/dist/launcher-QAOG2NUI.cjs.map +1 -0
- package/package.json +23 -18
- package/src/tools/AudioPlayer/lazy.tsx +100 -0
- package/src/tools/Chat/README.md +85 -1
- package/src/tools/Chat/context/ChatProvider.tsx +42 -0
- package/src/tools/Chat/lazy.tsx +213 -1
- package/src/tools/CodeEditor/lazy.tsx +70 -0
- package/src/tools/Map/lazy.tsx +38 -1
- package/src/tools/MarkdownEditor/lazy.tsx +42 -0
- package/src/tools/SpeechRecognition/README.md +48 -0
- package/src/tools/SpeechRecognition/core/index.ts +6 -1
- package/src/tools/SpeechRecognition/core/logger.ts +107 -1
- package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +15 -4
- package/src/tools/SpeechRecognition/index.ts +9 -0
- package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +37 -2
- package/dist/ChatRoot-JVR3M3H2.mjs +0 -5
- package/dist/ChatRoot-LXIUBOXF.cjs +0 -14
- package/dist/DictationField-U25MEYAL.mjs +0 -4
- package/dist/DictationField-XWR5VOID.cjs +0 -13
- package/dist/MapContainer-76YL2JXL.cjs.map +0 -1
- package/dist/MapContainer-7HXBI3OH.mjs.map +0 -1
- package/dist/chunk-4PFW7MIJ.cjs.map +0 -1
- package/dist/chunk-C2YN6WEO.mjs.map +0 -1
- package/dist/chunk-HPK3EWBF.cjs.map +0 -1
- package/dist/chunk-PEKBT75W.mjs.map +0 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `@djangocfg/ui-tools/code-editor` subpath entrypoint.
|
|
5
|
+
*
|
|
6
|
+
* Monaco itself weighs ~550 KB minified — we must never pay that cost
|
|
7
|
+
* unless the editor actually mounts. The `Lazy*` wrappers here dynamically
|
|
8
|
+
* import the `Editor` and `DiffEditor` components, which transitively pull
|
|
9
|
+
* in `monaco-editor` and the worker setup.
|
|
10
|
+
*
|
|
11
|
+
* The rest of the surface (types, EditorProvider, useMonaco hook, helpers)
|
|
12
|
+
* is light:
|
|
13
|
+
* - `monaco-editor` is referenced only as a TS type (`type *`) — erased
|
|
14
|
+
* at compile time.
|
|
15
|
+
* - `useMonaco` performs a dynamic `import('monaco-editor')` itself, so
|
|
16
|
+
* importing the hook is free until the caller actually invokes it.
|
|
17
|
+
*
|
|
18
|
+
* That means consumers can wire up keyboard shortcuts, theme toggles, or
|
|
19
|
+
* file-state hooks at the top of their tree without paying Monaco's cost,
|
|
20
|
+
* and only render `<LazyEditor>` lower in the tree where it's needed.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { createLazyComponent, LoadingFallback } from '../../components';
|
|
24
|
+
import type { EditorProps, DiffEditorProps } from './types';
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Lazy components
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
export const LazyEditor = createLazyComponent<EditorProps>(
|
|
31
|
+
() => import('./components/Editor').then((m) => ({ default: m.Editor })),
|
|
32
|
+
{
|
|
33
|
+
displayName: 'LazyEditor',
|
|
34
|
+
fallback: <LoadingFallback minHeight={320} text="Loading editor…" />,
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export const LazyDiffEditor = createLazyComponent<DiffEditorProps>(
|
|
39
|
+
() => import('./components/DiffEditor').then((m) => ({ default: m.DiffEditor })),
|
|
40
|
+
{
|
|
41
|
+
displayName: 'LazyDiffEditor',
|
|
42
|
+
fallback: <LoadingFallback minHeight={320} text="Loading diff editor…" />,
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Light surface
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
// Hooks — `useMonaco` does its own dynamic import; the others are pure.
|
|
51
|
+
export {
|
|
52
|
+
useMonaco,
|
|
53
|
+
useEditor,
|
|
54
|
+
useLanguage,
|
|
55
|
+
useEditorTheme,
|
|
56
|
+
} from './hooks';
|
|
57
|
+
|
|
58
|
+
// Provider + context hook
|
|
59
|
+
export { EditorProvider, useEditorContext } from './context';
|
|
60
|
+
|
|
61
|
+
// All types
|
|
62
|
+
export type {
|
|
63
|
+
EditorFile,
|
|
64
|
+
EditorOptions,
|
|
65
|
+
EditorProps,
|
|
66
|
+
EditorContextValue,
|
|
67
|
+
UseEditorReturn,
|
|
68
|
+
UseMonacoReturn,
|
|
69
|
+
DiffEditorProps,
|
|
70
|
+
} from './types';
|
package/src/tools/Map/lazy.tsx
CHANGED
|
@@ -42,11 +42,48 @@ export const LazyMapView = createLazyComponent(
|
|
|
42
42
|
);
|
|
43
43
|
|
|
44
44
|
// ============================================================================
|
|
45
|
-
//
|
|
45
|
+
// Light primitives — direct re-exports
|
|
46
|
+
//
|
|
47
|
+
// MapMarker / MapPopup / MapCluster / MapSource / MapLayer / MapControls
|
|
48
|
+
// etc. are thin wrappers around `react-map-gl/maplibre`. They don't import
|
|
49
|
+
// `maplibre-gl` at module scope (only types, which are erased), so exporting
|
|
50
|
+
// them synchronously here costs ~tens of KB at most — not the ~800KB of
|
|
51
|
+
// MapLibre GL itself.
|
|
52
|
+
//
|
|
53
|
+
// The heavy library only loads when `LazyMapContainer` actually mounts,
|
|
54
|
+
// because `MapContainer.tsx` (the only module that imports `maplibre-gl`
|
|
55
|
+
// at runtime) is reached exclusively via the dynamic import above.
|
|
56
|
+
//
|
|
57
|
+
// This means consumers can write:
|
|
58
|
+
//
|
|
59
|
+
// import { LazyMapContainer, MapMarker, MapPopup } from '@djangocfg/ui-tools/map'
|
|
60
|
+
//
|
|
61
|
+
// …and still get correct code-splitting.
|
|
46
62
|
// ============================================================================
|
|
47
63
|
|
|
64
|
+
export {
|
|
65
|
+
MapMarker,
|
|
66
|
+
MapPopup,
|
|
67
|
+
MapCluster,
|
|
68
|
+
MapSource,
|
|
69
|
+
MapLayer,
|
|
70
|
+
MapControls,
|
|
71
|
+
CustomOverlay,
|
|
72
|
+
MapLegend,
|
|
73
|
+
LayerSwitcher,
|
|
74
|
+
} from './components';
|
|
75
|
+
|
|
76
|
+
export { MapProvider, useMapContext, MapContext } from './context';
|
|
77
|
+
export type { MapProviderProps } from './context';
|
|
78
|
+
|
|
48
79
|
export type {
|
|
49
80
|
MapContainerProps,
|
|
81
|
+
MapMarkerProps,
|
|
82
|
+
MapPopupProps,
|
|
83
|
+
MapClusterProps,
|
|
84
|
+
MapSourceProps,
|
|
85
|
+
MapLayerProps,
|
|
86
|
+
MapControlsProps,
|
|
50
87
|
} from './components';
|
|
51
88
|
|
|
52
89
|
export type {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `@djangocfg/ui-tools/markdown-editor` subpath entrypoint.
|
|
5
|
+
*
|
|
6
|
+
* `MarkdownEditor` is a TipTap WYSIWYG with the full ProseMirror + TipTap
|
|
7
|
+
* extension stack — `@tiptap/react`, `starter-kit`, `markdown`, `mention`,
|
|
8
|
+
* `placeholder`, plus our `createMentionSuggestion` (floating-ui anchored
|
|
9
|
+
* dropdown). Together that's ~200 KB minified — wrap it in React.lazy so
|
|
10
|
+
* pages that don't render an editor don't pay.
|
|
11
|
+
*
|
|
12
|
+
* Light surface kept here:
|
|
13
|
+
* - All public types (erased at compile time).
|
|
14
|
+
* - `mentionPresets` — pure data describing how to render mentions to
|
|
15
|
+
* markdown. No TipTap imports at module scope.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createLazyComponent, LoadingFallback } from '../../components';
|
|
19
|
+
import type { MarkdownEditorProps } from './MarkdownEditor';
|
|
20
|
+
|
|
21
|
+
export const LazyMarkdownEditor = createLazyComponent<MarkdownEditorProps>(
|
|
22
|
+
() => import('./MarkdownEditor').then((m) => ({ default: m.MarkdownEditor })),
|
|
23
|
+
{
|
|
24
|
+
displayName: 'LazyMarkdownEditor',
|
|
25
|
+
fallback: <LoadingFallback minHeight={140} text="Loading editor…" />,
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Light surface — pure helpers + types.
|
|
30
|
+
export { mentionPresets } from './mentionPresets';
|
|
31
|
+
|
|
32
|
+
export type {
|
|
33
|
+
MarkdownEditorProps,
|
|
34
|
+
MarkdownEditorHandle,
|
|
35
|
+
} from './MarkdownEditor';
|
|
36
|
+
|
|
37
|
+
export type {
|
|
38
|
+
MentionItem,
|
|
39
|
+
MentionConfig,
|
|
40
|
+
MentionAttrs,
|
|
41
|
+
MentionMarkdownRenderer,
|
|
42
|
+
} from './types';
|
|
@@ -278,6 +278,54 @@ const unsubscribe = useSpeechPrefs.subscribe((state) => {
|
|
|
278
278
|
|
|
279
279
|
---
|
|
280
280
|
|
|
281
|
+
## Debug logger
|
|
282
|
+
|
|
283
|
+
Scoped, namespaced [consola](https://github.com/unjs/consola) wrapper that silences itself in production by default. Mirrors `getChatLogger()` in the Chat tool so both surfaces feel the same in DevTools.
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
import { getSpeechLogger } from '@djangocfg/ui-tools';
|
|
287
|
+
|
|
288
|
+
const log = getSpeechLogger();
|
|
289
|
+
log.dictation.info('final merged', { len: 42 });
|
|
290
|
+
log.engine.debug('state', 'listening');
|
|
291
|
+
log.error.error('engine threw', err);
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Sub-loggers: `engine`, `dictation`, `slot`, `composer`, `mic`, `push`, `error`. `error` always emits; everything else is gated.
|
|
295
|
+
|
|
296
|
+
**Opt-in (any one is enough):**
|
|
297
|
+
|
|
298
|
+
1. **Dev mode** — `NODE_ENV === 'development'` auto-enables everything.
|
|
299
|
+
2. **Runtime toggle** — paste this in DevTools to enable without a rebuild:
|
|
300
|
+
```js
|
|
301
|
+
localStorage.setItem('djangocfg:speech-debug', '1');
|
|
302
|
+
location.reload();
|
|
303
|
+
```
|
|
304
|
+
`'0'` (or `removeItem`) turns it back off.
|
|
305
|
+
3. **Explicit** — `getSpeechLogger(true)` from a host component (analogous to `<ChatRoot debug />`).
|
|
306
|
+
|
|
307
|
+
**What you'll see when on**, in order of a typical dictation session:
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
[speech][slot] mount { supported: true, hasComposerHandle: true, … }
|
|
311
|
+
[speech][engine] subscribe { engineId: 'webspeech' }
|
|
312
|
+
[speech][engine] state 'listening'
|
|
313
|
+
[speech][engine] partial { len: 6, segmentId: 's1' }
|
|
314
|
+
[speech][composer] setValue → composer handle { len: 12 }
|
|
315
|
+
[speech][engine] final { len: 42, confidence: 0.91 }
|
|
316
|
+
[speech][dictation] final merged { len: 42, totalLen: 54 }
|
|
317
|
+
[speech][engine] autoStop silence detected
|
|
318
|
+
[speech][engine] state 'closed'
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
If text never appears in your composer, look for:
|
|
322
|
+
|
|
323
|
+
- `[speech][slot] mount { hasComposerHandle: false, … }` → `<VoiceComposerSlot>` is outside a `<ChatProvider>` and no `value`/`onChange` props were given — text is going nowhere.
|
|
324
|
+
- `[speech][composer] warn setValue called but no composer handle is registered …` → the composer never called `useRegisterComposer(...)`. Built-in `<Composer>` and `MarkdownEditor` do this automatically; custom composers must opt in.
|
|
325
|
+
- `[speech][engine] final` arrives but no `[speech][dictation] final merged` follows → check `normaliseFinal` filtered the text (empty / whitespace only).
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
281
329
|
## Public surface
|
|
282
330
|
|
|
283
331
|
### Hooks
|
|
@@ -1,3 +1,109 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Speech recognition dev logger.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `getChatLogger()` in the Chat tool so consumers reason about both
|
|
5
|
+
* surfaces the same way. Silent in production by default; emits `error`
|
|
6
|
+
* level always.
|
|
7
|
+
*
|
|
8
|
+
* Opt-in mechanisms (any one is enough):
|
|
9
|
+
* 1. `NODE_ENV === 'development'` (auto, via `isDev` from `@djangocfg/ui-core`).
|
|
10
|
+
* 2. `localStorage.setItem('djangocfg:speech-debug', '1')` — runtime
|
|
11
|
+
* toggle without a rebuild. Useful for diagnosing a prod bug.
|
|
12
|
+
* 3. Explicit `getSpeechLogger(true)` from a host component.
|
|
13
|
+
*
|
|
14
|
+
* Sub-loggers:
|
|
15
|
+
* engine — engine lifecycle (start/stop/abort/result)
|
|
16
|
+
* dictation — `useDictation` state (anchor, partial → final merge)
|
|
17
|
+
* slot — `VoiceComposerSlot` UI (mount, support gating, button)
|
|
18
|
+
* composer — composer handle registry (focus / setValue / pin caret)
|
|
19
|
+
* mic — `useMicLevel` / `useMicDevices` / device picker
|
|
20
|
+
* push — push-to-talk hotkey events
|
|
21
|
+
* error — caught errors (always emitted, even when disabled)
|
|
22
|
+
*/
|
|
23
|
+
import { consola, type ConsolaInstance } from 'consola';
|
|
2
24
|
|
|
25
|
+
import { isBrowser, isDev } from '@djangocfg/ui-core/lib';
|
|
26
|
+
|
|
27
|
+
export type SpeechLogScope =
|
|
28
|
+
| 'engine'
|
|
29
|
+
| 'dictation'
|
|
30
|
+
| 'slot'
|
|
31
|
+
| 'composer'
|
|
32
|
+
| 'mic'
|
|
33
|
+
| 'push'
|
|
34
|
+
| 'error';
|
|
35
|
+
|
|
36
|
+
export interface SpeechLogger {
|
|
37
|
+
engine: ConsolaInstance;
|
|
38
|
+
dictation: ConsolaInstance;
|
|
39
|
+
slot: ConsolaInstance;
|
|
40
|
+
composer: ConsolaInstance;
|
|
41
|
+
mic: ConsolaInstance;
|
|
42
|
+
push: ConsolaInstance;
|
|
43
|
+
error: ConsolaInstance;
|
|
44
|
+
/** True when this logger is actually emitting. */
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const SCOPES: SpeechLogScope[] = [
|
|
49
|
+
'engine',
|
|
50
|
+
'dictation',
|
|
51
|
+
'slot',
|
|
52
|
+
'composer',
|
|
53
|
+
'mic',
|
|
54
|
+
'push',
|
|
55
|
+
'error',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const STORAGE_KEY = 'djangocfg:speech-debug';
|
|
59
|
+
|
|
60
|
+
function readStorageFlag(): boolean {
|
|
61
|
+
if (!isBrowser) return false;
|
|
62
|
+
try {
|
|
63
|
+
return window.localStorage.getItem(STORAGE_KEY) === '1';
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const cache = new Map<boolean, SpeechLogger>();
|
|
70
|
+
|
|
71
|
+
function buildLogger(enabled: boolean): SpeechLogger {
|
|
72
|
+
const root = consola.withTag('speech');
|
|
73
|
+
const subs = Object.fromEntries(
|
|
74
|
+
SCOPES.map((scope) => [scope, root.withTag(scope)]),
|
|
75
|
+
) as Record<SpeechLogScope, ConsolaInstance>;
|
|
76
|
+
|
|
77
|
+
if (!enabled) {
|
|
78
|
+
for (const scope of SCOPES) {
|
|
79
|
+
if (scope === 'error') continue;
|
|
80
|
+
subs[scope].level = -999;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { ...subs, enabled };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the speech logger.
|
|
89
|
+
*
|
|
90
|
+
* @param debug Explicit override. `undefined` falls back to `isDev` OR
|
|
91
|
+
* `localStorage['djangocfg:speech-debug'] === '1'`.
|
|
92
|
+
*/
|
|
93
|
+
export function getSpeechLogger(debug?: boolean): SpeechLogger {
|
|
94
|
+
const enabled = debug ?? (isDev || readStorageFlag());
|
|
95
|
+
let logger = cache.get(enabled);
|
|
96
|
+
if (!logger) {
|
|
97
|
+
logger = buildLogger(enabled);
|
|
98
|
+
cache.set(enabled, logger);
|
|
99
|
+
}
|
|
100
|
+
return logger;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Legacy flat-tag logger. New code should prefer `getSpeechLogger()` for
|
|
105
|
+
* scoped output and the runtime opt-in switch.
|
|
106
|
+
*
|
|
107
|
+
* @deprecated Use `getSpeechLogger()`.
|
|
108
|
+
*/
|
|
3
109
|
export const sttLogger = consola.withTag('ui-tools:speech');
|
|
@@ -6,9 +6,11 @@ import {
|
|
|
6
6
|
EMPTY_TRANSCRIPT,
|
|
7
7
|
INITIAL_STATE,
|
|
8
8
|
buildTranscript,
|
|
9
|
+
getSpeechLogger,
|
|
9
10
|
reducer,
|
|
10
|
-
sttLogger,
|
|
11
11
|
} from '../core';
|
|
12
|
+
|
|
13
|
+
const log = getSpeechLogger();
|
|
12
14
|
import { createWebSpeechEngine } from '../core/engine/webspeech';
|
|
13
15
|
import { useSpeechPrefs } from '../store/prefsStore';
|
|
14
16
|
import type {
|
|
@@ -46,8 +48,10 @@ export function useSpeechRecognition(
|
|
|
46
48
|
|
|
47
49
|
// Engine subscription lifecycle.
|
|
48
50
|
useEffect(() => {
|
|
51
|
+
log.engine.debug('subscribe', { engineId: engine.id });
|
|
49
52
|
const offs = [
|
|
50
53
|
engine.on('partial', (text, segmentId) => {
|
|
54
|
+
log.engine.debug('partial', { len: text.length, segmentId });
|
|
51
55
|
dispatch({ type: 'PARTIAL', text, segmentId });
|
|
52
56
|
const seg: Segment = {
|
|
53
57
|
id: segmentId,
|
|
@@ -58,6 +62,11 @@ export function useSpeechRecognition(
|
|
|
58
62
|
cbRef.current.onPartial?.(text, seg);
|
|
59
63
|
}),
|
|
60
64
|
engine.on('final', (text, segmentId, confidence) => {
|
|
65
|
+
log.engine.info('final', {
|
|
66
|
+
len: text.length,
|
|
67
|
+
segmentId,
|
|
68
|
+
confidence,
|
|
69
|
+
});
|
|
61
70
|
dispatch({ type: 'FINAL', text, segmentId, confidence });
|
|
62
71
|
const seg: Segment = {
|
|
63
72
|
id: segmentId,
|
|
@@ -70,10 +79,12 @@ export function useSpeechRecognition(
|
|
|
70
79
|
cbRef.current.onFinal?.(text, seg);
|
|
71
80
|
}),
|
|
72
81
|
engine.on('error', (err) => {
|
|
82
|
+
log.error.error('engine error', err);
|
|
73
83
|
dispatch({ type: 'ERROR', error: err });
|
|
74
84
|
cbRef.current.onError?.(err);
|
|
75
85
|
}),
|
|
76
86
|
engine.on('state', (s) => {
|
|
87
|
+
log.engine.debug('state', s);
|
|
77
88
|
if (s === 'listening') {
|
|
78
89
|
dispatch({ type: 'STARTED' });
|
|
79
90
|
cbRef.current.onStart?.();
|
|
@@ -98,7 +109,7 @@ export function useSpeechRecognition(
|
|
|
98
109
|
const { silenceMs, maxMs, silenceThreshold = 0.02 } = config.autoStop ?? {};
|
|
99
110
|
if (maxMs) {
|
|
100
111
|
maxTimer.current = window.setTimeout(() => {
|
|
101
|
-
|
|
112
|
+
log.engine.debug('autoStop max duration hit');
|
|
102
113
|
void engine.stop();
|
|
103
114
|
}, maxMs);
|
|
104
115
|
}
|
|
@@ -107,7 +118,7 @@ export function useSpeechRecognition(
|
|
|
107
118
|
if (level < silenceThreshold) {
|
|
108
119
|
if (silenceTimer.current == null) {
|
|
109
120
|
silenceTimer.current = window.setTimeout(() => {
|
|
110
|
-
|
|
121
|
+
log.engine.debug('autoStop silence detected');
|
|
111
122
|
void engine.stop();
|
|
112
123
|
}, silenceMs);
|
|
113
124
|
}
|
|
@@ -141,7 +152,7 @@ export function useSpeechRecognition(
|
|
|
141
152
|
});
|
|
142
153
|
} catch (cause) {
|
|
143
154
|
// engine already emitted 'error'; reducer caught it via subscription
|
|
144
|
-
|
|
155
|
+
log.engine.warn('start threw', cause);
|
|
145
156
|
}
|
|
146
157
|
}, [engine, language, config.interim, config.deviceId, prefs.deviceId, state.status]);
|
|
147
158
|
|
|
@@ -60,6 +60,15 @@ export * from './hooks';
|
|
|
60
60
|
export * from './components';
|
|
61
61
|
export * from './widgets';
|
|
62
62
|
export * from './context';
|
|
63
|
+
// Dev logger — namespaced, runtime opt-in via localStorage.
|
|
64
|
+
// Enable from devtools: `localStorage['djangocfg:speech-debug'] = '1'`.
|
|
65
|
+
export {
|
|
66
|
+
getSpeechLogger,
|
|
67
|
+
sttLogger,
|
|
68
|
+
type SpeechLogger,
|
|
69
|
+
type SpeechLogScope,
|
|
70
|
+
} from './core/logger';
|
|
71
|
+
|
|
63
72
|
export { LazyDictationField } from './lazy';
|
|
64
73
|
export { useSpeechPrefs } from './store';
|
|
65
74
|
export type { SpeechPrefs } from './store';
|
|
@@ -10,10 +10,13 @@ import { cn } from '@djangocfg/ui-core/lib';
|
|
|
10
10
|
import { useChatContextOptional } from '../../Chat/context';
|
|
11
11
|
import { useSpeechRecognition } from '../hooks/useSpeechRecognition';
|
|
12
12
|
import { useVoiceSupport } from '../hooks/useVoiceSupport';
|
|
13
|
+
import { getSpeechLogger } from '../core/logger';
|
|
13
14
|
import { normaliseFinal } from '../core/transcript';
|
|
14
15
|
import { DEFAULT_VOICE_SOUNDS, type VoiceSoundEvent } from '../core/audio/defaults';
|
|
15
16
|
import type { RecognitionEngine } from '../types';
|
|
16
17
|
|
|
18
|
+
const log = getSpeechLogger();
|
|
19
|
+
|
|
17
20
|
export interface VoiceComposerSlotProps {
|
|
18
21
|
/**
|
|
19
22
|
* Controlled composer value. Optional — when omitted, the slot
|
|
@@ -98,6 +101,16 @@ export function VoiceComposerSlot({
|
|
|
98
101
|
const composerHandleRef = useRef(chatCtx?.composer ?? null);
|
|
99
102
|
composerHandleRef.current = chatCtx?.composer ?? null;
|
|
100
103
|
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
log.slot.debug('mount', {
|
|
106
|
+
supported: support.supported,
|
|
107
|
+
reason: support.reason,
|
|
108
|
+
hasComposerHandle: !!chatCtx?.composer,
|
|
109
|
+
hasExplicitValue: value !== undefined,
|
|
110
|
+
hasOnChange: !!onChange,
|
|
111
|
+
});
|
|
112
|
+
}, [support.supported, support.reason, chatCtx?.composer, value, onChange]);
|
|
113
|
+
|
|
101
114
|
// Resolve value/onChange: prop wins; otherwise pull from the
|
|
102
115
|
// registered composer handle. The slot can therefore be dropped into
|
|
103
116
|
// `composerToolbarEnd` of `ChatRoot` with zero props.
|
|
@@ -108,10 +121,21 @@ export function VoiceComposerSlot({
|
|
|
108
121
|
const resolvedSetValue = useCallback(
|
|
109
122
|
(next: string): void => {
|
|
110
123
|
if (onChange) {
|
|
124
|
+
log.composer.debug('setValue → onChange prop', { len: next.length });
|
|
111
125
|
onChange(next);
|
|
112
126
|
return;
|
|
113
127
|
}
|
|
114
|
-
composerHandleRef.current
|
|
128
|
+
const handle = composerHandleRef.current;
|
|
129
|
+
if (!handle?.setValue) {
|
|
130
|
+
log.composer.warn(
|
|
131
|
+
'setValue called but no composer handle is registered — text will be lost. ' +
|
|
132
|
+
'Make sure <VoiceComposerSlot> lives inside a <ChatProvider> (or pass `value`/`onChange` props for standalone use).',
|
|
133
|
+
{ len: next.length },
|
|
134
|
+
);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
log.composer.debug('setValue → composer handle', { len: next.length });
|
|
138
|
+
handle.setValue(next);
|
|
115
139
|
},
|
|
116
140
|
[onChange],
|
|
117
141
|
);
|
|
@@ -157,6 +181,10 @@ export function VoiceComposerSlot({
|
|
|
157
181
|
|
|
158
182
|
const handlePartial = useCallback(
|
|
159
183
|
(text: string) => {
|
|
184
|
+
log.dictation.debug('partial', {
|
|
185
|
+
len: text.length,
|
|
186
|
+
anchorLen: anchorRef.current.length,
|
|
187
|
+
});
|
|
160
188
|
const next = anchorRef.current
|
|
161
189
|
? `${anchorRef.current} ${text}`
|
|
162
190
|
: text;
|
|
@@ -169,8 +197,15 @@ export function VoiceComposerSlot({
|
|
|
169
197
|
const handleFinal = useCallback(
|
|
170
198
|
(text: string) => {
|
|
171
199
|
const clean = normaliseFinal(text);
|
|
172
|
-
if (!clean)
|
|
200
|
+
if (!clean) {
|
|
201
|
+
log.dictation.debug('final dropped — empty after normalise', { raw: text });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
173
204
|
const merged = anchorRef.current ? `${anchorRef.current} ${clean}` : clean;
|
|
205
|
+
log.dictation.info('final merged', {
|
|
206
|
+
len: clean.length,
|
|
207
|
+
totalLen: merged.length,
|
|
208
|
+
});
|
|
174
209
|
anchorRef.current = merged;
|
|
175
210
|
resolvedSetValue(merged);
|
|
176
211
|
pinCaretToEnd();
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var chunkHPK3EWBF_cjs = require('./chunk-HPK3EWBF.cjs');
|
|
4
|
-
require('./chunk-FIRK5CEH.cjs');
|
|
5
|
-
require('./chunk-OLISEQHS.cjs');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Object.defineProperty(exports, "ChatRoot", {
|
|
10
|
-
enumerable: true,
|
|
11
|
-
get: function () { return chunkHPK3EWBF_cjs.ChatRoot; }
|
|
12
|
-
});
|
|
13
|
-
//# sourceMappingURL=ChatRoot-LXIUBOXF.cjs.map
|
|
14
|
-
//# sourceMappingURL=ChatRoot-LXIUBOXF.cjs.map
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var chunk4PFW7MIJ_cjs = require('./chunk-4PFW7MIJ.cjs');
|
|
4
|
-
require('./chunk-OLISEQHS.cjs');
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Object.defineProperty(exports, "DictationField", {
|
|
9
|
-
enumerable: true,
|
|
10
|
-
get: function () { return chunk4PFW7MIJ_cjs.DictationField; }
|
|
11
|
-
});
|
|
12
|
-
//# sourceMappingURL=DictationField-XWR5VOID.cjs.map
|
|
13
|
-
//# sourceMappingURL=DictationField-XWR5VOID.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tools/Map/context/MapContext.tsx","../src/tools/Map/styles/index.ts","../src/tools/Map/components/MapContainer.tsx"],"names":["createContext","useRef","useState","useCallback","useMemo","jsx","__name","useContext","useEffect","jsxs","Fragment","Map","cn","RotateCcw","ExternalLink"],"mappings":"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","file":"MapContainer-76YL2JXL.cjs","sourcesContent":["'use client'\n\nimport {\n createContext,\n useContext,\n useState,\n useRef,\n useMemo,\n useCallback,\n type ReactNode,\n} from 'react'\nimport type { MapRef } from 'react-map-gl/maplibre'\nimport type { MapContextValue, MapViewport, MarkerData } from '../types'\n\nconst MapContext = createContext<MapContextValue | null>(null)\n\nexport interface MapProviderProps {\n children: ReactNode\n initialViewport?: Partial<MapViewport>\n}\n\nconst DEFAULT_VIEWPORT: MapViewport = {\n longitude: 115.1889,\n latitude: -8.4095,\n zoom: 10,\n bearing: 0,\n pitch: 0,\n}\n\nexport function MapProvider({ children, initialViewport }: MapProviderProps) {\n const mapRef = useRef<MapRef | null>(null)\n const initialViewportRef = useRef<MapViewport>({\n ...DEFAULT_VIEWPORT,\n ...initialViewport,\n })\n const [viewport, setViewportState] = useState<MapViewport>(initialViewportRef.current)\n const [markers, setMarkers] = useState<MarkerData[]>([])\n const [selectedMarker, setSelectedMarker] = useState<MarkerData | null>(null)\n const [hoveredFeature, setHoveredFeature] = useState<GeoJSON.Feature | null>(null)\n const [isLoaded, setIsLoaded] = useState(false)\n\n const setViewport = useCallback((newViewport: Partial<MapViewport>) => {\n setViewportState((prev) => ({ ...prev, ...newViewport }))\n }, [])\n\n const resetToInitial = useCallback(() => {\n const map = mapRef.current\n if (map) {\n map.flyTo({\n center: [initialViewportRef.current.longitude, initialViewportRef.current.latitude],\n zoom: initialViewportRef.current.zoom,\n bearing: initialViewportRef.current.bearing ?? 0,\n pitch: initialViewportRef.current.pitch ?? 0,\n duration: 1000,\n })\n }\n }, [])\n\n const value = useMemo<MapContextValue>(\n () => ({\n mapRef,\n viewport,\n setViewport,\n initialViewport: initialViewportRef.current,\n resetToInitial,\n markers,\n setMarkers,\n selectedMarker,\n setSelectedMarker,\n hoveredFeature,\n setHoveredFeature,\n isLoaded,\n setIsLoaded,\n }),\n [viewport, setViewport, resetToInitial, markers, selectedMarker, hoveredFeature, isLoaded]\n )\n\n return <MapContext.Provider value={value}>{children}</MapContext.Provider>\n}\n\nexport function useMapContext(): MapContextValue {\n const context = useContext(MapContext)\n if (!context) {\n throw new Error('useMapContext must be used within a MapProvider')\n }\n return context\n}\n\nexport { MapContext }\n","export const MAP_STYLES = {\n light: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',\n dark: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',\n streets: 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json',\n satellite: 'https://api.maptiler.com/maps/satellite/style.json',\n} as const\n\nexport type MapStyleKey = keyof typeof MAP_STYLES\n\nexport function getMapStyle(key: MapStyleKey | string): string {\n if (key in MAP_STYLES) {\n return MAP_STYLES[key as MapStyleKey]\n }\n return key\n}\n","'use client'\n\nimport { useCallback, useEffect, useRef, type ReactNode } from 'react'\nimport Map, { type ViewStateChangeEvent } from 'react-map-gl/maplibre'\nimport { ExternalLink, RotateCcw } from 'lucide-react'\nimport { cn } from '@djangocfg/ui-core/lib'\nimport 'maplibre-gl/dist/maplibre-gl.css'\n\nimport { MapProvider, useMapContext } from '../context'\nimport { MAP_STYLES } from '../styles'\nimport type { MapViewport, MapStyleKey } from '../types'\n\nexport interface MapContainerProps {\n children?: ReactNode\n initialViewport?: Partial<MapViewport>\n mapStyle?: MapStyleKey | string\n interactiveLayerIds?: string[]\n className?: string\n style?: React.CSSProperties\n cursor?: string\n attributionControl?: boolean\n reuseMaps?: boolean\n /** URL to open in external maps app (shows \"Open in Maps\" button if provided) */\n openInMapsUrl?: string\n /** Label for the open in maps button */\n openInMapsLabel?: string\n /** Auto-reset to initial viewport after N ms of inactivity (0 = disabled) */\n autoResetDelay?: number\n /** Show reset button */\n showResetButton?: boolean\n}\n\ninterface MapInnerProps extends Omit<MapContainerProps, 'initialViewport'> {}\n\nfunction MapInner({\n children,\n mapStyle = 'light',\n interactiveLayerIds,\n style,\n cursor,\n attributionControl = true,\n reuseMaps = true,\n openInMapsUrl,\n openInMapsLabel = 'Open in Maps',\n autoResetDelay = 0,\n showResetButton = false,\n}: MapInnerProps) {\n const { mapRef, viewport, setViewport, setIsLoaded, resetToInitial, initialViewport } = useMapContext()\n const resetTimerRef = useRef<NodeJS.Timeout | null>(null)\n const isInteractingRef = useRef(false)\n\n // Check if viewport has changed from initial\n const hasViewportChanged =\n Math.abs(viewport.longitude - initialViewport.longitude) > 0.0001 ||\n Math.abs(viewport.latitude - initialViewport.latitude) > 0.0001 ||\n Math.abs(viewport.zoom - initialViewport.zoom) > 0.1\n\n const handleMove = useCallback(\n (evt: ViewStateChangeEvent) => {\n setViewport({\n longitude: evt.viewState.longitude,\n latitude: evt.viewState.latitude,\n zoom: evt.viewState.zoom,\n bearing: evt.viewState.bearing,\n pitch: evt.viewState.pitch,\n })\n },\n [setViewport]\n )\n\n const handleMoveStart = useCallback(() => {\n isInteractingRef.current = true\n // Clear any pending reset timer\n if (resetTimerRef.current) {\n clearTimeout(resetTimerRef.current)\n resetTimerRef.current = null\n }\n }, [])\n\n const handleMoveEnd = useCallback(() => {\n isInteractingRef.current = false\n // Start auto-reset timer if enabled\n if (autoResetDelay > 0) {\n resetTimerRef.current = setTimeout(() => {\n if (!isInteractingRef.current) {\n resetToInitial()\n }\n }, autoResetDelay)\n }\n }, [autoResetDelay, resetToInitial])\n\n const handleLoad = useCallback(() => {\n setIsLoaded(true)\n }, [setIsLoaded])\n\n // Cleanup timer on unmount\n useEffect(() => {\n return () => {\n if (resetTimerRef.current) {\n clearTimeout(resetTimerRef.current)\n }\n }\n }, [])\n\n const resolvedStyle = mapStyle in MAP_STYLES\n ? MAP_STYLES[mapStyle as MapStyleKey]\n : mapStyle\n\n return (\n <>\n <Map\n ref={mapRef}\n {...viewport}\n onMove={handleMove}\n onMoveStart={handleMoveStart}\n onMoveEnd={handleMoveEnd}\n onLoad={handleLoad}\n mapStyle={resolvedStyle}\n interactiveLayerIds={interactiveLayerIds}\n attributionControl={attributionControl ? {} : false}\n reuseMaps={reuseMaps}\n style={{\n width: '100%',\n height: '100%',\n ...style,\n }}\n cursor={cursor}\n >\n {children}\n </Map>\n\n {/* Map overlay buttons */}\n <div className=\"absolute bottom-3 right-3 flex items-center gap-2\">\n {/* Reset button */}\n {showResetButton && hasViewportChanged && (\n <button\n type=\"button\"\n onClick={resetToInitial}\n className={cn(\n 'inline-flex items-center gap-2 px-3 py-2 rounded-lg',\n 'bg-background/95 backdrop-blur-sm text-foreground text-sm font-medium',\n 'hover:bg-background transition-colors shadow-sm border border-border'\n )}\n >\n <RotateCcw className=\"w-4 h-4\" />\n Reset\n </button>\n )}\n\n {/* Open in Maps button */}\n {openInMapsUrl && (\n <a\n href={openInMapsUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className={cn(\n 'inline-flex items-center gap-2 px-4 py-2 rounded-lg',\n 'bg-background/95 backdrop-blur-sm text-foreground text-sm font-medium',\n 'hover:bg-background transition-colors shadow-sm border border-border'\n )}\n >\n <ExternalLink className=\"w-4 h-4\" />\n {openInMapsLabel}\n </a>\n )}\n </div>\n </>\n )\n}\n\nexport function MapContainer({\n children,\n initialViewport,\n className,\n ...props\n}: MapContainerProps) {\n return (\n <div className={className} style={{ width: '100%', height: '100%', position: 'relative' }}>\n <MapProvider initialViewport={initialViewport}>\n <MapInner {...props}>{children}</MapInner>\n </MapProvider>\n </div>\n )\n}\n\n/**\n * Use this when you need the map inside an existing MapProvider\n */\nexport function MapView(props: Omit<MapContainerProps, 'initialViewport'>) {\n return <MapInner {...props} />\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tools/Map/context/MapContext.tsx","../src/tools/Map/styles/index.ts","../src/tools/Map/components/MapContainer.tsx"],"names":["useRef","useCallback","jsx"],"mappings":"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","file":"MapContainer-7HXBI3OH.mjs","sourcesContent":["'use client'\n\nimport {\n createContext,\n useContext,\n useState,\n useRef,\n useMemo,\n useCallback,\n type ReactNode,\n} from 'react'\nimport type { MapRef } from 'react-map-gl/maplibre'\nimport type { MapContextValue, MapViewport, MarkerData } from '../types'\n\nconst MapContext = createContext<MapContextValue | null>(null)\n\nexport interface MapProviderProps {\n children: ReactNode\n initialViewport?: Partial<MapViewport>\n}\n\nconst DEFAULT_VIEWPORT: MapViewport = {\n longitude: 115.1889,\n latitude: -8.4095,\n zoom: 10,\n bearing: 0,\n pitch: 0,\n}\n\nexport function MapProvider({ children, initialViewport }: MapProviderProps) {\n const mapRef = useRef<MapRef | null>(null)\n const initialViewportRef = useRef<MapViewport>({\n ...DEFAULT_VIEWPORT,\n ...initialViewport,\n })\n const [viewport, setViewportState] = useState<MapViewport>(initialViewportRef.current)\n const [markers, setMarkers] = useState<MarkerData[]>([])\n const [selectedMarker, setSelectedMarker] = useState<MarkerData | null>(null)\n const [hoveredFeature, setHoveredFeature] = useState<GeoJSON.Feature | null>(null)\n const [isLoaded, setIsLoaded] = useState(false)\n\n const setViewport = useCallback((newViewport: Partial<MapViewport>) => {\n setViewportState((prev) => ({ ...prev, ...newViewport }))\n }, [])\n\n const resetToInitial = useCallback(() => {\n const map = mapRef.current\n if (map) {\n map.flyTo({\n center: [initialViewportRef.current.longitude, initialViewportRef.current.latitude],\n zoom: initialViewportRef.current.zoom,\n bearing: initialViewportRef.current.bearing ?? 0,\n pitch: initialViewportRef.current.pitch ?? 0,\n duration: 1000,\n })\n }\n }, [])\n\n const value = useMemo<MapContextValue>(\n () => ({\n mapRef,\n viewport,\n setViewport,\n initialViewport: initialViewportRef.current,\n resetToInitial,\n markers,\n setMarkers,\n selectedMarker,\n setSelectedMarker,\n hoveredFeature,\n setHoveredFeature,\n isLoaded,\n setIsLoaded,\n }),\n [viewport, setViewport, resetToInitial, markers, selectedMarker, hoveredFeature, isLoaded]\n )\n\n return <MapContext.Provider value={value}>{children}</MapContext.Provider>\n}\n\nexport function useMapContext(): MapContextValue {\n const context = useContext(MapContext)\n if (!context) {\n throw new Error('useMapContext must be used within a MapProvider')\n }\n return context\n}\n\nexport { MapContext }\n","export const MAP_STYLES = {\n light: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',\n dark: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',\n streets: 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json',\n satellite: 'https://api.maptiler.com/maps/satellite/style.json',\n} as const\n\nexport type MapStyleKey = keyof typeof MAP_STYLES\n\nexport function getMapStyle(key: MapStyleKey | string): string {\n if (key in MAP_STYLES) {\n return MAP_STYLES[key as MapStyleKey]\n }\n return key\n}\n","'use client'\n\nimport { useCallback, useEffect, useRef, type ReactNode } from 'react'\nimport Map, { type ViewStateChangeEvent } from 'react-map-gl/maplibre'\nimport { ExternalLink, RotateCcw } from 'lucide-react'\nimport { cn } from '@djangocfg/ui-core/lib'\nimport 'maplibre-gl/dist/maplibre-gl.css'\n\nimport { MapProvider, useMapContext } from '../context'\nimport { MAP_STYLES } from '../styles'\nimport type { MapViewport, MapStyleKey } from '../types'\n\nexport interface MapContainerProps {\n children?: ReactNode\n initialViewport?: Partial<MapViewport>\n mapStyle?: MapStyleKey | string\n interactiveLayerIds?: string[]\n className?: string\n style?: React.CSSProperties\n cursor?: string\n attributionControl?: boolean\n reuseMaps?: boolean\n /** URL to open in external maps app (shows \"Open in Maps\" button if provided) */\n openInMapsUrl?: string\n /** Label for the open in maps button */\n openInMapsLabel?: string\n /** Auto-reset to initial viewport after N ms of inactivity (0 = disabled) */\n autoResetDelay?: number\n /** Show reset button */\n showResetButton?: boolean\n}\n\ninterface MapInnerProps extends Omit<MapContainerProps, 'initialViewport'> {}\n\nfunction MapInner({\n children,\n mapStyle = 'light',\n interactiveLayerIds,\n style,\n cursor,\n attributionControl = true,\n reuseMaps = true,\n openInMapsUrl,\n openInMapsLabel = 'Open in Maps',\n autoResetDelay = 0,\n showResetButton = false,\n}: MapInnerProps) {\n const { mapRef, viewport, setViewport, setIsLoaded, resetToInitial, initialViewport } = useMapContext()\n const resetTimerRef = useRef<NodeJS.Timeout | null>(null)\n const isInteractingRef = useRef(false)\n\n // Check if viewport has changed from initial\n const hasViewportChanged =\n Math.abs(viewport.longitude - initialViewport.longitude) > 0.0001 ||\n Math.abs(viewport.latitude - initialViewport.latitude) > 0.0001 ||\n Math.abs(viewport.zoom - initialViewport.zoom) > 0.1\n\n const handleMove = useCallback(\n (evt: ViewStateChangeEvent) => {\n setViewport({\n longitude: evt.viewState.longitude,\n latitude: evt.viewState.latitude,\n zoom: evt.viewState.zoom,\n bearing: evt.viewState.bearing,\n pitch: evt.viewState.pitch,\n })\n },\n [setViewport]\n )\n\n const handleMoveStart = useCallback(() => {\n isInteractingRef.current = true\n // Clear any pending reset timer\n if (resetTimerRef.current) {\n clearTimeout(resetTimerRef.current)\n resetTimerRef.current = null\n }\n }, [])\n\n const handleMoveEnd = useCallback(() => {\n isInteractingRef.current = false\n // Start auto-reset timer if enabled\n if (autoResetDelay > 0) {\n resetTimerRef.current = setTimeout(() => {\n if (!isInteractingRef.current) {\n resetToInitial()\n }\n }, autoResetDelay)\n }\n }, [autoResetDelay, resetToInitial])\n\n const handleLoad = useCallback(() => {\n setIsLoaded(true)\n }, [setIsLoaded])\n\n // Cleanup timer on unmount\n useEffect(() => {\n return () => {\n if (resetTimerRef.current) {\n clearTimeout(resetTimerRef.current)\n }\n }\n }, [])\n\n const resolvedStyle = mapStyle in MAP_STYLES\n ? MAP_STYLES[mapStyle as MapStyleKey]\n : mapStyle\n\n return (\n <>\n <Map\n ref={mapRef}\n {...viewport}\n onMove={handleMove}\n onMoveStart={handleMoveStart}\n onMoveEnd={handleMoveEnd}\n onLoad={handleLoad}\n mapStyle={resolvedStyle}\n interactiveLayerIds={interactiveLayerIds}\n attributionControl={attributionControl ? {} : false}\n reuseMaps={reuseMaps}\n style={{\n width: '100%',\n height: '100%',\n ...style,\n }}\n cursor={cursor}\n >\n {children}\n </Map>\n\n {/* Map overlay buttons */}\n <div className=\"absolute bottom-3 right-3 flex items-center gap-2\">\n {/* Reset button */}\n {showResetButton && hasViewportChanged && (\n <button\n type=\"button\"\n onClick={resetToInitial}\n className={cn(\n 'inline-flex items-center gap-2 px-3 py-2 rounded-lg',\n 'bg-background/95 backdrop-blur-sm text-foreground text-sm font-medium',\n 'hover:bg-background transition-colors shadow-sm border border-border'\n )}\n >\n <RotateCcw className=\"w-4 h-4\" />\n Reset\n </button>\n )}\n\n {/* Open in Maps button */}\n {openInMapsUrl && (\n <a\n href={openInMapsUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className={cn(\n 'inline-flex items-center gap-2 px-4 py-2 rounded-lg',\n 'bg-background/95 backdrop-blur-sm text-foreground text-sm font-medium',\n 'hover:bg-background transition-colors shadow-sm border border-border'\n )}\n >\n <ExternalLink className=\"w-4 h-4\" />\n {openInMapsLabel}\n </a>\n )}\n </div>\n </>\n )\n}\n\nexport function MapContainer({\n children,\n initialViewport,\n className,\n ...props\n}: MapContainerProps) {\n return (\n <div className={className} style={{ width: '100%', height: '100%', position: 'relative' }}>\n <MapProvider initialViewport={initialViewport}>\n <MapInner {...props}>{children}</MapInner>\n </MapProvider>\n </div>\n )\n}\n\n/**\n * Use this when you need the map inside an existing MapProvider\n */\nexport function MapView(props: Omit<MapContainerProps, 'initialViewport'>) {\n return <MapInner {...props} />\n}\n"]}
|