@djangocfg/ui-tools 2.1.381 → 2.1.382

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 (183) 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-2ZLQWLYV.mjs +4 -0
  7. package/dist/DictationField-2ZLQWLYV.mjs.map +1 -0
  8. package/dist/DictationField-IPPJ54CU.cjs +13 -0
  9. package/dist/DictationField-IPPJ54CU.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-4LXG3NBV.mjs +833 -0
  15. package/dist/chunk-4LXG3NBV.mjs.map +1 -0
  16. package/dist/{chunk-XACCHZH2.cjs → chunk-FIRK5CEH.cjs} +42 -4
  17. package/dist/chunk-FIRK5CEH.cjs.map +1 -0
  18. package/dist/{chunk-NWUT327A.mjs → chunk-HIK6BPL7.mjs} +38 -5
  19. package/dist/chunk-HIK6BPL7.mjs.map +1 -0
  20. package/dist/chunk-KMSBGNVC.cjs +835 -0
  21. package/dist/chunk-KMSBGNVC.cjs.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 +1532 -100
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +1148 -107
  29. package/dist/index.d.ts +1148 -107
  30. package/dist/index.mjs +1421 -51
  31. package/dist/index.mjs.map +1 -1
  32. package/package.json +16 -8
  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/stories/index.ts +32 -2
  37. package/src/tools/Chat/README.md +347 -530
  38. package/src/tools/Chat/components/Attachments.tsx +6 -1
  39. package/src/tools/Chat/components/ChatRoot.tsx +30 -2
  40. package/src/tools/Chat/components/Composer.tsx +20 -3
  41. package/src/tools/Chat/components/ErrorBanner.tsx +7 -3
  42. package/src/tools/Chat/components/MessageActions.tsx +3 -1
  43. package/src/tools/Chat/components/MessageBubble.tsx +6 -5
  44. package/src/tools/Chat/components/MessageList.tsx +87 -1
  45. package/src/tools/Chat/components/ToolCalls.tsx +21 -3
  46. package/src/tools/Chat/context/ChatProvider.tsx +21 -3
  47. package/src/tools/Chat/core/audio/audioBus.ts +10 -163
  48. package/src/tools/Chat/core/audio/defaults.ts +43 -0
  49. package/src/tools/Chat/core/audio/index.ts +1 -0
  50. package/src/tools/Chat/core/audio/preferences.ts +5 -59
  51. package/src/tools/Chat/core/audio/sounds/error.mp3 +0 -0
  52. package/src/tools/Chat/core/audio/sounds/mention.mp3 +0 -0
  53. package/src/tools/Chat/core/audio/sounds/notification.mp3 +0 -0
  54. package/src/tools/Chat/core/audio/sounds/received.mp3 +0 -0
  55. package/src/tools/Chat/core/audio/sounds/sent.mp3 +0 -0
  56. package/src/tools/Chat/core/audio/sounds/start.mp3 +0 -0
  57. package/src/tools/Chat/core/audio/types.ts +28 -0
  58. package/src/tools/Chat/core/reducer.ts +33 -0
  59. package/src/tools/Chat/core/transport/index.ts +13 -0
  60. package/src/tools/Chat/core/transport/mappers/index.ts +6 -0
  61. package/src/tools/Chat/core/transport/mappers/pydantic-ai.ts +142 -0
  62. package/src/tools/Chat/core/transport/pydantic-ai-transport.ts +208 -0
  63. package/src/tools/Chat/core/transport/sse.ts +18 -5
  64. package/src/tools/Chat/hooks/index.ts +25 -0
  65. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +5 -3
  66. package/src/tools/Chat/hooks/useChat.ts +28 -0
  67. package/src/tools/Chat/hooks/useChatAudio.ts +59 -180
  68. package/src/tools/Chat/hooks/useChatDockPrefs.ts +74 -0
  69. package/src/tools/Chat/hooks/useChatReset.ts +70 -0
  70. package/src/tools/Chat/hooks/useChatUnread.ts +87 -0
  71. package/src/tools/Chat/hooks/useFocusOnEmptyClick.ts +111 -0
  72. package/src/tools/Chat/hooks/useVisitorFingerprint.ts +48 -0
  73. package/src/tools/Chat/index.ts +69 -1
  74. package/src/tools/Chat/launcher/ChatDock.tsx +263 -0
  75. package/src/tools/Chat/launcher/ChatFAB.tsx +349 -0
  76. package/src/tools/Chat/launcher/ChatGreeting.tsx +200 -0
  77. package/src/tools/Chat/launcher/ChatHeader.tsx +76 -0
  78. package/src/tools/Chat/launcher/ChatHeaderActionButton.tsx +87 -0
  79. package/src/tools/Chat/launcher/ChatHeaderAudioToggle.tsx +47 -0
  80. package/src/tools/Chat/launcher/ChatHeaderLanguageButton.tsx +179 -0
  81. package/src/tools/Chat/launcher/ChatHeaderModeToggle.tsx +57 -0
  82. package/src/tools/Chat/launcher/ChatHeaderResetButton.tsx +93 -0
  83. package/src/tools/Chat/launcher/ChatLauncher.tsx +321 -0
  84. package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +197 -0
  85. package/src/tools/Chat/launcher/index.ts +46 -0
  86. package/src/tools/Chat/launcher/useChatPresence.ts +44 -0
  87. package/src/tools/Chat/stories/01-basic.story.tsx +64 -0
  88. package/src/tools/Chat/stories/02-bubbles.story.tsx +21 -0
  89. package/src/tools/Chat/stories/03-tool-calls.story.tsx +59 -0
  90. package/src/tools/Chat/stories/04-personas.story.tsx +78 -0
  91. package/src/tools/Chat/stories/05-launcher.story.tsx +321 -0
  92. package/src/tools/Chat/stories/06-header.story.tsx +147 -0
  93. package/src/tools/Chat/stories/07-audio-actions.story.tsx +112 -0
  94. package/src/tools/Chat/stories/shared/Frame.tsx +21 -0
  95. package/src/tools/Chat/stories/shared/index.ts +5 -0
  96. package/src/tools/Chat/stories/shared/messages.ts +39 -0
  97. package/src/tools/Chat/stories/shared/personas.ts +13 -0
  98. package/src/tools/Chat/stories/shared/seeds.ts +92 -0
  99. package/src/tools/Chat/stories/shared/transports.ts +36 -0
  100. package/src/tools/Chat/styles/bubbleTokens.ts +71 -0
  101. package/src/tools/Chat/styles/index.ts +16 -0
  102. package/src/tools/Chat/styles/useChatStyles.ts +101 -0
  103. package/src/tools/Chat/types/attachment.ts +25 -0
  104. package/src/tools/Chat/types/config.ts +48 -0
  105. package/src/tools/Chat/types/events.ts +35 -0
  106. package/src/tools/Chat/types/index.ts +34 -0
  107. package/src/tools/Chat/types/labels.ts +38 -0
  108. package/src/tools/Chat/types/message.ts +32 -0
  109. package/src/tools/Chat/types/persona.ts +31 -0
  110. package/src/tools/Chat/types/session.ts +43 -0
  111. package/src/tools/Chat/types/tool-call.ts +17 -0
  112. package/src/tools/Chat/types/transport.ts +28 -0
  113. package/src/tools/Chat/types.ts +5 -240
  114. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +50 -14
  115. package/src/tools/MarkdownEditor/index.ts +1 -1
  116. package/src/tools/SpeechRecognition/README.md +336 -0
  117. package/src/tools/SpeechRecognition/__tests__/ids.test.ts +15 -0
  118. package/src/tools/SpeechRecognition/__tests__/language.test.ts +59 -0
  119. package/src/tools/SpeechRecognition/__tests__/reducer.test.ts +71 -0
  120. package/src/tools/SpeechRecognition/__tests__/transcript.test.ts +52 -0
  121. package/src/tools/SpeechRecognition/components/DevicePicker.tsx +49 -0
  122. package/src/tools/SpeechRecognition/components/DictationButton.tsx +93 -0
  123. package/src/tools/SpeechRecognition/components/EngineBadge.tsx +30 -0
  124. package/src/tools/SpeechRecognition/components/ErrorBanner.tsx +52 -0
  125. package/src/tools/SpeechRecognition/components/LanguagePicker.tsx +63 -0
  126. package/src/tools/SpeechRecognition/components/MicMeter.tsx +63 -0
  127. package/src/tools/SpeechRecognition/components/PushToTalkHint.tsx +51 -0
  128. package/src/tools/SpeechRecognition/components/TranscriptView.tsx +55 -0
  129. package/src/tools/SpeechRecognition/components/index.ts +16 -0
  130. package/src/tools/SpeechRecognition/context/SpeechRecognitionProvider.tsx +47 -0
  131. package/src/tools/SpeechRecognition/context/index.ts +6 -0
  132. package/src/tools/SpeechRecognition/core/audio/defaults.ts +24 -0
  133. package/src/tools/SpeechRecognition/core/engine/external.ts +222 -0
  134. package/src/tools/SpeechRecognition/core/engine/http.ts +147 -0
  135. package/src/tools/SpeechRecognition/core/engine/index.ts +52 -0
  136. package/src/tools/SpeechRecognition/core/engine/mediarecorder.ts +105 -0
  137. package/src/tools/SpeechRecognition/core/engine/websocket.ts +211 -0
  138. package/src/tools/SpeechRecognition/core/engine/webspeech.ts +188 -0
  139. package/src/tools/SpeechRecognition/core/ids.ts +11 -0
  140. package/src/tools/SpeechRecognition/core/index.ts +14 -0
  141. package/src/tools/SpeechRecognition/core/language.ts +78 -0
  142. package/src/tools/SpeechRecognition/core/languages-catalog.ts +229 -0
  143. package/src/tools/SpeechRecognition/core/logger.ts +3 -0
  144. package/src/tools/SpeechRecognition/core/reducer.ts +105 -0
  145. package/src/tools/SpeechRecognition/core/transcript.ts +36 -0
  146. package/src/tools/SpeechRecognition/hooks/index.ts +14 -0
  147. package/src/tools/SpeechRecognition/hooks/useDictation.ts +59 -0
  148. package/src/tools/SpeechRecognition/hooks/useEnginePrefs.ts +15 -0
  149. package/src/tools/SpeechRecognition/hooks/useMicDevices.ts +57 -0
  150. package/src/tools/SpeechRecognition/hooks/useMicLevel.ts +52 -0
  151. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +85 -0
  152. package/src/tools/SpeechRecognition/hooks/useResolvedLanguage.ts +28 -0
  153. package/src/tools/SpeechRecognition/hooks/useSpeechLanguageInfo.ts +108 -0
  154. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +188 -0
  155. package/src/tools/SpeechRecognition/hooks/useVoiceSupport.ts +78 -0
  156. package/src/tools/SpeechRecognition/index.ts +82 -0
  157. package/src/tools/SpeechRecognition/lazy.tsx +19 -0
  158. package/src/tools/SpeechRecognition/store/index.ts +2 -0
  159. package/src/tools/SpeechRecognition/store/prefsStore.ts +54 -0
  160. package/src/tools/SpeechRecognition/stories/01-basic.story.tsx +32 -0
  161. package/src/tools/SpeechRecognition/stories/02-dictation-field.story.tsx +32 -0
  162. package/src/tools/SpeechRecognition/stories/03-push-to-talk.story.tsx +27 -0
  163. package/src/tools/SpeechRecognition/stories/04-mic-meter.story.tsx +35 -0
  164. package/src/tools/SpeechRecognition/stories/05-custom-engine-http.story.tsx +40 -0
  165. package/src/tools/SpeechRecognition/stories/06-custom-engine-ws.story.tsx +48 -0
  166. package/src/tools/SpeechRecognition/stories/07-language-device.story.tsx +57 -0
  167. package/src/tools/SpeechRecognition/stories/08-errors-permissions.story.tsx +25 -0
  168. package/src/tools/SpeechRecognition/stories/09-chat-voice.story.tsx +90 -0
  169. package/src/tools/SpeechRecognition/stories/shared.tsx +123 -0
  170. package/src/tools/SpeechRecognition/types.ts +133 -0
  171. package/src/tools/SpeechRecognition/widgets/DictationField.tsx +105 -0
  172. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +305 -0
  173. package/src/tools/SpeechRecognition/widgets/VoiceMessageRecorder.tsx +88 -0
  174. package/src/tools/SpeechRecognition/widgets/index.ts +6 -0
  175. package/dist/ChatRoot-EJC5Y2YM.cjs +0 -14
  176. package/dist/ChatRoot-QOSKJPM6.mjs +0 -5
  177. package/dist/chunk-NWUT327A.mjs.map +0 -1
  178. package/dist/chunk-QLMKCSR6.mjs +0 -2420
  179. package/dist/chunk-QLMKCSR6.mjs.map +0 -1
  180. package/dist/chunk-SI5RD2GD.cjs +0 -2460
  181. package/dist/chunk-SI5RD2GD.cjs.map +0 -1
  182. package/dist/chunk-XACCHZH2.cjs.map +0 -1
  183. package/src/tools/Chat/Chat.story.tsx +0 -1457
@@ -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,51 @@ 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
+ ChatLauncher,
86
+ ChatGreeting,
87
+ ChatUnreadPreview,
88
+ useChatPresence,
89
+ type ChatFABProps,
90
+ type ChatFABPosition,
91
+ type ChatFABVariant,
92
+ type ChatFABSize,
93
+ type ChatDockProps,
94
+ type ChatDockMode,
95
+ type ChatDockSide,
96
+ type ChatHeaderProps,
97
+ type ChatHeaderActionButtonProps,
98
+ type ChatHeaderModeToggleProps,
99
+ type ChatHeaderAudioToggleProps,
100
+ type ChatHeaderResetButtonProps,
101
+ type ChatLauncherProps,
102
+ type ChatLauncherHotkey,
103
+ type ChatLauncherGreeting,
104
+ type ChatGreetingProps,
105
+ type ChatUnreadPreviewProps,
106
+ type ChatPresencePhase,
107
+ } from './launcher';
108
+
69
109
  // Hooks
70
110
  export {
71
111
  useChat,
@@ -76,6 +116,11 @@ export {
76
116
  useChatAudio,
77
117
  useAutoFocusOnStreamEnd,
78
118
  useRegisterComposer,
119
+ useChatReset,
120
+ useVisitorFingerprint,
121
+ useChatDockPrefs,
122
+ DEFAULT_DOCK_PREFS,
123
+ useFocusOnEmptyClick,
79
124
  type UseChatConfig,
80
125
  type UseChatReturn,
81
126
  type UseChatComposerOptions,
@@ -87,6 +132,13 @@ export {
87
132
  type UseChatLayoutReturn,
88
133
  type UseAutoFocusOnStreamEndOptions,
89
134
  type Focusable,
135
+ type UseChatResetOptions,
136
+ type UseChatResetReturn,
137
+ type UseVisitorFingerprintOptions,
138
+ type ChatDockPrefs,
139
+ type UseChatDockPrefsOptions,
140
+ type UseChatDockPrefsReturn,
141
+ type UseFocusOnEmptyClickOptions,
90
142
  } from './hooks';
91
143
 
92
144
  // Audio
@@ -96,7 +148,7 @@ export type {
96
148
  ChatAudioConfig,
97
149
  UseChatAudioReturn,
98
150
  } from './core/audio';
99
- export { useChatAudioPrefs } from './core/audio';
151
+ export { useChatAudioPrefs, DEFAULT_CHAT_SOUNDS } from './core/audio';
100
152
 
101
153
  // Tool-call payload dispatcher
102
154
  export {
@@ -111,6 +163,22 @@ export {
111
163
 
112
164
  // Lightbox helpers
113
165
  export { useChatLightbox, type UseChatLightboxReturn, type ChatLightboxState } from './hooks';
166
+
167
+ // Styles — role-aware className tokens + hooks
168
+ export {
169
+ BUBBLE_SURFACE,
170
+ ANCHOR,
171
+ TOGGLE,
172
+ DESTRUCTIVE_SURFACE,
173
+ TOOL_CALL,
174
+ useChatBubbleStyles,
175
+ useChatRoleStyles,
176
+ useChatDestructiveStyles,
177
+ type ChatBubbleSurface,
178
+ type ChatBubbleStyles,
179
+ type ChatRoleStyles,
180
+ type ChatDestructiveStyles,
181
+ } from './styles';
114
182
  export { collectImageAttachments } from './utils/collectImageAttachments';
115
183
 
116
184
  // Draft sanitation — trim, collapse runs, strip zero-width chars.
@@ -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
+ }