@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.
- package/README.md +132 -899
- package/dist/ChatRoot-6IZFM5HM.mjs +5 -0
- package/dist/{ChatRoot-EJC5Y2YM.cjs.map → ChatRoot-6IZFM5HM.mjs.map} +1 -1
- package/dist/ChatRoot-LW4XNIKP.cjs +14 -0
- package/dist/{ChatRoot-QOSKJPM6.mjs.map → ChatRoot-LW4XNIKP.cjs.map} +1 -1
- package/dist/DictationField-U25MEYAL.mjs +4 -0
- package/dist/DictationField-U25MEYAL.mjs.map +1 -0
- package/dist/DictationField-XWR5VOID.cjs +13 -0
- package/dist/DictationField-XWR5VOID.cjs.map +1 -0
- package/dist/{DocsLayout-2YKPXZYO.mjs → DocsLayout-2P3ONDWJ.mjs} +3 -3
- package/dist/{DocsLayout-2YKPXZYO.mjs.map → DocsLayout-2P3ONDWJ.mjs.map} +1 -1
- package/dist/{DocsLayout-Q4KS3QWW.cjs → DocsLayout-2YZNS5VK.cjs} +8 -8
- package/dist/{DocsLayout-Q4KS3QWW.cjs.map → DocsLayout-2YZNS5VK.cjs.map} +1 -1
- package/dist/chunk-4PFW7MIJ.cjs +837 -0
- package/dist/chunk-4PFW7MIJ.cjs.map +1 -0
- package/dist/chunk-C2YN6WEO.mjs +833 -0
- package/dist/chunk-C2YN6WEO.mjs.map +1 -0
- package/dist/{chunk-XACCHZH2.cjs → chunk-FIRK5CEH.cjs} +42 -4
- package/dist/chunk-FIRK5CEH.cjs.map +1 -0
- package/dist/{chunk-NWUT327A.mjs → chunk-HIK6BPL7.mjs} +38 -5
- package/dist/chunk-HIK6BPL7.mjs.map +1 -0
- package/dist/chunk-OZAU3QWD.cjs +2493 -0
- package/dist/chunk-OZAU3QWD.cjs.map +1 -0
- package/dist/chunk-UWVP6LCW.mjs +2447 -0
- package/dist/chunk-UWVP6LCW.mjs.map +1 -0
- package/dist/index.cjs +1668 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1215 -107
- package/dist/index.d.ts +1215 -107
- package/dist/index.mjs +1555 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -15
- package/src/audio-assets.d.ts +8 -0
- package/src/components/markdown/MarkdownMessage/CollapseToggle.tsx +3 -1
- package/src/components/markdown/MarkdownMessage/components.tsx +2 -5
- package/src/tools/Chat/README.md +347 -530
- package/src/tools/Chat/components/Attachments.tsx +6 -1
- package/src/tools/Chat/components/ChatRoot.tsx +30 -2
- package/src/tools/Chat/components/Composer.tsx +20 -3
- package/src/tools/Chat/components/ErrorBanner.tsx +7 -3
- package/src/tools/Chat/components/MessageActions.tsx +3 -1
- package/src/tools/Chat/components/MessageBubble.tsx +6 -5
- package/src/tools/Chat/components/MessageList.tsx +87 -1
- package/src/tools/Chat/components/ToolCalls.tsx +21 -3
- package/src/tools/Chat/context/ChatProvider.tsx +21 -3
- package/src/tools/Chat/core/audio/audioBus.ts +10 -163
- package/src/tools/Chat/core/audio/defaults.ts +43 -0
- package/src/tools/Chat/core/audio/index.ts +1 -0
- package/src/tools/Chat/core/audio/preferences.ts +5 -59
- package/src/tools/Chat/core/audio/sounds/error.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/mention.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/notification.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/received.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/sent.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/start.mp3 +0 -0
- package/src/tools/Chat/core/audio/types.ts +28 -0
- package/src/tools/Chat/core/reducer.ts +33 -0
- package/src/tools/Chat/core/transport/index.ts +13 -0
- package/src/tools/Chat/core/transport/mappers/index.ts +6 -0
- package/src/tools/Chat/core/transport/mappers/pydantic-ai.ts +142 -0
- package/src/tools/Chat/core/transport/pydantic-ai-transport.ts +208 -0
- package/src/tools/Chat/core/transport/sse.ts +18 -5
- package/src/tools/Chat/hooks/index.ts +25 -0
- package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +5 -3
- package/src/tools/Chat/hooks/useChat.ts +28 -0
- package/src/tools/Chat/hooks/useChatAudio.ts +59 -180
- package/src/tools/Chat/hooks/useChatDockPrefs.ts +74 -0
- package/src/tools/Chat/hooks/useChatReset.ts +70 -0
- package/src/tools/Chat/hooks/useChatUnread.ts +87 -0
- package/src/tools/Chat/hooks/useFocusOnEmptyClick.ts +111 -0
- package/src/tools/Chat/hooks/useVisitorFingerprint.ts +48 -0
- package/src/tools/Chat/index.ts +84 -1
- package/src/tools/Chat/launcher/ChatDock.tsx +263 -0
- package/src/tools/Chat/launcher/ChatFAB.tsx +349 -0
- package/src/tools/Chat/launcher/ChatGreeting.tsx +200 -0
- package/src/tools/Chat/launcher/ChatHeader.tsx +76 -0
- package/src/tools/Chat/launcher/ChatHeaderActionButton.tsx +87 -0
- package/src/tools/Chat/launcher/ChatHeaderAudioToggle.tsx +47 -0
- package/src/tools/Chat/launcher/ChatHeaderLanguageButton.tsx +179 -0
- package/src/tools/Chat/launcher/ChatHeaderModeToggle.tsx +57 -0
- package/src/tools/Chat/launcher/ChatHeaderResetButton.tsx +93 -0
- package/src/tools/Chat/launcher/ChatLauncher.tsx +321 -0
- package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +197 -0
- package/src/tools/Chat/launcher/index.ts +46 -0
- package/src/tools/Chat/launcher/useChatPresence.ts +44 -0
- package/src/tools/Chat/styles/bubbleTokens.ts +71 -0
- package/src/tools/Chat/styles/index.ts +16 -0
- package/src/tools/Chat/styles/useChatStyles.ts +101 -0
- package/src/tools/Chat/types/attachment.ts +25 -0
- package/src/tools/Chat/types/config.ts +48 -0
- package/src/tools/Chat/types/events.ts +35 -0
- package/src/tools/Chat/types/index.ts +34 -0
- package/src/tools/Chat/types/labels.ts +38 -0
- package/src/tools/Chat/types/message.ts +32 -0
- package/src/tools/Chat/types/persona.ts +31 -0
- package/src/tools/Chat/types/session.ts +43 -0
- package/src/tools/Chat/types/tool-call.ts +17 -0
- package/src/tools/Chat/types/transport.ts +28 -0
- package/src/tools/Chat/types.ts +5 -240
- package/src/tools/MarkdownEditor/MarkdownEditor.tsx +50 -14
- package/src/tools/MarkdownEditor/index.ts +1 -1
- package/src/tools/SpeechRecognition/README.md +336 -0
- package/src/tools/SpeechRecognition/__tests__/ids.test.ts +15 -0
- package/src/tools/SpeechRecognition/__tests__/language.test.ts +59 -0
- package/src/tools/SpeechRecognition/__tests__/reducer.test.ts +71 -0
- package/src/tools/SpeechRecognition/__tests__/transcript.test.ts +52 -0
- package/src/tools/SpeechRecognition/components/DevicePicker.tsx +49 -0
- package/src/tools/SpeechRecognition/components/DictationButton.tsx +93 -0
- package/src/tools/SpeechRecognition/components/EngineBadge.tsx +30 -0
- package/src/tools/SpeechRecognition/components/ErrorBanner.tsx +52 -0
- package/src/tools/SpeechRecognition/components/LanguagePicker.tsx +63 -0
- package/src/tools/SpeechRecognition/components/MicMeter.tsx +63 -0
- package/src/tools/SpeechRecognition/components/PushToTalkHint.tsx +51 -0
- package/src/tools/SpeechRecognition/components/TranscriptView.tsx +55 -0
- package/src/tools/SpeechRecognition/components/index.ts +16 -0
- package/src/tools/SpeechRecognition/context/SpeechRecognitionProvider.tsx +47 -0
- package/src/tools/SpeechRecognition/context/index.ts +6 -0
- package/src/tools/SpeechRecognition/core/audio/defaults.ts +24 -0
- package/src/tools/SpeechRecognition/core/engine/external.ts +222 -0
- package/src/tools/SpeechRecognition/core/engine/http.ts +147 -0
- package/src/tools/SpeechRecognition/core/engine/index.ts +52 -0
- package/src/tools/SpeechRecognition/core/engine/mediarecorder.ts +105 -0
- package/src/tools/SpeechRecognition/core/engine/websocket.ts +211 -0
- package/src/tools/SpeechRecognition/core/engine/webspeech.ts +188 -0
- package/src/tools/SpeechRecognition/core/ids.ts +11 -0
- package/src/tools/SpeechRecognition/core/index.ts +14 -0
- package/src/tools/SpeechRecognition/core/language.ts +78 -0
- package/src/tools/SpeechRecognition/core/languages-catalog.ts +229 -0
- package/src/tools/SpeechRecognition/core/logger.ts +3 -0
- package/src/tools/SpeechRecognition/core/reducer.ts +105 -0
- package/src/tools/SpeechRecognition/core/transcript.ts +36 -0
- package/src/tools/SpeechRecognition/hooks/index.ts +14 -0
- package/src/tools/SpeechRecognition/hooks/useDictation.ts +59 -0
- package/src/tools/SpeechRecognition/hooks/useEnginePrefs.ts +15 -0
- package/src/tools/SpeechRecognition/hooks/useMicDevices.ts +57 -0
- package/src/tools/SpeechRecognition/hooks/useMicLevel.ts +52 -0
- package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +85 -0
- package/src/tools/SpeechRecognition/hooks/useResolvedLanguage.ts +28 -0
- package/src/tools/SpeechRecognition/hooks/useSpeechLanguageInfo.ts +108 -0
- package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +188 -0
- package/src/tools/SpeechRecognition/hooks/useVoiceSupport.ts +78 -0
- package/src/tools/SpeechRecognition/index.ts +82 -0
- package/src/tools/SpeechRecognition/lazy.tsx +19 -0
- package/src/tools/SpeechRecognition/store/index.ts +2 -0
- package/src/tools/SpeechRecognition/store/prefsStore.ts +54 -0
- package/src/tools/SpeechRecognition/types.ts +133 -0
- package/src/tools/SpeechRecognition/widgets/DictationField.tsx +105 -0
- package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +305 -0
- package/src/tools/SpeechRecognition/widgets/VoiceMessageRecorder.tsx +88 -0
- package/src/tools/SpeechRecognition/widgets/index.ts +6 -0
- package/dist/ChatRoot-EJC5Y2YM.cjs +0 -14
- package/dist/ChatRoot-QOSKJPM6.mjs +0 -5
- package/dist/chunk-NWUT327A.mjs.map +0 -1
- package/dist/chunk-QLMKCSR6.mjs +0 -2420
- package/dist/chunk-QLMKCSR6.mjs.map +0 -1
- package/dist/chunk-SI5RD2GD.cjs +0 -2460
- package/dist/chunk-SI5RD2GD.cjs.map +0 -1
- package/dist/chunk-XACCHZH2.cjs.map +0 -1
- package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +0 -771
- package/src/stories/index.ts +0 -33
- package/src/tools/AudioPlayer/AudioPlayer.story.tsx +0 -481
- package/src/tools/Chat/Chat.story.tsx +0 -1457
- package/src/tools/CodeEditor/CodeEditor.story.tsx +0 -202
- package/src/tools/CronScheduler/CronScheduler.story.tsx +0 -300
- package/src/tools/Gallery/Gallery.story.tsx +0 -237
- package/src/tools/ImageViewer/ImageViewer.story.tsx +0 -85
- package/src/tools/JsonForm/JsonForm.story.tsx +0 -350
- package/src/tools/JsonTree/JsonTree.story.tsx +0 -141
- package/src/tools/LottiePlayer/LottiePlayer.story.tsx +0 -95
- package/src/tools/Map/Map.story.tsx +0 -458
- package/src/tools/MarkdownEditor/MarkdownEditor.story.tsx +0 -225
- package/src/tools/Mermaid/Mermaid.story.tsx +0 -251
- package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +0 -230
- package/src/tools/PrettyCode/PrettyCode.story.tsx +0 -304
- package/src/tools/Tour/Tour.story.tsx +0 -279
- package/src/tools/Tree/Tree.story.tsx +0 -620
- package/src/tools/Uploader/Uploader.story.tsx +0 -415
- 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
|
+
}
|
package/src/tools/Chat/index.ts
CHANGED
|
@@ -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
|
+
}
|