@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.
Files changed (69) hide show
  1. package/README.md +25 -11
  2. package/dist/ChatRoot-4KM2JMGA.mjs +6 -0
  3. package/dist/{ChatRoot-JVR3M3H2.mjs.map → ChatRoot-4KM2JMGA.mjs.map} +1 -1
  4. package/dist/ChatRoot-OILWMMZ6.cjs +15 -0
  5. package/dist/{ChatRoot-LXIUBOXF.cjs.map → ChatRoot-OILWMMZ6.cjs.map} +1 -1
  6. package/dist/DictationField-AS2F33WI.cjs +13 -0
  7. package/dist/{DictationField-U25MEYAL.mjs.map → DictationField-AS2F33WI.cjs.map} +1 -1
  8. package/dist/DictationField-WPONUCYE.mjs +4 -0
  9. package/dist/{DictationField-XWR5VOID.cjs.map → DictationField-WPONUCYE.mjs.map} +1 -1
  10. package/dist/MapContainer-AKIPABJK.mjs +4 -0
  11. package/dist/MapContainer-AKIPABJK.mjs.map +1 -0
  12. package/dist/MapContainer-STVDMC36.cjs +17 -0
  13. package/dist/MapContainer-STVDMC36.cjs.map +1 -0
  14. package/dist/{MapContainer-76YL2JXL.cjs → chunk-5D2OCOPQ.cjs} +3 -2
  15. package/dist/chunk-5D2OCOPQ.cjs.map +1 -0
  16. package/dist/{MapContainer-7HXBI3OH.mjs → chunk-7CWGZPO3.mjs} +3 -3
  17. package/dist/chunk-7CWGZPO3.mjs.map +1 -0
  18. package/dist/{chunk-4PFW7MIJ.cjs → chunk-ADEN3UA4.cjs} +60 -5
  19. package/dist/chunk-ADEN3UA4.cjs.map +1 -0
  20. package/dist/chunk-BVESQTBM.mjs +1439 -0
  21. package/dist/chunk-BVESQTBM.mjs.map +1 -0
  22. package/dist/{chunk-PEKBT75W.mjs → chunk-DMX7W4XZ.mjs} +53 -1387
  23. package/dist/chunk-DMX7W4XZ.mjs.map +1 -0
  24. package/dist/chunk-HNIMIIFR.mjs +1361 -0
  25. package/dist/chunk-HNIMIIFR.mjs.map +1 -0
  26. package/dist/chunk-L25HA3TM.cjs +1478 -0
  27. package/dist/chunk-L25HA3TM.cjs.map +1 -0
  28. package/dist/{chunk-HPK3EWBF.cjs → chunk-TBSHZO5R.cjs} +50 -1409
  29. package/dist/chunk-TBSHZO5R.cjs.map +1 -0
  30. package/dist/chunk-TSNRU3UO.cjs +1387 -0
  31. package/dist/chunk-TSNRU3UO.cjs.map +1 -0
  32. package/dist/{chunk-C2YN6WEO.mjs → chunk-UNCS5V5F.mjs} +61 -7
  33. package/dist/chunk-UNCS5V5F.mjs.map +1 -0
  34. package/dist/index.cjs +1236 -1768
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +780 -780
  37. package/dist/index.d.ts +780 -780
  38. package/dist/index.mjs +853 -1423
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/launcher-5WYPDPEP.mjs +7 -0
  41. package/dist/launcher-5WYPDPEP.mjs.map +1 -0
  42. package/dist/launcher-FCI3LTDY.css +7 -0
  43. package/dist/launcher-FCI3LTDY.css.map +1 -0
  44. package/dist/launcher-QAOG2NUI.cjs +60 -0
  45. package/dist/launcher-QAOG2NUI.cjs.map +1 -0
  46. package/package.json +23 -18
  47. package/src/tools/AudioPlayer/lazy.tsx +100 -0
  48. package/src/tools/Chat/README.md +85 -1
  49. package/src/tools/Chat/context/ChatProvider.tsx +42 -0
  50. package/src/tools/Chat/lazy.tsx +213 -1
  51. package/src/tools/CodeEditor/lazy.tsx +70 -0
  52. package/src/tools/Map/lazy.tsx +38 -1
  53. package/src/tools/MarkdownEditor/lazy.tsx +42 -0
  54. package/src/tools/SpeechRecognition/README.md +48 -0
  55. package/src/tools/SpeechRecognition/core/index.ts +6 -1
  56. package/src/tools/SpeechRecognition/core/logger.ts +107 -1
  57. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +15 -4
  58. package/src/tools/SpeechRecognition/index.ts +9 -0
  59. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +37 -2
  60. package/dist/ChatRoot-JVR3M3H2.mjs +0 -5
  61. package/dist/ChatRoot-LXIUBOXF.cjs +0 -14
  62. package/dist/DictationField-U25MEYAL.mjs +0 -4
  63. package/dist/DictationField-XWR5VOID.cjs +0 -13
  64. package/dist/MapContainer-76YL2JXL.cjs.map +0 -1
  65. package/dist/MapContainer-7HXBI3OH.mjs.map +0 -1
  66. package/dist/chunk-4PFW7MIJ.cjs.map +0 -1
  67. package/dist/chunk-C2YN6WEO.mjs.map +0 -1
  68. package/dist/chunk-HPK3EWBF.cjs.map +0 -1
  69. package/dist/chunk-PEKBT75W.mjs.map +0 -1
@@ -0,0 +1,7 @@
1
+ export { ChatDock, ChatFAB, ChatGreeting, ChatHeader, ChatHeaderActionButton, ChatHeaderAudioToggle, ChatHeaderLanguageButton, ChatHeaderModeToggle, ChatHeaderResetButton, ChatLauncher, ChatUnreadPreview, useChatPresence } from './chunk-BVESQTBM.mjs';
2
+ import './chunk-UNCS5V5F.mjs';
3
+ import './chunk-DMX7W4XZ.mjs';
4
+ import './chunk-HIK6BPL7.mjs';
5
+ import './chunk-N2XQF2OL.mjs';
6
+ //# sourceMappingURL=launcher-5WYPDPEP.mjs.map
7
+ //# sourceMappingURL=launcher-5WYPDPEP.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"launcher-5WYPDPEP.mjs"}
@@ -0,0 +1,7 @@
1
+ @source "../src/**/*.{ts,tsx}";
2
+ /* src/components/FloatingToolbar/FloatingToolbar.css */
3
+ .scroll-unlocked {
4
+ box-shadow: 0 0 0 2px hsl(var(--ring));
5
+ transition: box-shadow 150ms;
6
+ }
7
+ /*# sourceMappingURL=launcher-FCI3LTDY.css.map */
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/FloatingToolbar/FloatingToolbar.css"],"sourcesContent":["/* Focus ring when scroll isolation is unlocked */\n.scroll-unlocked {\n box-shadow: 0 0 0 2px hsl(var(--ring));\n transition: box-shadow 150ms;\n}\n"],"mappings":";AACA,CAAC;AACC,cAAY,EAAE,EAAE,EAAE,IAAI,IAAI,IAAI;AAC9B,cAAY,WAAW;AACzB;","names":[]}
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ var chunkL25HA3TM_cjs = require('./chunk-L25HA3TM.cjs');
4
+ require('./chunk-ADEN3UA4.cjs');
5
+ require('./chunk-TBSHZO5R.cjs');
6
+ require('./chunk-FIRK5CEH.cjs');
7
+ require('./chunk-OLISEQHS.cjs');
8
+
9
+
10
+
11
+ Object.defineProperty(exports, "ChatDock", {
12
+ enumerable: true,
13
+ get: function () { return chunkL25HA3TM_cjs.ChatDock; }
14
+ });
15
+ Object.defineProperty(exports, "ChatFAB", {
16
+ enumerable: true,
17
+ get: function () { return chunkL25HA3TM_cjs.ChatFAB; }
18
+ });
19
+ Object.defineProperty(exports, "ChatGreeting", {
20
+ enumerable: true,
21
+ get: function () { return chunkL25HA3TM_cjs.ChatGreeting; }
22
+ });
23
+ Object.defineProperty(exports, "ChatHeader", {
24
+ enumerable: true,
25
+ get: function () { return chunkL25HA3TM_cjs.ChatHeader; }
26
+ });
27
+ Object.defineProperty(exports, "ChatHeaderActionButton", {
28
+ enumerable: true,
29
+ get: function () { return chunkL25HA3TM_cjs.ChatHeaderActionButton; }
30
+ });
31
+ Object.defineProperty(exports, "ChatHeaderAudioToggle", {
32
+ enumerable: true,
33
+ get: function () { return chunkL25HA3TM_cjs.ChatHeaderAudioToggle; }
34
+ });
35
+ Object.defineProperty(exports, "ChatHeaderLanguageButton", {
36
+ enumerable: true,
37
+ get: function () { return chunkL25HA3TM_cjs.ChatHeaderLanguageButton; }
38
+ });
39
+ Object.defineProperty(exports, "ChatHeaderModeToggle", {
40
+ enumerable: true,
41
+ get: function () { return chunkL25HA3TM_cjs.ChatHeaderModeToggle; }
42
+ });
43
+ Object.defineProperty(exports, "ChatHeaderResetButton", {
44
+ enumerable: true,
45
+ get: function () { return chunkL25HA3TM_cjs.ChatHeaderResetButton; }
46
+ });
47
+ Object.defineProperty(exports, "ChatLauncher", {
48
+ enumerable: true,
49
+ get: function () { return chunkL25HA3TM_cjs.ChatLauncher; }
50
+ });
51
+ Object.defineProperty(exports, "ChatUnreadPreview", {
52
+ enumerable: true,
53
+ get: function () { return chunkL25HA3TM_cjs.ChatUnreadPreview; }
54
+ });
55
+ Object.defineProperty(exports, "useChatPresence", {
56
+ enumerable: true,
57
+ get: function () { return chunkL25HA3TM_cjs.useChatPresence; }
58
+ });
59
+ //# sourceMappingURL=launcher-QAOG2NUI.cjs.map
60
+ //# sourceMappingURL=launcher-QAOG2NUI.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"launcher-QAOG2NUI.cjs"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-tools",
3
- "version": "2.1.384",
3
+ "version": "2.1.387",
4
4
  "description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
5
5
  "keywords": [
6
6
  "ui-tools",
@@ -65,9 +65,9 @@
65
65
  "require": "./src/tools/Tour/index.ts"
66
66
  },
67
67
  "./code-editor": {
68
- "types": "./src/tools/CodeEditor/index.ts",
69
- "import": "./src/tools/CodeEditor/index.ts",
70
- "require": "./src/tools/CodeEditor/index.ts"
68
+ "types": "./src/tools/CodeEditor/lazy.tsx",
69
+ "import": "./src/tools/CodeEditor/lazy.tsx",
70
+ "require": "./src/tools/CodeEditor/lazy.tsx"
71
71
  },
72
72
  "./tree": {
73
73
  "types": "./dist/tree/index.d.ts",
@@ -80,14 +80,14 @@
80
80
  "require": "./dist/file-icon/index.cjs"
81
81
  },
82
82
  "./audio-player": {
83
- "types": "./src/tools/AudioPlayer/index.ts",
84
- "import": "./src/tools/AudioPlayer/index.ts",
85
- "require": "./src/tools/AudioPlayer/index.ts"
83
+ "types": "./src/tools/AudioPlayer/lazy.tsx",
84
+ "import": "./src/tools/AudioPlayer/lazy.tsx",
85
+ "require": "./src/tools/AudioPlayer/lazy.tsx"
86
86
  },
87
87
  "./chat": {
88
- "types": "./src/tools/Chat/index.ts",
89
- "import": "./src/tools/Chat/index.ts",
90
- "require": "./src/tools/Chat/index.ts"
88
+ "types": "./src/tools/Chat/lazy.tsx",
89
+ "import": "./src/tools/Chat/lazy.tsx",
90
+ "require": "./src/tools/Chat/lazy.tsx"
91
91
  },
92
92
  "./cron-scheduler": {
93
93
  "types": "./src/tools/CronScheduler/lazy.tsx",
@@ -115,9 +115,14 @@
115
115
  "require": "./src/tools/LottiePlayer/lazy.tsx"
116
116
  },
117
117
  "./markdown-editor": {
118
- "types": "./src/tools/MarkdownEditor/index.ts",
119
- "import": "./src/tools/MarkdownEditor/index.ts",
120
- "require": "./src/tools/MarkdownEditor/index.ts"
118
+ "types": "./src/tools/MarkdownEditor/lazy.tsx",
119
+ "import": "./src/tools/MarkdownEditor/lazy.tsx",
120
+ "require": "./src/tools/MarkdownEditor/lazy.tsx"
121
+ },
122
+ "./markdown-message": {
123
+ "types": "./src/components/markdown/MarkdownMessage/index.ts",
124
+ "import": "./src/components/markdown/MarkdownMessage/index.ts",
125
+ "require": "./src/components/markdown/MarkdownMessage/index.ts"
121
126
  },
122
127
  "./openapi-viewer": {
123
128
  "types": "./src/tools/OpenapiViewer/lazy.tsx",
@@ -157,8 +162,8 @@
157
162
  "test:watch": "vitest"
158
163
  },
159
164
  "peerDependencies": {
160
- "@djangocfg/i18n": "^2.1.384",
161
- "@djangocfg/ui-core": "^2.1.384",
165
+ "@djangocfg/i18n": "^2.1.387",
166
+ "@djangocfg/ui-core": "^2.1.387",
162
167
  "consola": "^3.4.2",
163
168
  "lodash-es": "^4.18.1",
164
169
  "lucide-react": "^0.545.0",
@@ -212,9 +217,9 @@
212
217
  "material-file-icons": "^2.4.0"
213
218
  },
214
219
  "devDependencies": {
215
- "@djangocfg/i18n": "^2.1.384",
216
- "@djangocfg/typescript-config": "^2.1.384",
217
- "@djangocfg/ui-core": "^2.1.384",
220
+ "@djangocfg/i18n": "^2.1.387",
221
+ "@djangocfg/typescript-config": "^2.1.387",
222
+ "@djangocfg/ui-core": "^2.1.387",
218
223
  "@types/lodash-es": "^4.17.12",
219
224
  "@types/mapbox__mapbox-gl-draw": "^1.4.8",
220
225
  "@types/node": "^24.7.2",
@@ -1,8 +1,30 @@
1
1
  'use client';
2
2
 
3
+ /**
4
+ * `@djangocfg/ui-tools/audio-player` subpath entrypoint.
5
+ *
6
+ * `LazyPlayer` is the only heavy export — it dynamically imports the full
7
+ * `Player` tree (PlayerShell + Layout + Cover + Waveform + Controls + audio
8
+ * decoding helpers). Everything else listed here is light:
9
+ * - types are erased,
10
+ * - the Zustand store, context hooks, and selectors carry no UI,
11
+ * - the slot components (`Cover`, `Title`, `PlayButton`, `Waveform`, …)
12
+ * are presentational React components that read from PlayerContext —
13
+ * they only become meaningful inside a `<PlayerProvider>` (which only
14
+ * exists inside `LazyPlayer` once loaded).
15
+ *
16
+ * Consumers building a fully custom layout: render `<LazyPlayer>` once to
17
+ * get the provider, then arrange slot components inside via render-children
18
+ * pattern, or use the bare `Player` re-exported from the root barrel.
19
+ */
20
+
3
21
  import { createLazyComponent } from '../../components';
4
22
  import type { PlayerProps } from './types';
5
23
 
24
+ // ============================================================================
25
+ // Lazy heavy component
26
+ // ============================================================================
27
+
6
28
  export const LazyPlayer = createLazyComponent<PlayerProps>(
7
29
  () => import('./Player').then((mod) => ({ default: mod.Player })),
8
30
  {
@@ -14,3 +36,81 @@ export const LazyPlayer = createLazyComponent<PlayerProps>(
14
36
  ),
15
37
  },
16
38
  );
39
+
40
+ // ============================================================================
41
+ // Light surface — types, store, context, slot components, hooks
42
+ // ============================================================================
43
+
44
+ // Types
45
+ export type {
46
+ PlayerProps,
47
+ PlayerState,
48
+ PlayerStateKind,
49
+ PlayerControls,
50
+ PlayerHandle,
51
+ WaveformConfig,
52
+ WaveformMode,
53
+ ReactiveCoverMode,
54
+ PlayerVariant,
55
+ PlayerErrorReason,
56
+ } from './types';
57
+
58
+ // Context provider + selector hooks (no UI)
59
+ export {
60
+ PlayerProvider,
61
+ usePlayerAudio,
62
+ usePlayerControls,
63
+ usePlayerDuration,
64
+ usePlayerLevels,
65
+ usePlayerMeta,
66
+ usePlayerPaused,
67
+ usePlayerState,
68
+ } from './context';
69
+
70
+ // Cross-instance store (active player, preferences)
71
+ export {
72
+ setActivePlayer,
73
+ getActivePlayer,
74
+ getLastActivePlayer,
75
+ subscribeActivePlayer,
76
+ getPreferences,
77
+ setStoredVolume,
78
+ setStoredMuted,
79
+ subscribePreferences,
80
+ type PlayerPreferences,
81
+ } from './store';
82
+
83
+ // Store-backed hooks
84
+ export {
85
+ useActivePlayer,
86
+ useLastActivePlayer,
87
+ useIsActivePlayer,
88
+ } from './hooks/useActivePlayer';
89
+ export { usePlayerPreferences } from './hooks/usePlayerPreferences';
90
+
91
+ // Peak cache helpers
92
+ export { clearPeaksCache, setPeaks } from './audio';
93
+
94
+ // Slot components — presentational, read from PlayerContext. Safe to
95
+ // re-export synchronously: they don't import the heavy Player tree
96
+ // (audio decoding, layouts, shell) — only context selectors and types.
97
+ export { Cover, CoverPlaceholder, ReactivePulse } from './parts/Cover';
98
+ export { Title, Artist, TimeDisplay } from './parts/Meta';
99
+ export {
100
+ PlayButton,
101
+ SkipButton,
102
+ VolumeControl,
103
+ LoopButton,
104
+ ControlsRow,
105
+ IconButton,
106
+ } from './parts/Controls';
107
+ export {
108
+ Waveform,
109
+ PeaksWaveform,
110
+ LiveWaveform,
111
+ BarsWaveform,
112
+ ProgressBar,
113
+ WaveformSkeleton,
114
+ } from './parts/Waveform';
115
+ export { ErrorState } from './parts/ErrorState';
116
+ export { DefaultLayout, CompactLayout } from './parts/Layout';
@@ -165,6 +165,67 @@ const prefs = useChatDockPrefs({ storageKey: 'crm.chat.dock' });
165
165
  >{children}</ChatLauncher>
166
166
  ```
167
167
 
168
+ #### Side-mode body reserve + the `--chat-dock-reserve` token
169
+
170
+ When the dock is in `mode='side'` it pushes the rest of the page out of its way by writing two things to `<body>`:
171
+
172
+ | Surface | Value | Set by |
173
+ |---|---|---|
174
+ | `body.style.paddingRight` (or `paddingLeft`) | dock width in `px` | `<ChatDock>` mount effect |
175
+ | `--chat-dock-reserve` (CSS custom property) | same `px` value, as a token | same effect |
176
+
177
+ That covers two layout flows automatically:
178
+
179
+ - **Static content inside `<body>`** — `paddingRight` shifts it inward so the chat doesn't cover the right edge.
180
+ - **CSS-only consumers** — read the token from anywhere:
181
+ ```css
182
+ .my-floating-thing {
183
+ padding-right: var(--chat-dock-reserve, 0px);
184
+ }
185
+ ```
186
+
187
+ **`position: fixed` consumers (navbars, FABs, sticky banners) are NOT auto-shifted** — body padding doesn't propagate to fixed positioning. They have to opt in by reading the token themselves. Example for a fixed top navbar:
188
+
189
+ ```tsx
190
+ 'use client';
191
+
192
+ import { useEffect, useState } from 'react';
193
+
194
+ /** Mirrors `<body>` padding onto a fixed element so it leaves room for the docked chat. */
195
+ function useChatDockReserve() {
196
+ const [reserve, setReserve] = useState({ left: 0, right: 0 });
197
+ useEffect(() => {
198
+ const read = () => {
199
+ const cs = getComputedStyle(document.body);
200
+ setReserve({
201
+ left: parseFloat(cs.paddingLeft) || 0,
202
+ right: parseFloat(cs.paddingRight) || 0,
203
+ });
204
+ };
205
+ read();
206
+ const mo = new MutationObserver(read);
207
+ mo.observe(document.body, { attributes: true, attributeFilter: ['style'] });
208
+ window.addEventListener('resize', read);
209
+ return () => { mo.disconnect(); window.removeEventListener('resize', read); };
210
+ }, []);
211
+ return reserve;
212
+ }
213
+
214
+ export function MyNavbar() {
215
+ const reserve = useChatDockReserve();
216
+ return (
217
+ <header
218
+ className="fixed top-0 z-50 transition-all"
219
+ style={{ left: reserve.left, right: reserve.right }}
220
+ >
221
+
222
+ </header>
223
+ );
224
+ }
225
+ ```
226
+
227
+ Same recipe works for any other fixed element (cookie banner, FAB, sticky toolbar). When the dock is in `popover` mode (or closed) both values are `0` and the navbar spans the full viewport.
228
+
168
229
  ### Greeting
169
230
 
170
231
  ```tsx
@@ -256,7 +317,7 @@ const audio = useChatAudio();
256
317
  </ChatLauncher>
257
318
  ```
258
319
 
259
- **Built-in sounds.** The chat tool ships its own notification pack (`messageSent`, `messageReceived`, `streamStart`, `error`, `mention`, `notification`) inlined into the lazy chunk via tsup's `dataurl` loader. ~136KB total, ~180KB after base64 — paid only by hosts that actually import Chat. No assets to copy, no CDN to host.
320
+ **Built-in sounds.** The chat tool ships its own notification pack (`messageSent`, `messageReceived`, `streamStart`, `error`, `mention`, `notification`) inlined as `data:audio/mpeg;base64,…` URLs inside `core/audio/sounds/*.ts` modules. ~136KB total — paid only by hosts that actually import Chat. No `.mp3` loader, no `*.d.ts` shim, no CDN to host. Re-encode source mp3s via ffmpeg + `base64 -i …` and paste back into the matching `.ts` file (see `core/audio/defaults.ts` header for the recipe).
260
321
 
261
322
  Customize:
262
323
 
@@ -318,6 +379,29 @@ Calls `window.dialog.confirm` (destructive variant) from `@djangocfg/ui-core/lib
318
379
  | Reusing `dismissStorageKey` across products | Greeting won't show on the second product |
319
380
  | Calling `useChatUnread()` outside `ChatProvider` | Hook reads the messages store; throws without provider |
320
381
  | Forgetting `<DialogProvider>` for `<ChatHeaderResetButton confirm>` | Falls back to native browser confirm (ugly) |
382
+ | Mixing **root barrel** and **subpath** imports | Loads the package twice → two React contexts → `useChatContextOptional()` returns `null` → VoiceComposerSlot silently drops transcripts. See **Import discipline** below. |
383
+
384
+ #### Import discipline
385
+
386
+ `@djangocfg/ui-tools` ships its root export (`.`) through a compiled bundle in `dist/`, while most subpaths (`./chat`, `./speech-recognition`, `./audio-player`, …) resolve to raw `src/`. JavaScript bundlers treat them as **two separate modules**, so each gets its own `React.createContext(...)` call.
387
+
388
+ ```tsx
389
+ // ❌ DON'T: ChatRoot from `dist`, VoiceComposerSlot from `src` → two contexts
390
+ import { ChatRoot } from '@djangocfg/ui-tools';
391
+ import { VoiceComposerSlot } from '@djangocfg/ui-tools/speech-recognition';
392
+ // ^^^^^^^^^^^^^^^^^^^
393
+ // VoiceComposerSlot's internal useChatContextOptional() reads a DIFFERENT
394
+ // React context than the one ChatProvider populated. ctx.composer is null,
395
+ // every transcript gets a "no composer handle registered" warning.
396
+
397
+ // ✅ DO: pull every Chat-side surface from the same `./chat` subpath
398
+ import { ChatRoot, ChatLauncher, useChatContextOptional } from '@djangocfg/ui-tools/chat';
399
+ import { VoiceComposerSlot } from '@djangocfg/ui-tools/speech-recognition';
400
+ ```
401
+
402
+ The rule of thumb: **anything that talks to `ChatProvider` belongs on the `./chat` subpath**. The root barrel is for tooling glue (types, transports, generic helpers) and is safe to mix with subpaths as long as you don't import anything that uses the Chat React context from it.
403
+
404
+ If two ChatProvider context instances ever end up in the same page, `ChatProvider` emits a one-time `console.warn` in development with the same fix instructions — that's the runtime guard, look for `[@djangocfg/ui-tools/chat] Two ChatProvider context instances detected …` in DevTools.
321
405
 
322
406
  ## Styling — role-aware tokens
323
407
 
@@ -64,6 +64,48 @@ export interface ChatContextValue extends UseChatReturn {
64
64
 
65
65
  const Ctx = createContext<ChatContextValue | null>(null);
66
66
 
67
+ // ─── Dual-bundle guard ────────────────────────────────────────────────────
68
+ //
69
+ // `@djangocfg/ui-tools` ships its root export through `dist/` (compiled) but
70
+ // most subpaths resolve to raw `src/`. If a consumer mixes them — e.g.
71
+ // `ChatRoot` from the root barrel and `VoiceComposerSlot` from
72
+ // `@djangocfg/ui-tools/speech-recognition` — `createContext(...)` runs twice
73
+ // and produces two distinct context instances. The Composer registers its
74
+ // handle on one; `useChatContextOptional()` in the voice slot reads from the
75
+ // other, sees `null`, and silently drops every transcript.
76
+ //
77
+ // We tag the global with our module identity. When more than one tag is
78
+ // observed the consumer is told exactly what to do.
79
+ type GuardSlot = { stamps: Set<symbol>; warned: boolean };
80
+ const GLOBAL_KEY = '__djangocfg_chat_ctx_stamps__';
81
+ const stamp = Symbol('djangocfg.chat.ctx');
82
+ function markCtxLoad(): void {
83
+ if (typeof globalThis === 'undefined') return;
84
+ const g = globalThis as unknown as Record<string, GuardSlot | undefined>;
85
+ let slot = g[GLOBAL_KEY];
86
+ if (!slot) {
87
+ slot = { stamps: new Set(), warned: false };
88
+ g[GLOBAL_KEY] = slot;
89
+ }
90
+ slot.stamps.add(stamp);
91
+ if (slot.stamps.size > 1 && !slot.warned && process.env.NODE_ENV !== 'production') {
92
+ slot.warned = true;
93
+ // eslint-disable-next-line no-console
94
+ console.warn(
95
+ '[@djangocfg/ui-tools/chat] Two ChatProvider context instances detected — ' +
96
+ 'this means `@djangocfg/ui-tools` was loaded twice (one bundle via the root ' +
97
+ '`.` export → `dist/`, another via a `./chat` / `./speech-recognition` subpath → `src/`). ' +
98
+ 'Symptom: `useChatContextOptional()` returns `null` for descendants of `<ChatProvider>`, ' +
99
+ 'so VoiceComposerSlot drops transcripts, useChatReset silently no-ops, etc. ' +
100
+ '\n\nFix: import every Chat surface from the SAME subpath. Recommended:\n' +
101
+ " import { ChatRoot, ChatLauncher, useChatContextOptional, … } from '@djangocfg/ui-tools/chat';\n" +
102
+ " import { VoiceComposerSlot } from '@djangocfg/ui-tools/speech-recognition';\n" +
103
+ '\n(See packages/ui-tools/src/tools/Chat/README.md → "Anti-patterns".)',
104
+ );
105
+ }
106
+ }
107
+ markCtxLoad();
108
+
67
109
  export interface ChatProviderProps {
68
110
  transport: ChatTransport;
69
111
  config?: ChatConfig;
@@ -1,7 +1,36 @@
1
1
  'use client';
2
2
 
3
+ /**
4
+ * `@djangocfg/ui-tools/chat` subpath entrypoint.
5
+ *
6
+ * This file is the public surface consumers see when they
7
+ * `import … from '@djangocfg/ui-tools/chat'`. It is designed so that the
8
+ * heavy chat UI (~hundreds of KB across MessageList, Composer, ToolCalls,
9
+ * Attachments, MarkdownMessage transitively) loads only when one of the
10
+ * `Lazy*` wrappers actually mounts.
11
+ *
12
+ * Rules of thumb:
13
+ * - **Heavy** (ChatRoot, ChatLauncher, ChatFAB, ChatDock, MessageList,
14
+ * MessageBubble, Composer, ToolCalls, Attachments*, ChatHeader*, …) —
15
+ * loaded only via `Lazy*` wrappers below. Do NOT add synchronous
16
+ * re-exports for these from this file.
17
+ * - **Light** (types, config constants, pure core reducer/utils,
18
+ * transports, hooks without UI, audio prefs, draft sanitizer) —
19
+ * re-exported synchronously here. Types are erased at compile time;
20
+ * helpers and hooks pull in no UI components.
21
+ *
22
+ * Consumers that need synchronous access to the heavy components
23
+ * (custom chat layouts, Storybook stories) can import from the root
24
+ * barrel `@djangocfg/ui-tools` instead.
25
+ */
26
+
3
27
  import { createLazyComponent, LoadingFallback } from '../../components/lazy-wrapper';
4
28
  import type { ChatRootProps } from './components/ChatRoot';
29
+ import type { ChatLauncherProps } from './launcher';
30
+
31
+ // ============================================================================
32
+ // Lazy UI components
33
+ // ============================================================================
5
34
 
6
35
  export const LazyChat = createLazyComponent<ChatRootProps>(
7
36
  () => import('./components/ChatRoot').then((m) => ({ default: m.ChatRoot })),
@@ -11,4 +40,187 @@ export const LazyChat = createLazyComponent<ChatRootProps>(
11
40
  },
12
41
  );
13
42
 
14
- export type { ChatRootProps };
43
+ export const LazyChatLauncher = createLazyComponent<ChatLauncherProps>(
44
+ () => import('./launcher').then((m) => ({ default: m.ChatLauncher })),
45
+ {
46
+ displayName: 'LazyChatLauncher',
47
+ // Launcher renders a floating FAB by default — no inline placeholder.
48
+ fallback: null,
49
+ },
50
+ );
51
+
52
+ // ============================================================================
53
+ // Light surface re-exports — types + pure helpers + hooks without UI
54
+ // ============================================================================
55
+
56
+ // Types (erased at compile time, free to re-export)
57
+ export type {
58
+ ChatRole,
59
+ ChatMessage,
60
+ ChatPersona,
61
+ ChatToolCall,
62
+ ChatAttachment,
63
+ ChatSource,
64
+ ChatDisplayMode,
65
+ ChatUserContext,
66
+ ChatAssistantContext,
67
+ ChatPrefs,
68
+ ChatConfig,
69
+ ChatLabels,
70
+ ChatTransport,
71
+ ChatStreamEvent,
72
+ CreateSessionOptions,
73
+ SessionInfo,
74
+ HistoryPage,
75
+ StreamOptions,
76
+ SendOptions,
77
+ } from './types';
78
+ export { DEFAULT_LABELS } from './types';
79
+
80
+ // Config — plain constants, no UI imports
81
+ export {
82
+ STORAGE_KEYS,
83
+ CSS_VARS,
84
+ DEFAULT_Z_INDEX,
85
+ LIMITS,
86
+ DEFAULT_SIDEBAR,
87
+ HOTKEYS,
88
+ CHAT_EVENT_NAME,
89
+ type ChatEventDetail,
90
+ } from './config';
91
+
92
+ // Core — pure reducer / id / token buffer / persona / initials
93
+ export {
94
+ reducer,
95
+ initialState,
96
+ createId,
97
+ createTokenBuffer,
98
+ resolvePersona,
99
+ deriveInitials,
100
+ type ChatState,
101
+ type ChatAction,
102
+ type TokenBuffer,
103
+ } from './core';
104
+
105
+ // Transports — pure functions, no UI
106
+ export {
107
+ createHttpTransport,
108
+ createMockTransport,
109
+ parseSSE,
110
+ TransportError,
111
+ createPydanticAIChatTransport,
112
+ createToolIdQueue,
113
+ mapPydanticAIEvent,
114
+ createPydanticAISSEMap,
115
+ type HttpTransportConfig,
116
+ type MockTransportOptions,
117
+ type ParseSSEOptions,
118
+ type PydanticAIChatTransportOpts,
119
+ type PydanticAIEvent,
120
+ type ToolIdQueue,
121
+ } from './core/transport';
122
+
123
+ // Hooks — no JSX, no UI
124
+ export {
125
+ useChat,
126
+ useChatComposer,
127
+ useChatScroll,
128
+ useChatHistory,
129
+ useChatLayout,
130
+ useChatAudio,
131
+ useAutoFocusOnStreamEnd,
132
+ useRegisterComposer,
133
+ useChatReset,
134
+ useVisitorFingerprint,
135
+ useChatDockPrefs,
136
+ DEFAULT_DOCK_PREFS,
137
+ useFocusOnEmptyClick,
138
+ useChatUnread,
139
+ useChatLightbox,
140
+ type UseChatUnreadOptions,
141
+ type UseChatUnreadReturn,
142
+ type UseChatConfig,
143
+ type UseChatReturn,
144
+ type UseChatComposerOptions,
145
+ type UseChatComposerReturn,
146
+ type UseChatScrollOptions,
147
+ type UseChatScrollReturn,
148
+ type UseChatHistoryOptions,
149
+ type UseChatLayoutConfig,
150
+ type UseChatLayoutReturn,
151
+ type UseAutoFocusOnStreamEndOptions,
152
+ type Focusable,
153
+ type UseChatResetOptions,
154
+ type UseChatResetReturn,
155
+ type UseVisitorFingerprintOptions,
156
+ type ChatDockPrefs,
157
+ type UseChatDockPrefsOptions,
158
+ type UseChatDockPrefsReturn,
159
+ type UseFocusOnEmptyClickOptions,
160
+ type UseChatLightboxReturn,
161
+ type ChatLightboxState,
162
+ } from './hooks';
163
+
164
+ // Audio
165
+ export {
166
+ useChatAudioPrefs,
167
+ DEFAULT_CHAT_SOUNDS,
168
+ type ChatAudioEvent,
169
+ type ChatAudioSounds,
170
+ type ChatAudioConfig,
171
+ type UseChatAudioReturn,
172
+ } from './core/audio';
173
+
174
+ // Tool-call payload dispatcher — pure
175
+ export {
176
+ dispatchToolPayload,
177
+ isPlainObject,
178
+ isLatLng,
179
+ isGeoJSONFeatureCollection,
180
+ isStringValue,
181
+ type ToolPayloadMatcher,
182
+ type ToolPayloadFallback,
183
+ } from './core/payload-dispatch';
184
+
185
+ // Logger — pure
186
+ export {
187
+ getChatLogger,
188
+ type ChatLogger,
189
+ type ChatLogScope,
190
+ } from './core/logger';
191
+
192
+ // Utils — pure
193
+ export { sanitizeDraft, isSubmittableDraft } from './utils/sanitizeDraft';
194
+ export { collectImageAttachments } from './utils/collectImageAttachments';
195
+
196
+ // Style tokens — strings + hooks, no UI components
197
+ export {
198
+ BUBBLE_SURFACE,
199
+ ANCHOR,
200
+ TOGGLE,
201
+ DESTRUCTIVE_SURFACE,
202
+ TOOL_CALL,
203
+ useChatBubbleStyles,
204
+ useChatRoleStyles,
205
+ useChatDestructiveStyles,
206
+ type ChatBubbleSurface,
207
+ type ChatBubbleStyles,
208
+ type ChatRoleStyles,
209
+ type ChatDestructiveStyles,
210
+ } from './styles';
211
+
212
+ // Provider / context — needed by callers wiring custom chat shells
213
+ export {
214
+ ChatProvider,
215
+ useChatContext,
216
+ useChatContextOptional,
217
+ type ChatContextValue,
218
+ type ChatProviderProps,
219
+ } from './context';
220
+
221
+ // Heavy-component prop types (consumers building custom layouts still need
222
+ // these for typing). The actual components are NOT re-exported.
223
+ export type {
224
+ ChatRootProps,
225
+ ChatLauncherProps,
226
+ };