@djangocfg/ui-tools 2.1.407 → 2.1.409
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 +9 -10
- package/dist/file-icon/index.cjs +449 -61
- package/dist/file-icon/index.cjs.map +1 -1
- package/dist/file-icon/index.d.cts +56 -18
- package/dist/file-icon/index.d.ts +56 -18
- package/dist/file-icon/index.mjs +448 -62
- package/dist/file-icon/index.mjs.map +1 -1
- package/dist/tree/index.cjs +49 -22
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.d.cts +9 -3
- package/dist/tree/index.d.ts +9 -3
- package/dist/tree/index.mjs +49 -22
- package/dist/tree/index.mjs.map +1 -1
- package/dist/{types-B_zhyAqR.d.cts → types-eEu8SeiQ.d.cts} +4 -0
- package/dist/{types-B_zhyAqR.d.ts → types-eEu8SeiQ.d.ts} +4 -0
- package/package.json +8 -13
- package/src/components/FloatingToolbar/index.tsx +37 -3
- package/src/lib/page-snapshot/__tests__/capture-integration.test.ts +85 -0
- package/src/lib/page-snapshot/__tests__/engine.test.ts +36 -0
- package/src/lib/page-snapshot/__tests__/redaction-integration.test.ts +99 -0
- package/src/lib/page-snapshot/__tests__/tokens.test.ts +17 -0
- package/src/lib/page-snapshot/capture/__tests__/budget.test.ts +49 -0
- package/src/lib/page-snapshot/capture/__tests__/chrome-filter.test.ts +47 -0
- package/src/lib/page-snapshot/capture/__tests__/fold.test.ts +66 -0
- package/src/lib/page-snapshot/capture/__tests__/scope.test.ts +74 -0
- package/src/lib/page-snapshot/capture/__tests__/walk.test.ts +129 -0
- package/src/lib/page-snapshot/capture/accessible-name.ts +73 -0
- package/src/lib/page-snapshot/capture/budget.ts +95 -0
- package/src/lib/page-snapshot/capture/chrome-filter.ts +81 -0
- package/src/lib/page-snapshot/capture/classify.ts +111 -0
- package/src/lib/page-snapshot/capture/dom-utils.ts +111 -0
- package/src/lib/page-snapshot/capture/fold.ts +96 -0
- package/src/lib/page-snapshot/capture/scope.ts +169 -0
- package/src/lib/page-snapshot/capture/walk.ts +250 -0
- package/src/lib/page-snapshot/cst/__tests__/serialize.test.ts +50 -0
- package/src/lib/page-snapshot/cst/directives.ts +47 -0
- package/src/lib/page-snapshot/cst/payload.ts +50 -0
- package/src/lib/page-snapshot/cst/serialize.ts +84 -0
- package/src/lib/page-snapshot/cst/types.ts +115 -0
- package/src/lib/page-snapshot/engine.ts +176 -0
- package/src/lib/page-snapshot/index.ts +93 -0
- package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +72 -0
- package/src/lib/page-snapshot/react/PageSnapshotPreview.tsx +78 -0
- package/src/lib/page-snapshot/react/__tests__/PageSnapshotChip.test.tsx +54 -0
- package/src/lib/page-snapshot/react/__tests__/provider.test.tsx +103 -0
- package/src/lib/page-snapshot/react/__tests__/use-page-snapshot-toggle.test.tsx +62 -0
- package/src/lib/page-snapshot/react/provider.tsx +162 -0
- package/src/lib/page-snapshot/react/use-page-snapshot-toggle.ts +47 -0
- package/src/lib/page-snapshot/react/use-page-snapshot.ts +67 -0
- package/src/lib/page-snapshot/redaction/__tests__/audit.test.ts +25 -0
- package/src/lib/page-snapshot/redaction/__tests__/heuristics.test.ts +73 -0
- package/src/lib/page-snapshot/redaction/__tests__/luhn.test.ts +26 -0
- package/src/lib/page-snapshot/redaction/__tests__/patterns.test.ts +60 -0
- package/src/lib/page-snapshot/redaction/audit.ts +58 -0
- package/src/lib/page-snapshot/redaction/heuristics.ts +75 -0
- package/src/lib/page-snapshot/redaction/index.ts +75 -0
- package/src/lib/page-snapshot/redaction/luhn.ts +25 -0
- package/src/lib/page-snapshot/redaction/patterns.ts +111 -0
- package/src/lib/page-snapshot/refs/__tests__/registry.test.ts +24 -0
- package/src/lib/page-snapshot/refs/registry.ts +46 -0
- package/src/lib/page-snapshot/staleness/__tests__/hash.test.ts +34 -0
- package/src/lib/page-snapshot/staleness/hash.ts +20 -0
- package/src/lib/page-snapshot/tokens.ts +15 -0
- package/src/tools/AudioPlayer/context/PlayerProvider.tsx +13 -14
- package/src/tools/AudioPlayer/hooks/useAudioElementEvents.ts +55 -6
- package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +2 -5
- package/src/tools/Chat/README.md +277 -39
- package/src/tools/Chat/composer/Composer.tsx +471 -0
- package/src/tools/Chat/composer/ComposerActionBar.tsx +65 -0
- package/src/tools/Chat/composer/ComposerBanner.tsx +128 -0
- package/src/tools/Chat/composer/ComposerButton.tsx +64 -0
- package/src/tools/Chat/composer/ComposerFooter.tsx +90 -0
- package/src/tools/Chat/composer/ComposerMenuButton.tsx +62 -0
- package/src/tools/Chat/composer/ComposerModelPicker.tsx +104 -0
- package/src/tools/Chat/composer/ComposerRichTextarea.tsx +88 -0
- package/src/tools/Chat/composer/ComposerToolPill.tsx +95 -0
- package/src/tools/Chat/composer/index.ts +45 -0
- package/src/tools/Chat/composer/size-context.tsx +26 -0
- package/src/tools/Chat/composer/types.ts +143 -0
- package/src/tools/Chat/composer/useComposerActions.tsx +164 -0
- package/src/tools/Chat/context/ChatProvider.tsx +54 -3
- package/src/tools/Chat/core/__tests__/metadata.test.ts +69 -0
- package/src/tools/Chat/core/index.ts +23 -1
- package/src/tools/Chat/core/markdown.ts +1 -1
- package/src/tools/Chat/core/metadata.ts +47 -0
- package/src/tools/Chat/core/payload-dispatch.ts +1 -1
- package/src/tools/Chat/core/transport/http.ts +71 -32
- package/src/tools/Chat/core/transport/sse.ts +18 -10
- package/src/tools/Chat/highlight/HighlightOverlay.tsx +101 -0
- package/src/tools/Chat/highlight/README.md +103 -0
- package/src/tools/Chat/highlight/SpotlightCanvas.tsx +153 -0
- package/src/tools/Chat/highlight/__tests__/HighlightOverlay.test.tsx +112 -0
- package/src/tools/Chat/highlight/__tests__/resolveRef.test.ts +55 -0
- package/src/tools/Chat/highlight/index.ts +21 -0
- package/src/tools/Chat/highlight/resolveRef.ts +42 -0
- package/src/tools/Chat/highlight/types.ts +49 -0
- package/src/tools/Chat/highlight/useHighlightTargets.ts +128 -0
- package/src/tools/Chat/hooks/index.ts +0 -5
- package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +28 -47
- package/src/tools/Chat/hooks/useChat.ts +47 -14
- package/src/tools/Chat/hooks/useChatComposer.ts +2 -2
- package/src/tools/Chat/hooks/useChatLayout.ts +1 -1
- package/src/tools/Chat/hooks/useStreamEndFocus.ts +54 -0
- package/src/tools/Chat/index.ts +25 -219
- package/src/tools/Chat/launcher/ChatDock.tsx +1 -1
- package/src/tools/Chat/launcher/ChatLauncher.tsx +1 -1
- package/src/tools/Chat/launcher/{ChatHeader.tsx → header/ChatHeader.tsx} +24 -11
- package/src/tools/Chat/launcher/{ChatHeaderActionButton.tsx → header/ChatHeaderActionButton.tsx} +34 -3
- package/src/tools/Chat/launcher/{ChatHeaderLanguageButton.tsx → header/ChatHeaderLanguageButton.tsx} +2 -2
- package/src/tools/Chat/launcher/{ChatHeaderModeToggle.tsx → header/ChatHeaderModeToggle.tsx} +1 -1
- package/src/tools/Chat/launcher/{ChatHeaderResetButton.tsx → header/ChatHeaderResetButton.tsx} +2 -1
- package/src/tools/Chat/launcher/{HeaderSlots.tsx → header/HeaderSlots.tsx} +3 -3
- package/src/tools/Chat/launcher/header/index.ts +26 -0
- package/src/tools/Chat/launcher/index.ts +3 -10
- package/src/tools/Chat/lazy.tsx +38 -284
- package/src/tools/Chat/{components → messages}/MessageBubble.tsx +58 -5
- package/src/tools/Chat/{components → messages}/MessageList.tsx +8 -25
- package/src/tools/Chat/messages/blocks/MessageBlocks.tsx +131 -0
- package/src/tools/Chat/messages/blocks/builtin.tsx +91 -0
- package/src/tools/Chat/messages/blocks/index.ts +12 -0
- package/src/tools/Chat/messages/blocks/registry.tsx +42 -0
- package/src/tools/Chat/messages/blocks/renderers/AudioBlock.tsx +20 -0
- package/src/tools/Chat/messages/blocks/renderers/CodeBlock.tsx +19 -0
- package/src/tools/Chat/messages/blocks/renderers/GalleryBlock.tsx +26 -0
- package/src/tools/Chat/messages/blocks/renderers/ImageBlock.tsx +27 -0
- package/src/tools/Chat/messages/blocks/renderers/JsonBlock.tsx +12 -0
- package/src/tools/Chat/messages/blocks/renderers/LottieBlock.tsx +11 -0
- package/src/tools/Chat/messages/blocks/renderers/MapBlock.tsx +36 -0
- package/src/tools/Chat/messages/blocks/renderers/MermaidBlock.tsx +11 -0
- package/src/tools/Chat/messages/blocks/renderers/VideoBlock.tsx +24 -0
- package/src/tools/Chat/messages/blocks/renderers/types.ts +8 -0
- package/src/tools/Chat/{components → messages}/index.ts +11 -5
- package/src/tools/Chat/public.ts +212 -0
- package/src/tools/Chat/shell/ChatRoot.tsx +345 -0
- package/src/tools/Chat/{components → shell}/EmptyState.tsx +4 -2
- package/src/tools/Chat/shell/index.ts +15 -0
- package/src/tools/Chat/types/block.ts +120 -0
- package/src/tools/Chat/types/config.ts +0 -5
- package/src/tools/Chat/types/index.ts +17 -0
- package/src/tools/Chat/types/message.ts +3 -0
- package/src/tools/Chat/utils/index.ts +4 -0
- package/src/tools/CodeEditor/README.md +4 -6
- package/src/tools/CodeEditor/components/DiffEditor.tsx +48 -13
- package/src/tools/CodeEditor/components/Editor.tsx +96 -44
- package/src/tools/CodeEditor/context/EditorProvider.tsx +34 -17
- package/src/tools/CodeEditor/hooks/useEditorTheme.ts +92 -99
- package/src/tools/CodeEditor/hooks/useMonaco.ts +37 -22
- package/src/tools/CodeEditor/lazy.tsx +6 -0
- package/src/tools/CodeEditor/lib/index.ts +1 -1
- package/src/tools/CodeEditor/lib/themes.ts +3 -39
- package/src/tools/CronScheduler/CronScheduler.client.tsx +230 -61
- package/src/tools/CronScheduler/components/CustomInput.tsx +21 -4
- package/src/tools/CronScheduler/components/DayChips.tsx +13 -11
- package/src/tools/CronScheduler/components/MonthDayGrid.tsx +4 -4
- package/src/tools/CronScheduler/components/SchedulePreview.tsx +7 -3
- package/src/tools/CronScheduler/components/TimeSelector.tsx +1 -1
- package/src/tools/CronScheduler/index.tsx +1 -1
- package/src/tools/CronScheduler/types/index.ts +8 -3
- package/src/tools/CronScheduler/utils/cron-humanize.ts +61 -16
- package/src/tools/CronScheduler/utils/cron-parser.ts +13 -4
- package/src/tools/FileIcon/FileIcon.tsx +24 -39
- package/src/tools/FileIcon/get-file-icon.ts +73 -0
- package/src/tools/FileIcon/icons/icon-data.ts +399 -0
- package/src/tools/FileIcon/index.ts +4 -0
- package/src/tools/FileIcon/loader.ts +17 -35
- package/src/tools/FileIcon/specialFolders.ts +18 -0
- package/src/tools/Gallery/components/lightbox/GalleryLightbox.tsx +112 -35
- package/src/tools/Gallery/components/media/GalleryVideo.tsx +21 -2
- package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +11 -1
- package/src/tools/Gallery/hooks/usePreloadImages.ts +54 -7
- package/src/tools/ImageViewer/components/ImageInfo.tsx +12 -1
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +51 -43
- package/src/tools/ImageViewer/components/ImageViewer.tsx +96 -24
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +13 -0
- package/src/tools/ImageViewer/utils/constants.ts +3 -0
- package/src/tools/ImageViewer/utils/index.ts +1 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +4 -1
- package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +5 -3
- package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +7 -4
- package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +3 -1
- package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +23 -3
- package/src/tools/JsonForm/widgets/ColorWidget.tsx +20 -12
- package/src/tools/JsonForm/widgets/NumberWidget.tsx +14 -9
- package/src/tools/JsonForm/widgets/RadioWidget.tsx +78 -0
- package/src/tools/JsonForm/widgets/SelectWidget.tsx +1 -0
- package/src/tools/JsonForm/widgets/SliderWidget.tsx +7 -4
- package/src/tools/JsonForm/widgets/TextWidget.tsx +41 -17
- package/src/tools/JsonForm/widgets/index.ts +1 -0
- package/src/tools/JsonTree/components/JsonContent.tsx +115 -40
- package/src/tools/LottiePlayer/LottiePlayer.client.tsx +177 -72
- package/src/tools/LottiePlayer/index.tsx +14 -4
- package/src/tools/LottiePlayer/lazy.tsx +11 -3
- package/src/tools/LottiePlayer/types.ts +31 -1
- package/src/tools/LottiePlayer/useLottie.ts +32 -9
- package/src/tools/LottiePlayer/usePrefersReducedMotion.ts +46 -0
- package/src/tools/Map/components/LayerSwitcher.tsx +54 -21
- package/src/tools/Map/components/MapCluster.tsx +28 -21
- package/src/tools/Map/components/MapContainer.tsx +11 -4
- package/src/tools/Map/components/MapLegend.tsx +46 -15
- package/src/tools/Map/components/MapMarker.tsx +31 -2
- package/src/tools/Map/hooks/useMapEvents.ts +64 -105
- package/src/tools/MarkdownEditor/MarkdownEditor.tsx +61 -6
- package/src/tools/MarkdownEditor/MentionList.tsx +37 -4
- package/src/tools/MarkdownEditor/createMentionSuggestion.ts +11 -0
- package/src/tools/MarkdownEditor/lazy.tsx +32 -7
- package/src/tools/MarkdownEditor/styles.css +13 -0
- package/src/tools/MarkdownMessage/CodeBlock.tsx +40 -17
- package/src/tools/MarkdownMessage/MarkdownMessage.tsx +26 -6
- package/src/tools/MarkdownMessage/components.tsx +22 -9
- package/src/tools/MarkdownMessage/types.ts +24 -1
- package/src/tools/Mermaid/Mermaid.client.tsx +27 -5
- package/src/tools/Mermaid/components/MermaidErrorPanel.tsx +31 -0
- package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +14 -17
- package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +264 -168
- package/src/tools/Mermaid/hooks/useMermaidValidation.ts +76 -10
- package/src/tools/Mermaid/index.tsx +6 -0
- package/src/tools/Mermaid/utils/mermaid-helpers.ts +141 -18
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +11 -1
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +49 -20
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +7 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +7 -4
- package/src/tools/OpenapiViewer/constants.ts +3 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +73 -11
- package/src/tools/OpenapiViewer/utils/schemaExport.ts +26 -6
- package/src/tools/PrettyCode/PrettyCode.client.tsx +23 -16
- package/src/tools/PrettyCode/lazy.tsx +1 -1
- package/src/tools/SpeechRecognition/README.md +1 -1
- package/src/tools/SpeechRecognition/__tests__/language.test.ts +9 -3
- package/src/tools/SpeechRecognition/components/RecordingPulse.tsx +59 -0
- package/src/tools/SpeechRecognition/components/index.ts +2 -0
- package/src/tools/SpeechRecognition/core/engine/external.ts +24 -7
- package/src/tools/SpeechRecognition/core/language.ts +23 -6
- package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +36 -5
- package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +18 -11
- package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +94 -26
- package/src/tools/SpeechRecognition/widgets/index.ts +1 -1
- package/src/tools/Tree/README.md +4 -8
- package/src/tools/Tree/TreeRoot.tsx +22 -10
- package/src/tools/Tree/components/TreeContent.tsx +24 -4
- package/src/tools/Tree/components/TreeLabel.tsx +8 -2
- package/src/tools/Tree/components/TreeRow.tsx +16 -6
- package/src/tools/Tree/data/flatten.ts +10 -4
- package/src/tools/Tree/types.ts +4 -0
- package/src/tools/Uploader/components/UploadAddButton.tsx +29 -6
- package/src/tools/Uploader/components/UploadDropzone.tsx +63 -7
- package/src/tools/Uploader/components/UploadPageDropOverlay.tsx +19 -5
- package/src/tools/Uploader/components/UploadPreviewItem.tsx +47 -17
- package/src/tools/Uploader/components/UploadPreviewList.tsx +24 -12
- package/src/tools/Uploader/utils/formatters.ts +8 -3
- package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +1 -0
- package/src/tools/VideoPlayer/canvas/{jsx.d.ts → jsx-augmentation.ts} +12 -19
- package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +1 -0
- package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +1 -0
- package/src/tools/VideoPlayer/parts/fullscreen.tsx +1 -1
- package/src/tools/VideoPlayer/parts/pip.tsx +1 -1
- package/src/tools/VideoPlayer/parts/playback-rate.tsx +1 -1
- package/src/tools/VideoPlayer/parts/seek-bar.tsx +2 -2
- package/src/tools/VideoPlayer/parts/volume.tsx +2 -2
- package/src/tools/index.ts +2 -1
- package/src/tools/Chat/components/AudioToggle.tsx +0 -78
- package/src/tools/Chat/components/ChatRoot.tsx +0 -305
- package/src/tools/Chat/components/Composer.tsx +0 -216
- package/src/tools/Chat/hooks/useChatScroll.ts +0 -145
- package/src/tools/Chat/types.ts +0 -9
- package/src/tools/JsonTree/components/JsonToolbar.tsx +0 -95
- package/src/tools/JsonTree/hooks/useElementCorner.ts +0 -84
- package/src/tools/JsonTree/hooks/useNavbarHeight.ts +0 -83
- package/src/tools/OpenapiViewer/components/DocsLayout/schemaFields.ts +0 -121
- package/src/tools/Tour/README.md +0 -373
- package/src/tools/Tour/components/Tour.tsx +0 -12
- package/src/tools/Tour/components/TourContent.tsx +0 -171
- package/src/tools/Tour/components/TourNavigation.tsx +0 -77
- package/src/tools/Tour/components/TourProgress.tsx +0 -88
- package/src/tools/Tour/components/TourSpotlight.tsx +0 -199
- package/src/tools/Tour/components/index.ts +0 -5
- package/src/tools/Tour/context/TourContext.ts +0 -19
- package/src/tools/Tour/context/TourProvider.tsx +0 -292
- package/src/tools/Tour/context/index.ts +0 -2
- package/src/tools/Tour/hooks/index.ts +0 -3
- package/src/tools/Tour/hooks/useKeyboardNavigation.ts +0 -59
- package/src/tools/Tour/hooks/useStepTarget.ts +0 -121
- package/src/tools/Tour/hooks/useTour.ts +0 -42
- package/src/tools/Tour/index.ts +0 -38
- package/src/tools/Tour/types/index.ts +0 -224
- package/src/tools/Tour/utils/dom.ts +0 -98
- package/src/tools/Tour/utils/index.ts +0 -3
- package/src/tools/Tour/utils/logger.ts +0 -3
- package/src/tools/Tour/utils/scrollIntoView.ts +0 -24
- /package/src/tools/Chat/{config.ts → constants.ts} +0 -0
- /package/src/tools/Chat/launcher/{ChatHeaderAudioToggle.tsx → header/ChatHeaderAudioToggle.tsx} +0 -0
- /package/src/tools/Chat/{components → messages}/Attachments.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/JumpToLatest.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/MessageActions.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/Sources.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/StreamingIndicator.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/ToolCalls.tsx +0 -0
- /package/src/tools/Chat/{components → shell}/ErrorBanner.tsx +0 -0
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { type ReactNode, forwardRef, useEffect, useRef } from 'react';
|
|
4
|
-
import { Paperclip, Send, Square } from 'lucide-react';
|
|
5
|
-
|
|
6
|
-
import { Button, Textarea } from '@djangocfg/ui-core/components';
|
|
7
|
-
import { cn } from '@djangocfg/ui-core/lib';
|
|
8
|
-
|
|
9
|
-
import { useChatContextOptional } from '../context';
|
|
10
|
-
import type { UseChatComposerReturn } from '../hooks/useChatComposer';
|
|
11
|
-
import { Attachments } from './Attachments';
|
|
12
|
-
|
|
13
|
-
export type ComposerSize = 'sm' | 'md' | 'lg';
|
|
14
|
-
|
|
15
|
-
export interface ComposerProps {
|
|
16
|
-
composer: UseChatComposerReturn;
|
|
17
|
-
placeholder?: string;
|
|
18
|
-
disabled?: boolean;
|
|
19
|
-
showAttachmentButton?: boolean;
|
|
20
|
-
onPickFiles?: () => void;
|
|
21
|
-
toolbarStart?: ReactNode;
|
|
22
|
-
toolbarEnd?: ReactNode;
|
|
23
|
-
attachmentTray?: ReactNode;
|
|
24
|
-
className?: string;
|
|
25
|
-
textareaClassName?: string;
|
|
26
|
-
/** Visual size — controls textarea height + button slot size.
|
|
27
|
-
*
|
|
28
|
-
* - ``sm`` — 32px slot, dense compact composer (admin sidebars, etc).
|
|
29
|
-
* - ``md`` — 36px slot, default. Same as the legacy fixed size.
|
|
30
|
-
* - ``lg`` — 48px slot, generous textarea. Use when the chat is
|
|
31
|
-
* the page's primary surface (onboarding, support).
|
|
32
|
-
*/
|
|
33
|
-
size?: ComposerSize;
|
|
34
|
-
/** Show "Stop" button instead of "Send" while streaming. */
|
|
35
|
-
isStreaming?: boolean;
|
|
36
|
-
onCancel?: () => void;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const SIZE_CLASSES: Record<ComposerSize, {
|
|
40
|
-
slot: string;
|
|
41
|
-
button: string;
|
|
42
|
-
iconButton: string;
|
|
43
|
-
textarea: string;
|
|
44
|
-
text: string;
|
|
45
|
-
padding: string;
|
|
46
|
-
containerPadding: string;
|
|
47
|
-
}> = {
|
|
48
|
-
sm: {
|
|
49
|
-
slot: '[&>:not(textarea)]:h-8',
|
|
50
|
-
button: 'h-8 w-8',
|
|
51
|
-
iconButton: 'size-3.5',
|
|
52
|
-
textarea: 'min-h-8 max-h-48 px-3 py-1.5',
|
|
53
|
-
text: 'text-sm',
|
|
54
|
-
padding: 'gap-1.5',
|
|
55
|
-
containerPadding: 'px-2 pt-1.5 pb-[max(0.375rem,env(safe-area-inset-bottom))]',
|
|
56
|
-
},
|
|
57
|
-
md: {
|
|
58
|
-
slot: '[&>:not(textarea)]:h-9',
|
|
59
|
-
button: 'h-9 w-9',
|
|
60
|
-
iconButton: 'size-4',
|
|
61
|
-
textarea: 'min-h-9 max-h-60 px-3.5 py-2',
|
|
62
|
-
text: 'text-base sm:text-sm',
|
|
63
|
-
padding: 'gap-1.5',
|
|
64
|
-
containerPadding: 'px-2.5 pt-2 pb-[max(0.5rem,env(safe-area-inset-bottom))]',
|
|
65
|
-
},
|
|
66
|
-
lg: {
|
|
67
|
-
slot: '[&>:not(textarea)]:h-12',
|
|
68
|
-
button: 'h-12 w-12',
|
|
69
|
-
iconButton: 'size-5',
|
|
70
|
-
textarea: 'min-h-12 max-h-72 px-4 py-3',
|
|
71
|
-
text: 'text-base',
|
|
72
|
-
padding: 'gap-2',
|
|
73
|
-
containerPadding: 'px-3.5 pt-3 pb-[max(0.875rem,env(safe-area-inset-bottom))]',
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Composer(
|
|
78
|
-
{
|
|
79
|
-
composer,
|
|
80
|
-
placeholder = 'Type a message...',
|
|
81
|
-
disabled,
|
|
82
|
-
showAttachmentButton = false,
|
|
83
|
-
onPickFiles,
|
|
84
|
-
toolbarStart,
|
|
85
|
-
toolbarEnd,
|
|
86
|
-
attachmentTray,
|
|
87
|
-
className,
|
|
88
|
-
textareaClassName,
|
|
89
|
-
size = 'md',
|
|
90
|
-
isStreaming: isStreamingProp,
|
|
91
|
-
onCancel: onCancelProp,
|
|
92
|
-
},
|
|
93
|
-
ref,
|
|
94
|
-
) {
|
|
95
|
-
const ctx = useChatContextOptional();
|
|
96
|
-
const isStreaming = isStreamingProp ?? ctx?.isStreaming ?? false;
|
|
97
|
-
const onCancel = onCancelProp ?? ctx?.cancelStream;
|
|
98
|
-
const isDisabled = disabled ?? isStreaming;
|
|
99
|
-
const sz = SIZE_CLASSES[size];
|
|
100
|
-
|
|
101
|
-
// Register the composer's focus() with the chat context so other
|
|
102
|
-
// parts of the tree (e.g. useAutoFocusOnStreamEnd) can drive it
|
|
103
|
-
// imperatively without prop-drilling a ref. No-op when used outside
|
|
104
|
-
// a ChatProvider.
|
|
105
|
-
const register = ctx?.registerComposer;
|
|
106
|
-
const composerFocus = composer.focus;
|
|
107
|
-
const composerSetValue = composer.setValue;
|
|
108
|
-
const textareaRef = composer.textareaRef;
|
|
109
|
-
// `getValue` reads the live ref instead of closing over `composer.value`
|
|
110
|
-
// so we don't need to re-register on every keystroke. Same trick for
|
|
111
|
-
// `setValue` — handler identity stays stable across renders.
|
|
112
|
-
const getValueRef = useRef<() => string>(() => composer.value);
|
|
113
|
-
getValueRef.current = () => composer.value;
|
|
114
|
-
useEffect(() => {
|
|
115
|
-
if (!register) return;
|
|
116
|
-
register({
|
|
117
|
-
focus: composerFocus,
|
|
118
|
-
moveCursorToEnd: () => {
|
|
119
|
-
const el = textareaRef.current;
|
|
120
|
-
if (!el) return;
|
|
121
|
-
const end = el.value.length;
|
|
122
|
-
el.setSelectionRange(end, end);
|
|
123
|
-
},
|
|
124
|
-
getValue: () => getValueRef.current(),
|
|
125
|
-
setValue: composerSetValue,
|
|
126
|
-
});
|
|
127
|
-
return () => register(null);
|
|
128
|
-
}, [register, composerFocus, composerSetValue, textareaRef]);
|
|
129
|
-
|
|
130
|
-
return (
|
|
131
|
-
<div
|
|
132
|
-
ref={ref}
|
|
133
|
-
className={cn(
|
|
134
|
-
'border-t border-border bg-background/95',
|
|
135
|
-
sz.containerPadding,
|
|
136
|
-
className,
|
|
137
|
-
)}
|
|
138
|
-
>
|
|
139
|
-
{composer.attachments.length > 0 ? (
|
|
140
|
-
<div className="mb-1.5">
|
|
141
|
-
{attachmentTray ?? (
|
|
142
|
-
<Attachments
|
|
143
|
-
attachments={composer.attachments}
|
|
144
|
-
onRemove={(a) => composer.removeAttachment(a.id)}
|
|
145
|
-
/>
|
|
146
|
-
)}
|
|
147
|
-
</div>
|
|
148
|
-
) : null}
|
|
149
|
-
|
|
150
|
-
{/* Size-aware slot row. ``[&>:not(textarea)]:h-{N}`` enforces a
|
|
151
|
-
* consistent slot height so toolbar buttons line up with the
|
|
152
|
-
* textarea baseline. Toolbar slots that want to opt out can
|
|
153
|
-
* pass an explicit class like `!h-auto`. */}
|
|
154
|
-
<div className={cn('flex items-end [&>:not(textarea)]:shrink-0', sz.padding, sz.slot)}>
|
|
155
|
-
{showAttachmentButton ? (
|
|
156
|
-
<Button
|
|
157
|
-
type="button"
|
|
158
|
-
variant="ghost"
|
|
159
|
-
size="icon"
|
|
160
|
-
onClick={onPickFiles}
|
|
161
|
-
aria-label="Attach files"
|
|
162
|
-
disabled={isDisabled}
|
|
163
|
-
className={sz.button}
|
|
164
|
-
>
|
|
165
|
-
<Paperclip aria-hidden className={sz.iconButton} />
|
|
166
|
-
</Button>
|
|
167
|
-
) : null}
|
|
168
|
-
|
|
169
|
-
{toolbarStart}
|
|
170
|
-
|
|
171
|
-
<Textarea
|
|
172
|
-
{...composer.textareaProps}
|
|
173
|
-
rows={1}
|
|
174
|
-
placeholder={placeholder}
|
|
175
|
-
aria-label={placeholder}
|
|
176
|
-
aria-multiline="true"
|
|
177
|
-
disabled={isDisabled}
|
|
178
|
-
className={cn(
|
|
179
|
-
'flex-1 resize-none rounded-2xl',
|
|
180
|
-
sz.textarea,
|
|
181
|
-
sz.text,
|
|
182
|
-
textareaClassName,
|
|
183
|
-
)}
|
|
184
|
-
/>
|
|
185
|
-
|
|
186
|
-
{toolbarEnd}
|
|
187
|
-
|
|
188
|
-
{isStreaming ? (
|
|
189
|
-
<Button
|
|
190
|
-
type="button"
|
|
191
|
-
variant="secondary"
|
|
192
|
-
size="icon"
|
|
193
|
-
onClick={onCancel}
|
|
194
|
-
aria-label="Stop"
|
|
195
|
-
aria-keyshortcuts="Escape"
|
|
196
|
-
className={sz.button}
|
|
197
|
-
>
|
|
198
|
-
<Square aria-hidden className={sz.iconButton} />
|
|
199
|
-
</Button>
|
|
200
|
-
) : (
|
|
201
|
-
<Button
|
|
202
|
-
type="button"
|
|
203
|
-
size="icon"
|
|
204
|
-
onClick={() => void composer.submit()}
|
|
205
|
-
disabled={!composer.canSubmit}
|
|
206
|
-
aria-label="Send"
|
|
207
|
-
aria-keyshortcuts="Enter"
|
|
208
|
-
className={sz.button}
|
|
209
|
-
>
|
|
210
|
-
<Send aria-hidden className={sz.iconButton} />
|
|
211
|
-
</Button>
|
|
212
|
-
)}
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
);
|
|
216
|
-
});
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { type RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @deprecated Plan64. As of ui-tools 2.1.369, `<MessageList>` is
|
|
7
|
-
* virtualized via react-virtuoso and owns its own scroll viewport.
|
|
8
|
-
* Sticky-bottom + auto-follow on streaming live inside the component;
|
|
9
|
-
* use the new `MessageListProps.onAtBottomChange` prop and the
|
|
10
|
-
* `MessageListHandle.scrollToBottom()` imperative method instead.
|
|
11
|
-
*
|
|
12
|
-
* This hook is kept for hosts that render messages outside
|
|
13
|
-
* `<MessageList>` (e.g. headless story shells, custom non-virtualized
|
|
14
|
-
* scroll containers). It still works against any HTMLElement scroll
|
|
15
|
-
* container, but it does NOT integrate with Virtuoso — passing a
|
|
16
|
-
* Virtuoso-managed element here will read garbage scroll metrics.
|
|
17
|
-
*/
|
|
18
|
-
export interface UseChatScrollOptions {
|
|
19
|
-
containerRef: RefObject<HTMLElement | null>;
|
|
20
|
-
bottomRef: RefObject<HTMLElement | null>;
|
|
21
|
-
isStreaming?: boolean;
|
|
22
|
-
/** Distance from bottom (px) considered "at bottom". */
|
|
23
|
-
bottomThresholdPx?: number;
|
|
24
|
-
/** Bump key — increment when a new message arrives so the hook re-evaluates auto-scroll. */
|
|
25
|
-
messagesCount?: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface UseChatScrollReturn {
|
|
29
|
-
isAtBottom: boolean;
|
|
30
|
-
unreadCount: number;
|
|
31
|
-
scrollToBottom: (smooth?: boolean) => void;
|
|
32
|
-
resetUnread: () => void;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function useChatScroll(options: UseChatScrollOptions): UseChatScrollReturn {
|
|
36
|
-
const {
|
|
37
|
-
containerRef,
|
|
38
|
-
bottomRef,
|
|
39
|
-
isStreaming = false,
|
|
40
|
-
bottomThresholdPx = 80,
|
|
41
|
-
messagesCount = 0,
|
|
42
|
-
} = options;
|
|
43
|
-
|
|
44
|
-
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
45
|
-
const [unreadCount, setUnreadCount] = useState(0);
|
|
46
|
-
const lastCountRef = useRef(messagesCount);
|
|
47
|
-
const stickyRef = useRef(true);
|
|
48
|
-
const wasStreamingRef = useRef(isStreaming);
|
|
49
|
-
|
|
50
|
-
const scrollToBottom = useCallback(
|
|
51
|
-
(smooth = false) => {
|
|
52
|
-
const el = containerRef.current;
|
|
53
|
-
if (!el) return;
|
|
54
|
-
el.scrollTo({
|
|
55
|
-
top: el.scrollHeight,
|
|
56
|
-
behavior: smooth ? 'smooth' : 'auto',
|
|
57
|
-
});
|
|
58
|
-
stickyRef.current = true;
|
|
59
|
-
setIsAtBottom(true);
|
|
60
|
-
setUnreadCount(0);
|
|
61
|
-
},
|
|
62
|
-
[containerRef],
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
const resetUnread = useCallback(() => setUnreadCount(0), []);
|
|
66
|
-
|
|
67
|
-
// Track scroll position relative to bottom.
|
|
68
|
-
useEffect(() => {
|
|
69
|
-
const el = containerRef.current;
|
|
70
|
-
if (!el) return;
|
|
71
|
-
const onScroll = () => {
|
|
72
|
-
const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
|
|
73
|
-
const atBottom = distance <= bottomThresholdPx;
|
|
74
|
-
stickyRef.current = atBottom;
|
|
75
|
-
setIsAtBottom(atBottom);
|
|
76
|
-
if (atBottom) setUnreadCount(0);
|
|
77
|
-
};
|
|
78
|
-
onScroll();
|
|
79
|
-
el.addEventListener('scroll', onScroll, { passive: true });
|
|
80
|
-
return () => {
|
|
81
|
-
el.removeEventListener('scroll', onScroll);
|
|
82
|
-
};
|
|
83
|
-
}, [containerRef, bottomThresholdPx]);
|
|
84
|
-
|
|
85
|
-
// Stick to bottom while streaming, and one extra rAF after stream ends so
|
|
86
|
-
// the final layout (markdown re-render, sources/tool-call panels) doesn't
|
|
87
|
-
// push the latest content out of view.
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
const el = containerRef.current;
|
|
90
|
-
if (!el) return;
|
|
91
|
-
|
|
92
|
-
if (isStreaming) {
|
|
93
|
-
wasStreamingRef.current = true;
|
|
94
|
-
if (!stickyRef.current) return;
|
|
95
|
-
let raf = 0;
|
|
96
|
-
const tick = () => {
|
|
97
|
-
if (!stickyRef.current) return;
|
|
98
|
-
el.scrollTop = el.scrollHeight;
|
|
99
|
-
raf = requestAnimationFrame(tick);
|
|
100
|
-
};
|
|
101
|
-
raf = requestAnimationFrame(tick);
|
|
102
|
-
return () => cancelAnimationFrame(raf);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Stream just ended — flush one more scroll on the next two frames so
|
|
106
|
-
// both markdown swap and any post-stream panel insertions are reflected.
|
|
107
|
-
if (wasStreamingRef.current && stickyRef.current) {
|
|
108
|
-
wasStreamingRef.current = false;
|
|
109
|
-
let raf1 = 0;
|
|
110
|
-
let raf2 = 0;
|
|
111
|
-
raf1 = requestAnimationFrame(() => {
|
|
112
|
-
el.scrollTop = el.scrollHeight;
|
|
113
|
-
raf2 = requestAnimationFrame(() => {
|
|
114
|
-
el.scrollTop = el.scrollHeight;
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
return () => {
|
|
118
|
-
cancelAnimationFrame(raf1);
|
|
119
|
-
cancelAnimationFrame(raf2);
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
wasStreamingRef.current = false;
|
|
123
|
-
return;
|
|
124
|
-
}, [containerRef, isStreaming]);
|
|
125
|
-
|
|
126
|
-
// On message count increase, decide whether to scroll or bump unread.
|
|
127
|
-
useEffect(() => {
|
|
128
|
-
if (messagesCount > lastCountRef.current) {
|
|
129
|
-
if (stickyRef.current) {
|
|
130
|
-
const el = containerRef.current;
|
|
131
|
-
if (el) el.scrollTop = el.scrollHeight;
|
|
132
|
-
} else {
|
|
133
|
-
setUnreadCount((n) => n + (messagesCount - lastCountRef.current));
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
lastCountRef.current = messagesCount;
|
|
137
|
-
}, [containerRef, messagesCount]);
|
|
138
|
-
|
|
139
|
-
// Watch bottom sentinel just for symmetry/future hooks.
|
|
140
|
-
useEffect(() => {
|
|
141
|
-
void bottomRef;
|
|
142
|
-
}, [bottomRef]);
|
|
143
|
-
|
|
144
|
-
return { isAtBottom, unreadCount, scrollToBottom, resetUnread };
|
|
145
|
-
}
|
package/src/tools/Chat/types.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Backwards-compatibility re-export.
|
|
3
|
-
*
|
|
4
|
-
* Types live in `./types/` (one file per domain). Import directly from
|
|
5
|
-
* `@djangocfg/ui-tools/Chat` in new code; this barrel exists so old
|
|
6
|
-
* `import … from '.../types'` paths keep working.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export * from './types/index';
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { ChevronDown, ChevronUp, Download } from 'lucide-react';
|
|
4
|
-
import React, { memo } from 'react';
|
|
5
|
-
|
|
6
|
-
import { Button, CopyButton } from '@djangocfg/ui-core/components';
|
|
7
|
-
|
|
8
|
-
interface JsonToolbarProps {
|
|
9
|
-
/** Viewport right coordinate (distance from right edge) for fixed positioning */
|
|
10
|
-
right: number;
|
|
11
|
-
/** Bottom edge of the container in viewport coords */
|
|
12
|
-
bottom: number;
|
|
13
|
-
/** Top edge — used only to detect if block is fully off-screen */
|
|
14
|
-
top: number;
|
|
15
|
-
isExpanded: boolean;
|
|
16
|
-
onToggleExpand: () => void;
|
|
17
|
-
jsonString: string;
|
|
18
|
-
onDownload: () => void;
|
|
19
|
-
showExpandControls: boolean;
|
|
20
|
-
showActionButtons: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const BUTTON_CLASS = 'h-6 w-6 rounded-sm bg-muted/80 hover:bg-muted border border-border/50 backdrop-blur-sm';
|
|
24
|
-
const TOOLBAR_H = 24 + 8 + 8;
|
|
25
|
-
const OFFSET = 8;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* JsonToolbar — floating toolbar for JsonTree.
|
|
29
|
-
*
|
|
30
|
-
* Memoised: re-renders only when any prop changes. Position props
|
|
31
|
-
* (`top`, `right`, `bottom`) are primitives, so default shallow
|
|
32
|
-
* comparison is sufficient.
|
|
33
|
-
*/
|
|
34
|
-
const JsonToolbar = memo(({
|
|
35
|
-
top,
|
|
36
|
-
right,
|
|
37
|
-
bottom,
|
|
38
|
-
isExpanded,
|
|
39
|
-
onToggleExpand,
|
|
40
|
-
jsonString,
|
|
41
|
-
onDownload,
|
|
42
|
-
showExpandControls,
|
|
43
|
-
showActionButtons,
|
|
44
|
-
}: JsonToolbarProps) => {
|
|
45
|
-
const viewportHeight = window.visualViewport?.height ?? document.documentElement.clientHeight;
|
|
46
|
-
|
|
47
|
-
// Hide when block is fully above or below viewport
|
|
48
|
-
if (bottom <= 0 || top >= viewportHeight) return null;
|
|
49
|
-
// Hide when block is too small to fit toolbar
|
|
50
|
-
if (bottom - top < TOOLBAR_H) return null;
|
|
51
|
-
|
|
52
|
-
// Anchor to bottom-right of block, clamped so toolbar stays in viewport
|
|
53
|
-
const toolbarBottom = Math.min(bottom - OFFSET, viewportHeight - OFFSET);
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<div
|
|
57
|
-
className="flex items-center gap-1"
|
|
58
|
-
style={{ position: 'fixed', bottom: viewportHeight - toolbarBottom, right: right + OFFSET, zIndex: 30 }}
|
|
59
|
-
>
|
|
60
|
-
{showExpandControls && (
|
|
61
|
-
<Button
|
|
62
|
-
variant="ghost"
|
|
63
|
-
size="icon"
|
|
64
|
-
onClick={onToggleExpand}
|
|
65
|
-
className={BUTTON_CLASS}
|
|
66
|
-
title={isExpanded ? 'Collapse All' : 'Expand All'}
|
|
67
|
-
>
|
|
68
|
-
{isExpanded ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
|
69
|
-
</Button>
|
|
70
|
-
)}
|
|
71
|
-
|
|
72
|
-
{showActionButtons && (
|
|
73
|
-
<>
|
|
74
|
-
<CopyButton
|
|
75
|
-
value={jsonString}
|
|
76
|
-
variant="ghost"
|
|
77
|
-
size="icon"
|
|
78
|
-
className={BUTTON_CLASS}
|
|
79
|
-
iconClassName="h-3 w-3"
|
|
80
|
-
title="Copy JSON"
|
|
81
|
-
/>
|
|
82
|
-
<Button
|
|
83
|
-
variant="ghost"
|
|
84
|
-
size="icon"
|
|
85
|
-
onClick={onDownload}
|
|
86
|
-
className={BUTTON_CLASS}
|
|
87
|
-
title="Download JSON"
|
|
88
|
-
>
|
|
89
|
-
<Download className="h-3 w-3" />
|
|
90
|
-
</Button>
|
|
91
|
-
</>
|
|
92
|
-
)}
|
|
93
|
-
</div>
|
|
94
|
-
);
|
|
95
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef, useState } from 'react';
|
|
4
|
-
|
|
5
|
-
interface Corner {
|
|
6
|
-
top: number;
|
|
7
|
-
right: number;
|
|
8
|
-
bottom: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Tracks the top-right corner of a referenced element in viewport coordinates.
|
|
13
|
-
* Returns { top, right } in px — ready for `position: fixed` toolbar placement.
|
|
14
|
-
*
|
|
15
|
-
* Uses `visualViewport` for accurate viewport width — correctly handles:
|
|
16
|
-
* - DevTools panel open (docked left/right/bottom)
|
|
17
|
-
* - Browser zoom
|
|
18
|
-
* - Mobile virtual keyboard / pinch-zoom
|
|
19
|
-
* - Scrollbars
|
|
20
|
-
*
|
|
21
|
-
* Falls back to `document.documentElement.clientWidth` (excludes scrollbars,
|
|
22
|
-
* matches `position: fixed` coordinate space) when visualViewport is unavailable.
|
|
23
|
-
*
|
|
24
|
-
* Updates on:
|
|
25
|
-
* - Any ancestor scroll (capture phase)
|
|
26
|
-
* - visualViewport resize/scroll
|
|
27
|
-
* - ResizeObserver on the element itself
|
|
28
|
-
*/
|
|
29
|
-
export function useElementCorner(ref: React.RefObject<HTMLElement | null>) {
|
|
30
|
-
const [corner, setCorner] = useState<Corner | null>(null);
|
|
31
|
-
const updateRef = useRef<() => void>(() => {});
|
|
32
|
-
|
|
33
|
-
updateRef.current = () => {
|
|
34
|
-
if (!ref.current) return;
|
|
35
|
-
const rect = ref.current.getBoundingClientRect();
|
|
36
|
-
|
|
37
|
-
// `position: fixed` is relative to the layout viewport (document.documentElement.clientWidth),
|
|
38
|
-
// NOT window.innerWidth (which includes scrollbar gutter).
|
|
39
|
-
// visualViewport.width is the visible area — same as layout viewport when no zoom/keyboard.
|
|
40
|
-
const viewportWidth =
|
|
41
|
-
window.visualViewport?.width ?? document.documentElement.clientWidth;
|
|
42
|
-
|
|
43
|
-
setCorner({
|
|
44
|
-
top: rect.top,
|
|
45
|
-
right: viewportWidth - rect.right,
|
|
46
|
-
bottom: rect.bottom,
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
const handler = () => updateRef.current();
|
|
52
|
-
|
|
53
|
-
handler();
|
|
54
|
-
|
|
55
|
-
// Element size changes
|
|
56
|
-
const ro = new ResizeObserver(handler);
|
|
57
|
-
if (ref.current) ro.observe(ref.current);
|
|
58
|
-
|
|
59
|
-
// Any ancestor scroll (capture catches all, including overflow:auto containers)
|
|
60
|
-
window.addEventListener('scroll', handler, { capture: true, passive: true });
|
|
61
|
-
|
|
62
|
-
// visualViewport handles: DevTools resize, browser zoom, mobile keyboard
|
|
63
|
-
const vv = window.visualViewport;
|
|
64
|
-
if (vv) {
|
|
65
|
-
vv.addEventListener('resize', handler);
|
|
66
|
-
vv.addEventListener('scroll', handler);
|
|
67
|
-
} else {
|
|
68
|
-
window.addEventListener('resize', handler, { passive: true });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return () => {
|
|
72
|
-
ro.disconnect();
|
|
73
|
-
window.removeEventListener('scroll', handler, { capture: true });
|
|
74
|
-
if (vv) {
|
|
75
|
-
vv.removeEventListener('resize', handler);
|
|
76
|
-
vv.removeEventListener('scroll', handler);
|
|
77
|
-
} else {
|
|
78
|
-
window.removeEventListener('resize', handler);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
}, [ref]);
|
|
82
|
-
|
|
83
|
-
return corner;
|
|
84
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef, useState } from 'react';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Detects the height of any element covering the top of the viewport.
|
|
7
|
-
*
|
|
8
|
-
* Strategy: uses elementFromPoint() along the top of the viewport to find
|
|
9
|
-
* whatever is actually rendered there — fixed, sticky, or in-flow elements.
|
|
10
|
-
* Walks up the DOM from the hit element to find the topmost covering block.
|
|
11
|
-
*
|
|
12
|
-
* Performance:
|
|
13
|
-
* - Samples a few x-points across the viewport top (not the whole width)
|
|
14
|
-
* - Throttled with requestAnimationFrame
|
|
15
|
-
* - Only re-runs on resize (navbars rarely change height on scroll)
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const SAMPLE_POINTS = 5; // how many x positions to probe across viewport
|
|
19
|
-
|
|
20
|
-
function measureNavbarHeight(): number {
|
|
21
|
-
const vv = window.visualViewport;
|
|
22
|
-
const viewportWidth = vv?.width ?? document.documentElement.clientWidth;
|
|
23
|
-
|
|
24
|
-
let maxBottom = 0;
|
|
25
|
-
|
|
26
|
-
for (let i = 0; i < SAMPLE_POINTS; i++) {
|
|
27
|
-
const x = (viewportWidth / (SAMPLE_POINTS + 1)) * (i + 1);
|
|
28
|
-
|
|
29
|
-
// Probe just below the very top — y=1 to avoid hitting browser chrome
|
|
30
|
-
const el = document.elementFromPoint(x, 1);
|
|
31
|
-
if (!el || el === document.documentElement || el === document.body) continue;
|
|
32
|
-
|
|
33
|
-
const rect = el.getBoundingClientRect();
|
|
34
|
-
// Only count elements that are anchored at the very top (top ≤ 2px)
|
|
35
|
-
if (rect.top <= 2 && rect.bottom > maxBottom) {
|
|
36
|
-
maxBottom = rect.bottom;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return maxBottom;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function useNavbarHeight(): number {
|
|
44
|
-
const [height, setHeight] = useState(0);
|
|
45
|
-
const rafRef = useRef<number | null>(null);
|
|
46
|
-
|
|
47
|
-
const scheduleUpdate = () => {
|
|
48
|
-
if (rafRef.current !== null) return;
|
|
49
|
-
rafRef.current = requestAnimationFrame(() => {
|
|
50
|
-
rafRef.current = null;
|
|
51
|
-
setHeight(measureNavbarHeight());
|
|
52
|
-
});
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
// Initial measure after first paint
|
|
57
|
-
scheduleUpdate();
|
|
58
|
-
|
|
59
|
-
// Navbars can change height on resize (mobile keyboard, orientation)
|
|
60
|
-
const vv = window.visualViewport;
|
|
61
|
-
if (vv) {
|
|
62
|
-
vv.addEventListener('resize', scheduleUpdate);
|
|
63
|
-
} else {
|
|
64
|
-
window.addEventListener('resize', scheduleUpdate, { passive: true });
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Watch for navbar being added/removed from DOM
|
|
68
|
-
const mo = new MutationObserver(scheduleUpdate);
|
|
69
|
-
mo.observe(document.body, { childList: true, subtree: false });
|
|
70
|
-
|
|
71
|
-
return () => {
|
|
72
|
-
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
|
73
|
-
if (vv) {
|
|
74
|
-
vv.removeEventListener('resize', scheduleUpdate);
|
|
75
|
-
} else {
|
|
76
|
-
window.removeEventListener('resize', scheduleUpdate);
|
|
77
|
-
}
|
|
78
|
-
mo.disconnect();
|
|
79
|
-
};
|
|
80
|
-
}, []);
|
|
81
|
-
|
|
82
|
-
return height;
|
|
83
|
-
}
|