@djangocfg/ui-tools 2.1.381 → 2.1.383

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 (178) hide show
  1. package/README.md +132 -899
  2. package/dist/ChatRoot-6IZFM5HM.mjs +5 -0
  3. package/dist/{ChatRoot-EJC5Y2YM.cjs.map → ChatRoot-6IZFM5HM.mjs.map} +1 -1
  4. package/dist/ChatRoot-LW4XNIKP.cjs +14 -0
  5. package/dist/{ChatRoot-QOSKJPM6.mjs.map → ChatRoot-LW4XNIKP.cjs.map} +1 -1
  6. package/dist/DictationField-U25MEYAL.mjs +4 -0
  7. package/dist/DictationField-U25MEYAL.mjs.map +1 -0
  8. package/dist/DictationField-XWR5VOID.cjs +13 -0
  9. package/dist/DictationField-XWR5VOID.cjs.map +1 -0
  10. package/dist/{DocsLayout-2YKPXZYO.mjs → DocsLayout-2P3ONDWJ.mjs} +3 -3
  11. package/dist/{DocsLayout-2YKPXZYO.mjs.map → DocsLayout-2P3ONDWJ.mjs.map} +1 -1
  12. package/dist/{DocsLayout-Q4KS3QWW.cjs → DocsLayout-2YZNS5VK.cjs} +8 -8
  13. package/dist/{DocsLayout-Q4KS3QWW.cjs.map → DocsLayout-2YZNS5VK.cjs.map} +1 -1
  14. package/dist/chunk-4PFW7MIJ.cjs +837 -0
  15. package/dist/chunk-4PFW7MIJ.cjs.map +1 -0
  16. package/dist/chunk-C2YN6WEO.mjs +833 -0
  17. package/dist/chunk-C2YN6WEO.mjs.map +1 -0
  18. package/dist/{chunk-XACCHZH2.cjs → chunk-FIRK5CEH.cjs} +42 -4
  19. package/dist/chunk-FIRK5CEH.cjs.map +1 -0
  20. package/dist/{chunk-NWUT327A.mjs → chunk-HIK6BPL7.mjs} +38 -5
  21. package/dist/chunk-HIK6BPL7.mjs.map +1 -0
  22. package/dist/chunk-OZAU3QWD.cjs +2493 -0
  23. package/dist/chunk-OZAU3QWD.cjs.map +1 -0
  24. package/dist/chunk-UWVP6LCW.mjs +2447 -0
  25. package/dist/chunk-UWVP6LCW.mjs.map +1 -0
  26. package/dist/index.cjs +1668 -99
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +1215 -107
  29. package/dist/index.d.ts +1215 -107
  30. package/dist/index.mjs +1555 -50
  31. package/dist/index.mjs.map +1 -1
  32. package/package.json +16 -15
  33. package/src/audio-assets.d.ts +8 -0
  34. package/src/components/markdown/MarkdownMessage/CollapseToggle.tsx +3 -1
  35. package/src/components/markdown/MarkdownMessage/components.tsx +2 -5
  36. package/src/tools/Chat/README.md +347 -530
  37. package/src/tools/Chat/components/Attachments.tsx +6 -1
  38. package/src/tools/Chat/components/ChatRoot.tsx +30 -2
  39. package/src/tools/Chat/components/Composer.tsx +20 -3
  40. package/src/tools/Chat/components/ErrorBanner.tsx +7 -3
  41. package/src/tools/Chat/components/MessageActions.tsx +3 -1
  42. package/src/tools/Chat/components/MessageBubble.tsx +6 -5
  43. package/src/tools/Chat/components/MessageList.tsx +87 -1
  44. package/src/tools/Chat/components/ToolCalls.tsx +21 -3
  45. package/src/tools/Chat/context/ChatProvider.tsx +21 -3
  46. package/src/tools/Chat/core/audio/audioBus.ts +10 -163
  47. package/src/tools/Chat/core/audio/defaults.ts +43 -0
  48. package/src/tools/Chat/core/audio/index.ts +1 -0
  49. package/src/tools/Chat/core/audio/preferences.ts +5 -59
  50. package/src/tools/Chat/core/audio/sounds/error.mp3 +0 -0
  51. package/src/tools/Chat/core/audio/sounds/mention.mp3 +0 -0
  52. package/src/tools/Chat/core/audio/sounds/notification.mp3 +0 -0
  53. package/src/tools/Chat/core/audio/sounds/received.mp3 +0 -0
  54. package/src/tools/Chat/core/audio/sounds/sent.mp3 +0 -0
  55. package/src/tools/Chat/core/audio/sounds/start.mp3 +0 -0
  56. package/src/tools/Chat/core/audio/types.ts +28 -0
  57. package/src/tools/Chat/core/reducer.ts +33 -0
  58. package/src/tools/Chat/core/transport/index.ts +13 -0
  59. package/src/tools/Chat/core/transport/mappers/index.ts +6 -0
  60. package/src/tools/Chat/core/transport/mappers/pydantic-ai.ts +142 -0
  61. package/src/tools/Chat/core/transport/pydantic-ai-transport.ts +208 -0
  62. package/src/tools/Chat/core/transport/sse.ts +18 -5
  63. package/src/tools/Chat/hooks/index.ts +25 -0
  64. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +5 -3
  65. package/src/tools/Chat/hooks/useChat.ts +28 -0
  66. package/src/tools/Chat/hooks/useChatAudio.ts +59 -180
  67. package/src/tools/Chat/hooks/useChatDockPrefs.ts +74 -0
  68. package/src/tools/Chat/hooks/useChatReset.ts +70 -0
  69. package/src/tools/Chat/hooks/useChatUnread.ts +87 -0
  70. package/src/tools/Chat/hooks/useFocusOnEmptyClick.ts +111 -0
  71. package/src/tools/Chat/hooks/useVisitorFingerprint.ts +48 -0
  72. package/src/tools/Chat/index.ts +84 -1
  73. package/src/tools/Chat/launcher/ChatDock.tsx +263 -0
  74. package/src/tools/Chat/launcher/ChatFAB.tsx +349 -0
  75. package/src/tools/Chat/launcher/ChatGreeting.tsx +200 -0
  76. package/src/tools/Chat/launcher/ChatHeader.tsx +76 -0
  77. package/src/tools/Chat/launcher/ChatHeaderActionButton.tsx +87 -0
  78. package/src/tools/Chat/launcher/ChatHeaderAudioToggle.tsx +47 -0
  79. package/src/tools/Chat/launcher/ChatHeaderLanguageButton.tsx +179 -0
  80. package/src/tools/Chat/launcher/ChatHeaderModeToggle.tsx +57 -0
  81. package/src/tools/Chat/launcher/ChatHeaderResetButton.tsx +93 -0
  82. package/src/tools/Chat/launcher/ChatLauncher.tsx +321 -0
  83. package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +197 -0
  84. package/src/tools/Chat/launcher/index.ts +46 -0
  85. package/src/tools/Chat/launcher/useChatPresence.ts +44 -0
  86. package/src/tools/Chat/styles/bubbleTokens.ts +71 -0
  87. package/src/tools/Chat/styles/index.ts +16 -0
  88. package/src/tools/Chat/styles/useChatStyles.ts +101 -0
  89. package/src/tools/Chat/types/attachment.ts +25 -0
  90. package/src/tools/Chat/types/config.ts +48 -0
  91. package/src/tools/Chat/types/events.ts +35 -0
  92. package/src/tools/Chat/types/index.ts +34 -0
  93. package/src/tools/Chat/types/labels.ts +38 -0
  94. package/src/tools/Chat/types/message.ts +32 -0
  95. package/src/tools/Chat/types/persona.ts +31 -0
  96. package/src/tools/Chat/types/session.ts +43 -0
  97. package/src/tools/Chat/types/tool-call.ts +17 -0
  98. package/src/tools/Chat/types/transport.ts +28 -0
  99. package/src/tools/Chat/types.ts +5 -240
  100. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +50 -14
  101. package/src/tools/MarkdownEditor/index.ts +1 -1
  102. package/src/tools/SpeechRecognition/README.md +336 -0
  103. package/src/tools/SpeechRecognition/__tests__/ids.test.ts +15 -0
  104. package/src/tools/SpeechRecognition/__tests__/language.test.ts +59 -0
  105. package/src/tools/SpeechRecognition/__tests__/reducer.test.ts +71 -0
  106. package/src/tools/SpeechRecognition/__tests__/transcript.test.ts +52 -0
  107. package/src/tools/SpeechRecognition/components/DevicePicker.tsx +49 -0
  108. package/src/tools/SpeechRecognition/components/DictationButton.tsx +93 -0
  109. package/src/tools/SpeechRecognition/components/EngineBadge.tsx +30 -0
  110. package/src/tools/SpeechRecognition/components/ErrorBanner.tsx +52 -0
  111. package/src/tools/SpeechRecognition/components/LanguagePicker.tsx +63 -0
  112. package/src/tools/SpeechRecognition/components/MicMeter.tsx +63 -0
  113. package/src/tools/SpeechRecognition/components/PushToTalkHint.tsx +51 -0
  114. package/src/tools/SpeechRecognition/components/TranscriptView.tsx +55 -0
  115. package/src/tools/SpeechRecognition/components/index.ts +16 -0
  116. package/src/tools/SpeechRecognition/context/SpeechRecognitionProvider.tsx +47 -0
  117. package/src/tools/SpeechRecognition/context/index.ts +6 -0
  118. package/src/tools/SpeechRecognition/core/audio/defaults.ts +24 -0
  119. package/src/tools/SpeechRecognition/core/engine/external.ts +222 -0
  120. package/src/tools/SpeechRecognition/core/engine/http.ts +147 -0
  121. package/src/tools/SpeechRecognition/core/engine/index.ts +52 -0
  122. package/src/tools/SpeechRecognition/core/engine/mediarecorder.ts +105 -0
  123. package/src/tools/SpeechRecognition/core/engine/websocket.ts +211 -0
  124. package/src/tools/SpeechRecognition/core/engine/webspeech.ts +188 -0
  125. package/src/tools/SpeechRecognition/core/ids.ts +11 -0
  126. package/src/tools/SpeechRecognition/core/index.ts +14 -0
  127. package/src/tools/SpeechRecognition/core/language.ts +78 -0
  128. package/src/tools/SpeechRecognition/core/languages-catalog.ts +229 -0
  129. package/src/tools/SpeechRecognition/core/logger.ts +3 -0
  130. package/src/tools/SpeechRecognition/core/reducer.ts +105 -0
  131. package/src/tools/SpeechRecognition/core/transcript.ts +36 -0
  132. package/src/tools/SpeechRecognition/hooks/index.ts +14 -0
  133. package/src/tools/SpeechRecognition/hooks/useDictation.ts +59 -0
  134. package/src/tools/SpeechRecognition/hooks/useEnginePrefs.ts +15 -0
  135. package/src/tools/SpeechRecognition/hooks/useMicDevices.ts +57 -0
  136. package/src/tools/SpeechRecognition/hooks/useMicLevel.ts +52 -0
  137. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +85 -0
  138. package/src/tools/SpeechRecognition/hooks/useResolvedLanguage.ts +28 -0
  139. package/src/tools/SpeechRecognition/hooks/useSpeechLanguageInfo.ts +108 -0
  140. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +188 -0
  141. package/src/tools/SpeechRecognition/hooks/useVoiceSupport.ts +78 -0
  142. package/src/tools/SpeechRecognition/index.ts +82 -0
  143. package/src/tools/SpeechRecognition/lazy.tsx +19 -0
  144. package/src/tools/SpeechRecognition/store/index.ts +2 -0
  145. package/src/tools/SpeechRecognition/store/prefsStore.ts +54 -0
  146. package/src/tools/SpeechRecognition/types.ts +133 -0
  147. package/src/tools/SpeechRecognition/widgets/DictationField.tsx +105 -0
  148. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +305 -0
  149. package/src/tools/SpeechRecognition/widgets/VoiceMessageRecorder.tsx +88 -0
  150. package/src/tools/SpeechRecognition/widgets/index.ts +6 -0
  151. package/dist/ChatRoot-EJC5Y2YM.cjs +0 -14
  152. package/dist/ChatRoot-QOSKJPM6.mjs +0 -5
  153. package/dist/chunk-NWUT327A.mjs.map +0 -1
  154. package/dist/chunk-QLMKCSR6.mjs +0 -2420
  155. package/dist/chunk-QLMKCSR6.mjs.map +0 -1
  156. package/dist/chunk-SI5RD2GD.cjs +0 -2460
  157. package/dist/chunk-SI5RD2GD.cjs.map +0 -1
  158. package/dist/chunk-XACCHZH2.cjs.map +0 -1
  159. package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +0 -771
  160. package/src/stories/index.ts +0 -33
  161. package/src/tools/AudioPlayer/AudioPlayer.story.tsx +0 -481
  162. package/src/tools/Chat/Chat.story.tsx +0 -1457
  163. package/src/tools/CodeEditor/CodeEditor.story.tsx +0 -202
  164. package/src/tools/CronScheduler/CronScheduler.story.tsx +0 -300
  165. package/src/tools/Gallery/Gallery.story.tsx +0 -237
  166. package/src/tools/ImageViewer/ImageViewer.story.tsx +0 -85
  167. package/src/tools/JsonForm/JsonForm.story.tsx +0 -350
  168. package/src/tools/JsonTree/JsonTree.story.tsx +0 -141
  169. package/src/tools/LottiePlayer/LottiePlayer.story.tsx +0 -95
  170. package/src/tools/Map/Map.story.tsx +0 -458
  171. package/src/tools/MarkdownEditor/MarkdownEditor.story.tsx +0 -225
  172. package/src/tools/Mermaid/Mermaid.story.tsx +0 -251
  173. package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +0 -230
  174. package/src/tools/PrettyCode/PrettyCode.story.tsx +0 -304
  175. package/src/tools/Tour/Tour.story.tsx +0 -279
  176. package/src/tools/Tree/Tree.story.tsx +0 -620
  177. package/src/tools/Uploader/Uploader.story.tsx +0 -415
  178. package/src/tools/VideoPlayer/VideoPlayer.story.tsx +0 -87
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ const DEFAULT_STORAGE_KEY = 'chat.visitor.fingerprint';
6
+
7
+ function generate(): string {
8
+ if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
9
+ return crypto.randomUUID();
10
+ }
11
+ return `v-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
12
+ }
13
+
14
+ export interface UseVisitorFingerprintOptions {
15
+ /** localStorage key. @default 'chat.visitor.fingerprint' */
16
+ storageKey?: string;
17
+ }
18
+
19
+ /**
20
+ * Persistent anonymous visitor id, kept in `localStorage`.
21
+ *
22
+ * Returns `null` on the first render (SSR-safe) and the stable id from
23
+ * the second render onwards. Use as `fingerprint` for public chat
24
+ * transports that need to dedupe sessions per visitor without auth.
25
+ */
26
+ export function useVisitorFingerprint(
27
+ opts: UseVisitorFingerprintOptions = {},
28
+ ): string | null {
29
+ const storageKey = opts.storageKey ?? DEFAULT_STORAGE_KEY;
30
+ const [fp, setFp] = useState<string | null>(null);
31
+
32
+ useEffect(() => {
33
+ let value: string | null = null;
34
+ try {
35
+ value = window.localStorage.getItem(storageKey);
36
+ if (!value) {
37
+ value = generate();
38
+ window.localStorage.setItem(storageKey, value);
39
+ }
40
+ } catch {
41
+ // Private mode / storage disabled — fall back to ephemeral.
42
+ value = generate();
43
+ }
44
+ setFp(value);
45
+ }, [storageKey]);
46
+
47
+ return fp;
48
+ }
@@ -61,11 +61,53 @@ export {
61
61
  createMockTransport,
62
62
  parseSSE,
63
63
  TransportError,
64
+ createPydanticAIChatTransport,
65
+ createToolIdQueue,
66
+ mapPydanticAIEvent,
67
+ createPydanticAISSEMap,
64
68
  type HttpTransportConfig,
65
69
  type MockTransportOptions,
66
70
  type ParseSSEOptions,
71
+ type PydanticAIChatTransportOpts,
72
+ type PydanticAIEvent,
73
+ type ToolIdQueue,
67
74
  } from './core/transport';
68
75
 
76
+ // Launcher (FAB + Dock + Header + Greeting composition)
77
+ export {
78
+ ChatFAB,
79
+ ChatDock,
80
+ ChatHeader,
81
+ ChatHeaderActionButton,
82
+ ChatHeaderModeToggle,
83
+ ChatHeaderAudioToggle,
84
+ ChatHeaderResetButton,
85
+ ChatHeaderLanguageButton,
86
+ ChatLauncher,
87
+ ChatGreeting,
88
+ ChatUnreadPreview,
89
+ useChatPresence,
90
+ type ChatFABProps,
91
+ type ChatFABPosition,
92
+ type ChatFABVariant,
93
+ type ChatFABSize,
94
+ type ChatDockProps,
95
+ type ChatDockMode,
96
+ type ChatDockSide,
97
+ type ChatHeaderProps,
98
+ type ChatHeaderActionButtonProps,
99
+ type ChatHeaderModeToggleProps,
100
+ type ChatHeaderAudioToggleProps,
101
+ type ChatHeaderResetButtonProps,
102
+ type ChatHeaderLanguageButtonProps,
103
+ type ChatLauncherProps,
104
+ type ChatLauncherHotkey,
105
+ type ChatLauncherGreeting,
106
+ type ChatGreetingProps,
107
+ type ChatUnreadPreviewProps,
108
+ type ChatPresencePhase,
109
+ } from './launcher';
110
+
69
111
  // Hooks
70
112
  export {
71
113
  useChat,
@@ -76,6 +118,14 @@ export {
76
118
  useChatAudio,
77
119
  useAutoFocusOnStreamEnd,
78
120
  useRegisterComposer,
121
+ useChatReset,
122
+ useVisitorFingerprint,
123
+ useChatDockPrefs,
124
+ DEFAULT_DOCK_PREFS,
125
+ useFocusOnEmptyClick,
126
+ useChatUnread,
127
+ type UseChatUnreadOptions,
128
+ type UseChatUnreadReturn,
79
129
  type UseChatConfig,
80
130
  type UseChatReturn,
81
131
  type UseChatComposerOptions,
@@ -87,6 +137,13 @@ export {
87
137
  type UseChatLayoutReturn,
88
138
  type UseAutoFocusOnStreamEndOptions,
89
139
  type Focusable,
140
+ type UseChatResetOptions,
141
+ type UseChatResetReturn,
142
+ type UseVisitorFingerprintOptions,
143
+ type ChatDockPrefs,
144
+ type UseChatDockPrefsOptions,
145
+ type UseChatDockPrefsReturn,
146
+ type UseFocusOnEmptyClickOptions,
90
147
  } from './hooks';
91
148
 
92
149
  // Audio
@@ -96,7 +153,7 @@ export type {
96
153
  ChatAudioConfig,
97
154
  UseChatAudioReturn,
98
155
  } from './core/audio';
99
- export { useChatAudioPrefs } from './core/audio';
156
+ export { useChatAudioPrefs, DEFAULT_CHAT_SOUNDS } from './core/audio';
100
157
 
101
158
  // Tool-call payload dispatcher
102
159
  export {
@@ -111,6 +168,22 @@ export {
111
168
 
112
169
  // Lightbox helpers
113
170
  export { useChatLightbox, type UseChatLightboxReturn, type ChatLightboxState } from './hooks';
171
+
172
+ // Styles — role-aware className tokens + hooks
173
+ export {
174
+ BUBBLE_SURFACE,
175
+ ANCHOR,
176
+ TOGGLE,
177
+ DESTRUCTIVE_SURFACE,
178
+ TOOL_CALL,
179
+ useChatBubbleStyles,
180
+ useChatRoleStyles,
181
+ useChatDestructiveStyles,
182
+ type ChatBubbleSurface,
183
+ type ChatBubbleStyles,
184
+ type ChatRoleStyles,
185
+ type ChatDestructiveStyles,
186
+ } from './styles';
114
187
  export { collectImageAttachments } from './utils/collectImageAttachments';
115
188
 
116
189
  // Draft sanitation — trim, collapse runs, strip zero-width chars.
@@ -171,3 +244,13 @@ export {
171
244
 
172
245
  // Lazy preset
173
246
  export { LazyChat } from './lazy';
247
+
248
+ // Markdown renderer (used internally by MessageBubble; re-exported so
249
+ // hosts can render chat-style markdown outside a bubble — e.g. previews,
250
+ // receipts, support emails).
251
+ export {
252
+ MarkdownMessage,
253
+ extractTextFromChildren,
254
+ type MarkdownMessageProps,
255
+ type LinkRule,
256
+ } from '../../components/markdown';
@@ -0,0 +1,263 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import type { CSSProperties, ReactNode } from 'react';
5
+
6
+ import { Portal } from '@djangocfg/ui-core/components';
7
+ import { useIsMobile, useIsTabletOrBelow } from '@djangocfg/ui-core/hooks';
8
+ import { cn } from '@djangocfg/ui-core/lib';
9
+
10
+ import { ChatHeader } from './ChatHeader';
11
+ import { useChatPresence } from './useChatPresence';
12
+ import type { ChatFABPosition } from './ChatFAB';
13
+
14
+ export type ChatDockMode = 'popover' | 'side';
15
+ export type ChatDockSide = 'left' | 'right';
16
+
17
+ export interface ChatDockProps {
18
+ /** Controlled open state. */
19
+ open: boolean;
20
+ /** Called when the user clicks the close button. */
21
+ onClose: () => void;
22
+ /** Dock contents (typically a `<Chat>` component). */
23
+ children: ReactNode;
24
+ /**
25
+ * Visual mode.
26
+ * - `popover` (default): floating card anchored to a corner, fixed size, FAB-style.
27
+ * - `side`: docked panel pinned to the left/right edge, full viewport height.
28
+ */
29
+ mode?: ChatDockMode;
30
+ /** Side for `mode='side'`. @default 'right' */
31
+ side?: ChatDockSide;
32
+ /** Header title text. */
33
+ title?: ReactNode;
34
+ /** Header icon. Defaults to a bot glyph. */
35
+ icon?: ReactNode;
36
+ /**
37
+ * Header actions slot (right side, before the close button).
38
+ * Use `ChatHeaderActionButton` to keep visual consistency.
39
+ */
40
+ headerActions?: ReactNode;
41
+ /** Hide the header entirely (you render your own inside `children`). */
42
+ hideHeader?: boolean;
43
+ /** ARIA label for the close button. @default 'Close' */
44
+ closeLabel?: string;
45
+ /** Dock width in px. Clamped to viewport. @default 480 (popover) / 420 (side) */
46
+ width?: number;
47
+ /** Dock height in px. Only used in `popover` mode. @default 720 */
48
+ height?: number;
49
+ /** Which screen corner to dock to in `popover` mode. @default 'bottom-right' */
50
+ position?: ChatFABPosition;
51
+ /** Offset from screen edges in px (popover only). @default 24 / 96 */
52
+ offset?: { horizontal?: number; vertical?: number };
53
+ /** Transition duration in ms — should match CSS animation. @default 200 */
54
+ exitDurationMs?: number;
55
+ /** z-index. @default 10000 */
56
+ zIndex?: number;
57
+ /** Accessible dialog label. */
58
+ ariaLabel?: string;
59
+ /** Extra classes on the dock container. */
60
+ className?: string;
61
+ /**
62
+ * Take over the full viewport on mobile (< 768px). Applies to both modes.
63
+ * @default true
64
+ */
65
+ mobileFullscreen?: boolean;
66
+ /**
67
+ * Render in-place (not in `document.body` via a portal). Useful for stories,
68
+ * screenshots, or wrapping the dock inside a custom container. @default false
69
+ */
70
+ disablePortal?: boolean;
71
+ /**
72
+ * Drop fixed positioning entirely — the dock renders as a normal flow
73
+ * element sized by `width`/`height`. Combine with `disablePortal` for
74
+ * stories/previews where the dock should sit inside the panel instead
75
+ * of attaching to the viewport. @default false
76
+ */
77
+ inline?: boolean;
78
+ /**
79
+ * In `mode='side'`, reserve space on the document body so page content
80
+ * isn't covered by the dock. Sets `padding-{side}` on `<body>` while
81
+ * the dock is open and exposes the width via the `--chat-dock-reserve`
82
+ * CSS variable for custom layouts. @default true (when mode='side')
83
+ */
84
+ reserveBodySpace?: boolean;
85
+ }
86
+
87
+ function dockPositionStyle(
88
+ position: ChatFABPosition,
89
+ horizontal: number,
90
+ vertical: number,
91
+ ): CSSProperties {
92
+ const [vert, horiz] = position.split('-') as ['bottom' | 'top', 'right' | 'left'];
93
+ return { [vert]: vertical, [horiz]: horizontal } as CSSProperties;
94
+ }
95
+
96
+ /**
97
+ * Fixed-position chat surface. Two modes:
98
+ *
99
+ * - `popover` — floating card anchored to a corner. Companion to `<ChatFAB>`.
100
+ * - `side` — full-height panel pinned to the left/right edge. App-shell style.
101
+ *
102
+ * Renders only when `open` is true (plus the leave-transition tail). Uses
103
+ * `useChatPresence` for the four-phase mount/animate/unmount cycle.
104
+ */
105
+ export function ChatDock({
106
+ open,
107
+ onClose,
108
+ children,
109
+ mode = 'popover',
110
+ side = 'right',
111
+ title = 'Chat',
112
+ icon,
113
+ headerActions,
114
+ hideHeader = false,
115
+ closeLabel,
116
+ width,
117
+ height = 720,
118
+ position = 'bottom-right',
119
+ offset,
120
+ exitDurationMs = 200,
121
+ zIndex = 10000,
122
+ ariaLabel,
123
+ className,
124
+ mobileFullscreen = true,
125
+ disablePortal = false,
126
+ inline = false,
127
+ reserveBodySpace,
128
+ }: ChatDockProps) {
129
+ const phase = useChatPresence(open, exitDurationMs);
130
+ const isMobile = useIsMobile();
131
+ // Side mode is desktop-only — narrow viewports fall back to popover so
132
+ // we never cover 33% of a phone/tablet with a chat panel.
133
+ const isBelowDesktop = useIsTabletOrBelow();
134
+ const effectiveMode: ChatDockMode =
135
+ mode === 'side' && !isBelowDesktop ? 'side' : 'popover';
136
+ const fullscreen = mobileFullscreen && isMobile;
137
+
138
+ // Reserve body padding for side mode so page content stays visible
139
+ // next to the dock. Auto-on when mode='side' unless explicitly disabled.
140
+ const wantsReserve =
141
+ !inline && !fullscreen && effectiveMode === 'side' && (reserveBodySpace ?? true);
142
+ const resolvedSideWidth = width ?? 420;
143
+ useEffect(() => {
144
+ if (!wantsReserve || phase === 'hidden') return;
145
+ const body = document.body;
146
+ if (!body) return;
147
+ const cssVar = `${resolvedSideWidth}px`;
148
+ const padKey = side === 'right' ? 'paddingRight' : 'paddingLeft';
149
+ const prevPad = body.style[padKey as 'paddingRight' | 'paddingLeft'];
150
+ const prevVar = body.style.getPropertyValue('--chat-dock-reserve');
151
+ body.style[padKey as 'paddingRight' | 'paddingLeft'] = cssVar;
152
+ body.style.setProperty('--chat-dock-reserve', cssVar);
153
+ return () => {
154
+ body.style[padKey as 'paddingRight' | 'paddingLeft'] = prevPad;
155
+ if (prevVar) body.style.setProperty('--chat-dock-reserve', prevVar);
156
+ else body.style.removeProperty('--chat-dock-reserve');
157
+ };
158
+ }, [wantsReserve, phase, side, resolvedSideWidth]);
159
+
160
+ if (phase === 'hidden') return null;
161
+
162
+ const animating = phase === 'entering' || phase === 'leaving';
163
+
164
+ const horizontal = offset?.horizontal ?? 24;
165
+ const vertical = offset?.vertical ?? 96;
166
+ const resolvedWidth = width ?? (effectiveMode === 'side' ? resolvedSideWidth : 480);
167
+
168
+ let containerStyle: CSSProperties;
169
+ let cornerClass: string;
170
+
171
+ // Dynamic viewport heights — `dvh` follows iOS Safari URL bar (preferred),
172
+ // `svh`/`lvh` are the small/large fallbacks if the dynamic value isn't
173
+ // supported. Min-height keeps the popover usable even on tiny landscape phones.
174
+ const dynVH = '100dvh';
175
+
176
+ if (inline) {
177
+ containerStyle = {
178
+ position: 'relative',
179
+ width: resolvedWidth,
180
+ height,
181
+ maxHeight: `calc(${dynVH} - 16px)`,
182
+ pointerEvents: phase === 'visible' ? 'auto' : 'none',
183
+ };
184
+ cornerClass = 'rounded-xl border';
185
+ } else if (fullscreen) {
186
+ containerStyle = {
187
+ position: 'fixed',
188
+ top: 0,
189
+ [side === 'left' ? 'left' : 'right']: 0,
190
+ width: '100vw',
191
+ height: dynVH,
192
+ zIndex,
193
+ pointerEvents: phase === 'visible' ? 'auto' : 'none',
194
+ } as CSSProperties;
195
+ cornerClass = 'rounded-none border-0';
196
+ } else if (effectiveMode === 'side') {
197
+ containerStyle = {
198
+ position: 'fixed',
199
+ top: 0,
200
+ [side]: 0,
201
+ height: dynVH,
202
+ zIndex,
203
+ width: `min(${resolvedWidth}px, 100vw)`,
204
+ pointerEvents: phase === 'visible' ? 'auto' : 'none',
205
+ } as CSSProperties;
206
+ cornerClass = side === 'right' ? 'rounded-none border-l' : 'rounded-none border-r';
207
+ } else {
208
+ // popover — anchored to a corner, capped to viewport so it never
209
+ // overlaps the FAB or goes off-screen on small windows.
210
+ const heightCap = `calc(${dynVH} - ${vertical + 24}px)`;
211
+ containerStyle = {
212
+ position: 'fixed',
213
+ ...dockPositionStyle(position, horizontal, vertical),
214
+ zIndex,
215
+ width: `min(${resolvedWidth}px, calc(100vw - 32px))`,
216
+ height: `min(${height}px, ${heightCap})`,
217
+ minHeight: `min(320px, ${heightCap})`,
218
+ pointerEvents: phase === 'visible' ? 'auto' : 'none',
219
+ };
220
+ cornerClass = 'rounded-xl border';
221
+ }
222
+
223
+ // Per-mode enter/leave transform classes — side slides in horizontally,
224
+ // popover scales + lifts.
225
+ const enterClass = (() => {
226
+ if (fullscreen) return 'opacity-0';
227
+ if (effectiveMode === 'side') {
228
+ return side === 'right' ? 'opacity-0 translate-x-4' : 'opacity-0 -translate-x-4';
229
+ }
230
+ return 'opacity-0 scale-95 translate-y-2';
231
+ })();
232
+ const visibleClass = 'opacity-100 scale-100 translate-y-0 translate-x-0';
233
+
234
+ return (
235
+ <Portal disablePortal={disablePortal || inline}>
236
+ <div
237
+ role="dialog"
238
+ aria-label={ariaLabel ?? (typeof title === 'string' ? title : 'Chat')}
239
+ aria-hidden={phase === 'leaving'}
240
+ className={cn(
241
+ 'bg-popover text-popover-foreground border-border',
242
+ 'flex flex-col overflow-hidden shadow-2xl',
243
+ cornerClass,
244
+ 'transition-all duration-200 ease-out',
245
+ animating ? enterClass : visibleClass,
246
+ className,
247
+ )}
248
+ style={containerStyle}
249
+ >
250
+ {!hideHeader && (
251
+ <ChatHeader
252
+ title={title}
253
+ icon={icon}
254
+ actions={headerActions}
255
+ onClose={onClose}
256
+ closeLabel={closeLabel}
257
+ />
258
+ )}
259
+ <div className="min-h-0 min-w-0 flex-1 overflow-hidden">{children}</div>
260
+ </div>
261
+ </Portal>
262
+ );
263
+ }