@djangocfg/ui-tools 2.1.385 → 2.1.389

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 (80) hide show
  1. package/README.md +25 -11
  2. package/dist/ChatRoot-EFNXQXXN.cjs +15 -0
  3. package/dist/{ChatRoot-JVR3M3H2.mjs.map → ChatRoot-EFNXQXXN.cjs.map} +1 -1
  4. package/dist/ChatRoot-FITF5RVP.mjs +6 -0
  5. package/dist/{ChatRoot-LXIUBOXF.cjs.map → ChatRoot-FITF5RVP.mjs.map} +1 -1
  6. package/dist/{DocsLayout-2P3ONDWJ.mjs → DocsLayout-EKASBSP7.mjs} +3 -3
  7. package/dist/{DocsLayout-2P3ONDWJ.mjs.map → DocsLayout-EKASBSP7.mjs.map} +1 -1
  8. package/dist/{DocsLayout-2YZNS5VK.cjs → DocsLayout-OURFYWQE.cjs} +8 -8
  9. package/dist/{DocsLayout-2YZNS5VK.cjs.map → DocsLayout-OURFYWQE.cjs.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/{chunk-HIK6BPL7.mjs → chunk-2NG4SXEP.mjs} +6 -5
  15. package/dist/chunk-2NG4SXEP.mjs.map +1 -0
  16. package/dist/chunk-4LFB7I5K.cjs +1387 -0
  17. package/dist/chunk-4LFB7I5K.cjs.map +1 -0
  18. package/dist/{MapContainer-76YL2JXL.cjs → chunk-5D2OCOPQ.cjs} +3 -2
  19. package/dist/chunk-5D2OCOPQ.cjs.map +1 -0
  20. package/dist/chunk-6ZX2G25W.mjs +1361 -0
  21. package/dist/chunk-6ZX2G25W.mjs.map +1 -0
  22. package/dist/{MapContainer-7HXBI3OH.mjs → chunk-7CWGZPO3.mjs} +3 -3
  23. package/dist/chunk-7CWGZPO3.mjs.map +1 -0
  24. package/dist/{chunk-FIRK5CEH.cjs → chunk-7IYXZUJO.cjs} +8 -4
  25. package/dist/chunk-7IYXZUJO.cjs.map +1 -0
  26. package/dist/{chunk-PEKBT75W.mjs → chunk-DMX7W4XZ.mjs} +53 -1387
  27. package/dist/chunk-DMX7W4XZ.mjs.map +1 -0
  28. package/dist/chunk-NTVBIIUD.mjs +1439 -0
  29. package/dist/chunk-NTVBIIUD.mjs.map +1 -0
  30. package/dist/{chunk-HPK3EWBF.cjs → chunk-TBSHZO5R.cjs} +50 -1409
  31. package/dist/chunk-TBSHZO5R.cjs.map +1 -0
  32. package/dist/chunk-W75B7Y6C.cjs +1478 -0
  33. package/dist/chunk-W75B7Y6C.cjs.map +1 -0
  34. package/dist/index.cjs +1269 -1790
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +660 -623
  37. package/dist/index.d.ts +660 -623
  38. package/dist/index.mjs +856 -1427
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/launcher-5Y42OBSN.mjs +6 -0
  41. package/dist/launcher-5Y42OBSN.mjs.map +1 -0
  42. package/dist/launcher-PMW2YB24.cjs +59 -0
  43. package/dist/launcher-PMW2YB24.cjs.map +1 -0
  44. package/package.json +23 -18
  45. package/src/components/index.ts +2 -2
  46. package/src/index.ts +20 -2
  47. package/src/tools/AudioPlayer/lazy.tsx +100 -0
  48. package/src/tools/Chat/README.md +85 -1
  49. package/src/tools/Chat/components/MessageBubble.tsx +1 -1
  50. package/src/tools/Chat/context/ChatProvider.tsx +42 -0
  51. package/src/tools/Chat/index.ts +1 -1
  52. package/src/tools/Chat/lazy.tsx +300 -1
  53. package/src/tools/CodeEditor/lazy.tsx +70 -0
  54. package/src/tools/Map/lazy.tsx +38 -1
  55. package/src/tools/MarkdownEditor/lazy.tsx +42 -0
  56. package/src/{components/markdown → tools}/MarkdownMessage/CodeBlock.tsx +1 -1
  57. package/src/{components/markdown → tools}/MarkdownMessage/CollapseToggle.tsx +1 -1
  58. package/src/{components/markdown → tools}/MarkdownMessage/MarkdownMessage.tsx +1 -1
  59. package/src/{components/markdown → tools}/MarkdownMessage/components.tsx +2 -2
  60. package/src/tools/OpenapiViewer/components/DocsLayout/ApiIntroSection.tsx +1 -1
  61. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/index.tsx +1 -1
  62. package/src/tools/SpeechRecognition/README.md +48 -0
  63. package/dist/ChatRoot-JVR3M3H2.mjs +0 -5
  64. package/dist/ChatRoot-LXIUBOXF.cjs +0 -14
  65. package/dist/MapContainer-76YL2JXL.cjs.map +0 -1
  66. package/dist/MapContainer-7HXBI3OH.mjs.map +0 -1
  67. package/dist/chunk-FIRK5CEH.cjs.map +0 -1
  68. package/dist/chunk-HIK6BPL7.mjs.map +0 -1
  69. package/dist/chunk-HPK3EWBF.cjs.map +0 -1
  70. package/dist/chunk-PEKBT75W.mjs.map +0 -1
  71. package/src/components/markdown/index.ts +0 -19
  72. /package/src/{components/markdown → hooks}/useCollapsibleContent.ts +0 -0
  73. /package/src/{components/markdown → tools}/MarkdownMessage/ActionRow.tsx +0 -0
  74. /package/src/{components/markdown → tools}/MarkdownMessage/ChatMessageRow.tsx +0 -0
  75. /package/src/{components/markdown → tools}/MarkdownMessage/README.md +0 -0
  76. /package/src/{components/markdown → tools}/MarkdownMessage/index.ts +0 -0
  77. /package/src/{components/markdown → tools}/MarkdownMessage/linkRules.ts +0 -0
  78. /package/src/{components/markdown → tools}/MarkdownMessage/plainText.ts +0 -0
  79. /package/src/{components/markdown → tools}/MarkdownMessage/sanitize.ts +0 -0
  80. /package/src/{components/markdown → tools}/MarkdownMessage/types.ts +0 -0
@@ -0,0 +1,6 @@
1
+ export { ChatDock, ChatFAB, ChatGreeting, ChatHeader, ChatHeaderActionButton, ChatHeaderAudioToggle, ChatHeaderLanguageButton, ChatHeaderModeToggle, ChatHeaderResetButton, ChatLauncher, ChatUnreadPreview, useChatPresence } from './chunk-NTVBIIUD.mjs';
2
+ import './chunk-DMX7W4XZ.mjs';
3
+ import './chunk-UNCS5V5F.mjs';
4
+ import './chunk-N2XQF2OL.mjs';
5
+ //# sourceMappingURL=launcher-5Y42OBSN.mjs.map
6
+ //# sourceMappingURL=launcher-5Y42OBSN.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"launcher-5Y42OBSN.mjs"}
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ var chunkW75B7Y6C_cjs = require('./chunk-W75B7Y6C.cjs');
4
+ require('./chunk-TBSHZO5R.cjs');
5
+ require('./chunk-ADEN3UA4.cjs');
6
+ require('./chunk-OLISEQHS.cjs');
7
+
8
+
9
+
10
+ Object.defineProperty(exports, "ChatDock", {
11
+ enumerable: true,
12
+ get: function () { return chunkW75B7Y6C_cjs.ChatDock; }
13
+ });
14
+ Object.defineProperty(exports, "ChatFAB", {
15
+ enumerable: true,
16
+ get: function () { return chunkW75B7Y6C_cjs.ChatFAB; }
17
+ });
18
+ Object.defineProperty(exports, "ChatGreeting", {
19
+ enumerable: true,
20
+ get: function () { return chunkW75B7Y6C_cjs.ChatGreeting; }
21
+ });
22
+ Object.defineProperty(exports, "ChatHeader", {
23
+ enumerable: true,
24
+ get: function () { return chunkW75B7Y6C_cjs.ChatHeader; }
25
+ });
26
+ Object.defineProperty(exports, "ChatHeaderActionButton", {
27
+ enumerable: true,
28
+ get: function () { return chunkW75B7Y6C_cjs.ChatHeaderActionButton; }
29
+ });
30
+ Object.defineProperty(exports, "ChatHeaderAudioToggle", {
31
+ enumerable: true,
32
+ get: function () { return chunkW75B7Y6C_cjs.ChatHeaderAudioToggle; }
33
+ });
34
+ Object.defineProperty(exports, "ChatHeaderLanguageButton", {
35
+ enumerable: true,
36
+ get: function () { return chunkW75B7Y6C_cjs.ChatHeaderLanguageButton; }
37
+ });
38
+ Object.defineProperty(exports, "ChatHeaderModeToggle", {
39
+ enumerable: true,
40
+ get: function () { return chunkW75B7Y6C_cjs.ChatHeaderModeToggle; }
41
+ });
42
+ Object.defineProperty(exports, "ChatHeaderResetButton", {
43
+ enumerable: true,
44
+ get: function () { return chunkW75B7Y6C_cjs.ChatHeaderResetButton; }
45
+ });
46
+ Object.defineProperty(exports, "ChatLauncher", {
47
+ enumerable: true,
48
+ get: function () { return chunkW75B7Y6C_cjs.ChatLauncher; }
49
+ });
50
+ Object.defineProperty(exports, "ChatUnreadPreview", {
51
+ enumerable: true,
52
+ get: function () { return chunkW75B7Y6C_cjs.ChatUnreadPreview; }
53
+ });
54
+ Object.defineProperty(exports, "useChatPresence", {
55
+ enumerable: true,
56
+ get: function () { return chunkW75B7Y6C_cjs.useChatPresence; }
57
+ });
58
+ //# sourceMappingURL=launcher-PMW2YB24.cjs.map
59
+ //# sourceMappingURL=launcher-PMW2YB24.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"launcher-PMW2YB24.cjs"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-tools",
3
- "version": "2.1.385",
3
+ "version": "2.1.389",
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.385",
161
- "@djangocfg/ui-core": "^2.1.385",
165
+ "@djangocfg/i18n": "^2.1.389",
166
+ "@djangocfg/ui-core": "^2.1.389",
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.385",
216
- "@djangocfg/typescript-config": "^2.1.385",
217
- "@djangocfg/ui-core": "^2.1.385",
220
+ "@djangocfg/i18n": "^2.1.389",
221
+ "@djangocfg/typescript-config": "^2.1.389",
222
+ "@djangocfg/ui-core": "^2.1.389",
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",
@@ -13,5 +13,5 @@ export type {
13
13
  CreateLazyComponentOptions,
14
14
  } from './lazy-wrapper';
15
15
 
16
- // Markdown
17
- export * from './markdown';
16
+ // Markdown moved to src/tools/MarkdownMessage (re-exported from the root barrel).
17
+ // useCollapsibleContent moved to src/hooks/.
package/src/index.ts CHANGED
@@ -127,10 +127,28 @@ export * from './tools/Chat';
127
127
  export * from './tools';
128
128
 
129
129
  // ============================================================================
130
- // Components (Markdown, etc.)
130
+ // Markdown renderer (sits alongside MarkdownEditor under tools/)
131
131
  // ============================================================================
132
132
 
133
- export * from './components/markdown';
133
+ export {
134
+ MarkdownMessage,
135
+ default as MarkdownMessageDefault,
136
+ ChatMessageRow,
137
+ ActionRow,
138
+ extractTextFromChildren,
139
+ type MarkdownMessageProps,
140
+ type LinkRule,
141
+ } from './tools/MarkdownMessage';
142
+
143
+ // ============================================================================
144
+ // Shared hooks
145
+ // ============================================================================
146
+
147
+ export {
148
+ useCollapsibleContent,
149
+ type UseCollapsibleContentOptions,
150
+ type UseCollapsibleContentResult,
151
+ } from './hooks/useCollapsibleContent';
134
152
 
135
153
  // ============================================================================
136
154
  // Stores
@@ -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
 
@@ -5,7 +5,7 @@ import { memo, type ReactNode } from 'react';
5
5
  import { Avatar, AvatarFallback, AvatarImage } from '@djangocfg/ui-core/components';
6
6
  import { cn } from '@djangocfg/ui-core/lib';
7
7
 
8
- import { MarkdownMessage } from '../../../components/markdown';
8
+ import { MarkdownMessage } from '../../MarkdownMessage';
9
9
  import type {
10
10
  ChatAssistantContext,
11
11
  ChatAttachment,
@@ -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;
@@ -253,4 +253,4 @@ export {
253
253
  extractTextFromChildren,
254
254
  type MarkdownMessageProps,
255
255
  type LinkRule,
256
- } from '../../components/markdown';
256
+ } from '../MarkdownMessage';