| author | |
| committer | |
| log | 4d83436415c0f6020afe84c127b88874b6f0103c |
| tree | fa4995e62b2c26cf2496f081a2f5880c266e5db2 |
| parent | e51538b812392d522947494ae32dacf3f3859f5e |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>7 files changed, 102 insertions(+), 29 deletions(-)
apps/web/src/components/ChatView.tsx+2-2| ... | ... | @@ -5561,7 +5561,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 5561 | 5561 | ref={attachDraftHeroTransitionGroupRef} |
| 5562 | 5562 | className="chat-composer-horizontal-inset w-full" |
| 5563 | 5563 | > |
| 5564 | <div className="pointer-events-auto relative z-10 isolate"> | |
| 5564 | <div className="pointer-events-auto relative z-10"> | |
| 5565 | 5565 | {isDraftHeroState ? ( |
| 5566 | 5566 | <div className="absolute inset-x-0 bottom-full z-0"> |
| 5567 | 5567 | <div |
| ... | ... | @@ -5593,7 +5593,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 5593 | 5593 | } |
| 5594 | 5594 | > |
| 5595 | 5595 | <div className="chat-composer-glass-host mx-auto w-full max-w-3xl rounded-[22px]"> |
| 5596 | <div ref={attachDraftHeroComposerAnchorRef} className="relative z-10 isolate"> | |
| 5596 | <div ref={attachDraftHeroComposerAnchorRef} className="relative z-10"> | |
| 5597 | 5597 | <ChatComposer |
| 5598 | 5598 | composerRef={composerRef} |
| 5599 | 5599 | composerDraftTarget={composerDraftTarget} |
apps/web/src/components/chat/ChatComposer.tsx+64-2| ... | ... | @@ -26,6 +26,7 @@ import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger"; |
| 26 | 26 | import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model"; |
| 27 | 27 | import { |
| 28 | 28 | memo, |
| 29 | type ReactNode, | |
| 29 | 30 | useCallback, |
| 30 | 31 | useEffect, |
| 31 | 32 | useImperativeHandle, |
| ... | ... | @@ -34,6 +35,7 @@ import { |
| 34 | 35 | useRef, |
| 35 | 36 | useState, |
| 36 | 37 | } from "react"; |
| 38 | import { createPortal } from "react-dom"; | |
| 37 | 39 | import { |
| 38 | 40 | clampCollapsedComposerCursor, |
| 39 | 41 | type ComposerTrigger, |
| ... | ... | @@ -93,6 +95,64 @@ import { buildExpandedImagePreview, type ExpandedImagePreview } from "./Expanded |
| 93 | 95 | import { basenameOfPath } from "../../pierre-icons"; |
| 94 | 96 | import { cn, randomUUID } from "~/lib/utils"; |
| 95 | 97 | import { Separator } from "../ui/separator"; |
| 98 | ||
| 99 | function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: ReactNode }) { | |
| 100 | const [position, setPosition] = useState<{ | |
| 101 | bottom: number; | |
| 102 | left: number; | |
| 103 | maxHeight: number; | |
| 104 | width: number; | |
| 105 | } | null>(null); | |
| 106 | ||
| 107 | useLayoutEffect(() => { | |
| 108 | const anchor = props.anchor; | |
| 109 | if (!anchor) { | |
| 110 | setPosition(null); | |
| 111 | return; | |
| 112 | } | |
| 113 | ||
| 114 | const updatePosition = () => { | |
| 115 | const rect = anchor.getBoundingClientRect(); | |
| 116 | setPosition({ | |
| 117 | bottom: window.innerHeight - rect.top + 8, | |
| 118 | left: rect.left, | |
| 119 | maxHeight: Math.max(96, rect.top - 24), | |
| 120 | width: rect.width, | |
| 121 | }); | |
| 122 | }; | |
| 123 | ||
| 124 | updatePosition(); | |
| 125 | window.addEventListener("resize", updatePosition); | |
| 126 | window.addEventListener("scroll", updatePosition, true); | |
| 127 | ||
| 128 | const observer = | |
| 129 | typeof ResizeObserver === "undefined" ? null : new ResizeObserver(updatePosition); | |
| 130 | observer?.observe(anchor); | |
| 131 | ||
| 132 | return () => { | |
| 133 | observer?.disconnect(); | |
| 134 | window.removeEventListener("resize", updatePosition); | |
| 135 | window.removeEventListener("scroll", updatePosition, true); | |
| 136 | }; | |
| 137 | }, [props.anchor]); | |
| 138 | ||
| 139 | if (!position) return null; | |
| 140 | ||
| 141 | return createPortal( | |
| 142 | <div | |
| 143 | className="pointer-events-auto fixed z-[70]" | |
| 144 | style={{ | |
| 145 | bottom: position.bottom, | |
| 146 | left: position.left, | |
| 147 | maxHeight: position.maxHeight, | |
| 148 | width: position.width, | |
| 149 | }} | |
| 150 | > | |
| 151 | {props.children} | |
| 152 | </div>, | |
| 153 | document.body, | |
| 154 | ); | |
| 155 | } | |
| 96 | 156 | import { Button } from "../ui/button"; |
| 97 | 157 | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; |
| 98 | 158 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| ... | ... | @@ -903,6 +963,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 903 | 963 | const [isComposerPrimaryActionsCompact, setIsComposerPrimaryActionsCompact] = useState(false); |
| 904 | 964 | const [isComposerModelPickerOpen, setIsComposerModelPickerOpen] = useState(false); |
| 905 | 965 | const [isComposerFocused, setIsComposerFocused] = useState(false); |
| 966 | const [composerMenuAnchor, setComposerMenuAnchor] = useState<HTMLDivElement | null>(null); | |
| 906 | 967 | const isMobileViewport = useMediaQuery("max-sm"); |
| 907 | 968 | const isComposerCollapsedMobile = |
| 908 | 969 | isMobileViewport && !forceExpandedOnMobile && !isComposerFocused; |
| ... | ... | @@ -2337,6 +2398,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2337 | 2398 | ) : null} |
| 2338 | 2399 | |
| 2339 | 2400 | <div |
| 2401 | ref={setComposerMenuAnchor} | |
| 2340 | 2402 | className={cn( |
| 2341 | 2403 | "relative px-3 pb-2 sm:px-4", |
| 2342 | 2404 | hasComposerHeader ? "pt-2.5 sm:pt-3" : "pt-3.5 sm:pt-4", |
| ... | ... | @@ -2344,7 +2406,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2344 | 2406 | )} |
| 2345 | 2407 | > |
| 2346 | 2408 | {composerMenuOpen && !isComposerApprovalState && ( |
| 2347 | <div className="absolute inset-x-0 bottom-full z-20 mb-2"> | |
| 2409 | <ComposerCommandMenuLayer anchor={composerMenuAnchor}> | |
| 2348 | 2410 | <ComposerCommandMenu |
| 2349 | 2411 | items={composerMenuItems} |
| 2350 | 2412 | resolvedTheme={resolvedTheme} |
| ... | ... | @@ -2359,7 +2421,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2359 | 2421 | onHighlightedItemChange={onComposerMenuItemHighlighted} |
| 2360 | 2422 | onSelect={onSelectComposerItem} |
| 2361 | 2423 | /> |
| 2362 | </div> | |
| 2424 | </ComposerCommandMenuLayer> | |
| 2363 | 2425 | )} |
| 2364 | 2426 | |
| 2365 | 2427 | {!isComposerCollapsedMobile && |
apps/web/src/components/ui/alert-dialog.tsx+9-7| ... | ... | @@ -3,6 +3,11 @@ |
| 3 | 3 | import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"; |
| 4 | 4 | |
| 5 | 5 | import { cn } from "~/lib/utils"; |
| 6 | import { | |
| 7 | DIALOG_BACKDROP_CLASS, | |
| 8 | DIALOG_MOBILE_SHEET_CLASS, | |
| 9 | DIALOG_POPUP_CLASS, | |
| 10 | } from "~/components/ui/dialog-styles"; | |
| 6 | 11 | |
| 7 | 12 | const AlertDialogCreateHandle = AlertDialogPrimitive.createHandle; |
| 8 | 13 | |
| ... | ... | @@ -18,10 +23,7 @@ function AlertDialogBackdrop({ className, ...props }: AlertDialogPrimitive.Backd |
| 18 | 23 | return ( |
| 19 | 24 | <AlertDialogPrimitive.Backdrop |
| 20 | 25 | forceRender |
| 21 | className={cn( | |
| 22 | "dialog-backdrop fixed inset-0 z-50 transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0", | |
| 23 | className, | |
| 24 | )} | |
| 26 | className={cn(DIALOG_BACKDROP_CLASS, className)} | |
| 25 | 27 | data-slot="alert-dialog-backdrop" |
| 26 | 28 | {...props} |
| 27 | 29 | /> |
| ... | ... | @@ -56,9 +58,9 @@ function AlertDialogPopup({ |
| 56 | 58 | > |
| 57 | 59 | <AlertDialogPrimitive.Popup |
| 58 | 60 | className={cn( |
| 59 | "dialog-glass -translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-full min-h-0 w-full min-w-0 max-w-lg scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] outline-none transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0", | |
| 60 | bottomStickOnMobile && | |
| 61 | "max-sm:max-w-none max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4", | |
| 61 | DIALOG_POPUP_CLASS, | |
| 62 | "row-start-2 max-h-full max-w-lg text-popover-foreground", | |
| 63 | bottomStickOnMobile && DIALOG_MOBILE_SHEET_CLASS, | |
| 62 | 64 | className, |
| 63 | 65 | )} |
| 64 | 66 | data-slot="alert-dialog-popup" |
apps/web/src/components/ui/command.tsx+4-5| ... | ... | @@ -15,6 +15,7 @@ import { |
| 15 | 15 | AutocompleteList, |
| 16 | 16 | AutocompleteSeparator, |
| 17 | 17 | } from "~/components/ui/autocomplete"; |
| 18 | import { DIALOG_BACKDROP_CLASS, DIALOG_POPUP_CLASS } from "~/components/ui/dialog-styles"; | |
| 18 | 19 | |
| 19 | 20 | const CommandDialog = CommandDialogPrimitive.Root; |
| 20 | 21 | |
| ... | ... | @@ -29,10 +30,7 @@ function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) { |
| 29 | 30 | function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.Backdrop.Props) { |
| 30 | 31 | return ( |
| 31 | 32 | <CommandDialogPrimitive.Backdrop |
| 32 | className={cn( | |
| 33 | "dialog-backdrop fixed inset-0 z-50 transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0", | |
| 34 | className, | |
| 35 | )} | |
| 33 | className={cn(DIALOG_BACKDROP_CLASS, className)} | |
| 36 | 34 | data-slot="command-dialog-backdrop" |
| 37 | 35 | {...props} |
| 38 | 36 | /> |
| ... | ... | @@ -66,7 +64,8 @@ function CommandDialogPopup({ |
| 66 | 64 | <CommandDialogViewport> |
| 67 | 65 | <CommandDialogPrimitive.Popup |
| 68 | 66 | className={cn( |
| 69 | "dialog-glass pointer-events-auto -translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-105 min-h-0 w-full min-w-0 max-w-xl scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border text-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] outline-none transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 **:data-[slot=scroll-area-viewport]:data-has-overflow-y:pe-1", | |
| 67 | DIALOG_POPUP_CLASS, | |
| 68 | "pointer-events-auto max-h-105 max-w-xl text-foreground **:data-[slot=scroll-area-viewport]:data-has-overflow-y:pe-1", | |
| 70 | 69 | className, |
| 71 | 70 | )} |
| 72 | 71 | data-slot="command-dialog-popup" |
apps/web/src/components/ui/dialog-styles.ts created+10| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | const DIALOG_BACKDROP_CLASS = | |
| 2 | "dialog-backdrop fixed inset-0 z-50 transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0"; | |
| 3 | ||
| 4 | const DIALOG_POPUP_CLASS = | |
| 5 | "dialog-glass -translate-y-[calc(1.25rem*var(--nested-dialogs))] relative flex min-h-0 w-full min-w-0 scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border opacity-[calc(1-0.1*var(--nested-dialogs))] outline-none transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0"; | |
| 6 | ||
| 7 | const DIALOG_MOBILE_SHEET_CLASS = | |
| 8 | "max-sm:max-w-none max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4"; | |
| 9 | ||
| 10 | export { DIALOG_BACKDROP_CLASS, DIALOG_MOBILE_SHEET_CLASS, DIALOG_POPUP_CLASS }; |
apps/web/src/components/ui/dialog.tsx+10-10| ... | ... | @@ -4,6 +4,11 @@ import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"; |
| 4 | 4 | import { XIcon } from "lucide-react"; |
| 5 | 5 | import { cn } from "~/lib/utils"; |
| 6 | 6 | import { Button } from "~/components/ui/button"; |
| 7 | import { | |
| 8 | DIALOG_BACKDROP_CLASS, | |
| 9 | DIALOG_MOBILE_SHEET_CLASS, | |
| 10 | DIALOG_POPUP_CLASS, | |
| 11 | } from "~/components/ui/dialog-styles"; | |
| 7 | 12 | import { ScrollArea } from "~/components/ui/scroll-area"; |
| 8 | 13 | |
| 9 | 14 | const DialogCreateHandle = DialogPrimitive.createHandle; |
| ... | ... | @@ -24,10 +29,7 @@ function DialogBackdrop({ className, ...props }: DialogPrimitive.Backdrop.Props) |
| 24 | 29 | return ( |
| 25 | 30 | <DialogPrimitive.Backdrop |
| 26 | 31 | forceRender |
| 27 | className={cn( | |
| 28 | "dialog-backdrop fixed inset-0 z-50 transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0", | |
| 29 | className, | |
| 30 | )} | |
| 32 | className={cn(DIALOG_BACKDROP_CLASS, className)} | |
| 31 | 33 | data-slot="dialog-backdrop" |
| 32 | 34 | {...props} |
| 33 | 35 | /> |
| ... | ... | @@ -49,27 +51,25 @@ function DialogViewport({ className, ...props }: DialogPrimitive.Viewport.Props) |
| 49 | 51 | |
| 50 | 52 | function DialogPopup({ |
| 51 | 53 | className, |
| 52 | backdropClassName, | |
| 53 | 54 | children, |
| 54 | 55 | showCloseButton = true, |
| 55 | 56 | bottomStickOnMobile = true, |
| 56 | 57 | ...props |
| 57 | 58 | }: DialogPrimitive.Popup.Props & { |
| 58 | backdropClassName?: string; | |
| 59 | 59 | showCloseButton?: boolean; |
| 60 | 60 | bottomStickOnMobile?: boolean; |
| 61 | 61 | }) { |
| 62 | 62 | return ( |
| 63 | 63 | <DialogPortal> |
| 64 | <DialogBackdrop className={backdropClassName} /> | |
| 64 | <DialogBackdrop /> | |
| 65 | 65 | <DialogViewport |
| 66 | 66 | className={cn(bottomStickOnMobile && "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12")} |
| 67 | 67 | > |
| 68 | 68 | <DialogPrimitive.Popup |
| 69 | 69 | className={cn( |
| 70 | "dialog-glass -translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-full min-h-0 w-full min-w-0 max-w-lg scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] outline-none transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0", | |
| 71 | bottomStickOnMobile && | |
| 72 | "max-sm:max-w-none max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4", | |
| 70 | DIALOG_POPUP_CLASS, | |
| 71 | "row-start-2 max-h-full max-w-lg text-popover-foreground", | |
| 72 | bottomStickOnMobile && DIALOG_MOBILE_SHEET_CLASS, | |
| 73 | 73 | className, |
| 74 | 74 | )} |
| 75 | 75 | data-slot="dialog-popup" |
apps/web/src/index.css+3-3| ... | ... | @@ -381,15 +381,15 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 381 | 381 | --chat-composer-glass-surface: var(--card); |
| 382 | 382 | |
| 383 | 383 | position: relative; |
| 384 | isolation: isolate; | |
| 385 | 384 | box-shadow: |
| 386 | 385 | 0 0 0 1px rgb(0 0 0 / 8%), |
| 387 | 386 | 0 12px 28px -18px rgb(0 0 0 / 40%); |
| 388 | 387 | } |
| 389 | 388 | |
| 390 | 389 | .chat-composer-glass-host::before { |
| 390 | pointer-events: none; | |
| 391 | 391 | position: absolute; |
| 392 | z-index: -1; | |
| 392 | z-index: 0; | |
| 393 | 393 | inset: 0; |
| 394 | 394 | border-radius: inherit; |
| 395 | 395 | background: color-mix( |
| ... | ... | @@ -469,7 +469,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 469 | 469 | } |
| 470 | 470 | |
| 471 | 471 | .dark .dialog-glass { |
| 472 | border-color: color-mix(in srgb, var(--color-white) 14%, transparent); | |
| 472 | border-color: color-mix(in srgb, var(--color-white) 8%, transparent); | |
| 473 | 473 | box-shadow: |
| 474 | 474 | inset 0 1px rgb(255 255 255 / 4%), |
| 475 | 475 | 0 24px 72px -20px rgb(0 0 0 / 90%); |