authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-23 14:15:29+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-23 14:15:29+02:00
log4d83436415c0f6020afe84c127b88874b6f0103c
treefa4995e62b2c26cf2496f081a2f5880c266e5db2
parente51538b812392d522947494ae32dacf3f3859f5e
signature Signed by PGP key B5690EEEBB952194

Unify dialog glass and fix composer overlays (#4365)

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,7 +5561,7 @@ function ChatViewContent(props: ChatViewProps) {
5561 ref={attachDraftHeroTransitionGroupRef}5561 ref={attachDraftHeroTransitionGroupRef}
5562 className="chat-composer-horizontal-inset w-full"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 {isDraftHeroState ? (5565 {isDraftHeroState ? (
5566 <div className="absolute inset-x-0 bottom-full z-0">5566 <div className="absolute inset-x-0 bottom-full z-0">
5567 <div5567 <div
...@@ -5593,7 +5593,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -5593,7 +5593,7 @@ function ChatViewContent(props: ChatViewProps) {
5593 }5593 }
5594 >5594 >
5595 <div className="chat-composer-glass-host mx-auto w-full max-w-3xl rounded-[22px]">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 <ChatComposer5597 <ChatComposer
5598 composerRef={composerRef}5598 composerRef={composerRef}
5599 composerDraftTarget={composerDraftTarget}5599 composerDraftTarget={composerDraftTarget}
apps/web/src/components/chat/ChatComposer.tsx+64-2
...@@ -26,6 +26,7 @@ import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger";...@@ -26,6 +26,7 @@ import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger";
26import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model";26import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model";
27import {27import {
28 memo,28 memo,
29 type ReactNode,
29 useCallback,30 useCallback,
30 useEffect,31 useEffect,
31 useImperativeHandle,32 useImperativeHandle,
...@@ -34,6 +35,7 @@ import {...@@ -34,6 +35,7 @@ import {
34 useRef,35 useRef,
35 useState,36 useState,
36} from "react";37} from "react";
38import { createPortal } from "react-dom";
37import {39import {
38 clampCollapsedComposerCursor,40 clampCollapsedComposerCursor,
39 type ComposerTrigger,41 type ComposerTrigger,
...@@ -93,6 +95,64 @@ import { buildExpandedImagePreview, type ExpandedImagePreview } from "./Expanded...@@ -93,6 +95,64 @@ import { buildExpandedImagePreview, type ExpandedImagePreview } from "./Expanded
93import { basenameOfPath } from "../../pierre-icons";95import { basenameOfPath } from "../../pierre-icons";
94import { cn, randomUUID } from "~/lib/utils";96import { cn, randomUUID } from "~/lib/utils";
95import { Separator } from "../ui/separator";97import { Separator } from "../ui/separator";
98
99function 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}
96import { Button } from "../ui/button";156import { Button } from "../ui/button";
97import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";157import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
98import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";158import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
...@@ -903,6 +963,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -903,6 +963,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
903 const [isComposerPrimaryActionsCompact, setIsComposerPrimaryActionsCompact] = useState(false);963 const [isComposerPrimaryActionsCompact, setIsComposerPrimaryActionsCompact] = useState(false);
904 const [isComposerModelPickerOpen, setIsComposerModelPickerOpen] = useState(false);964 const [isComposerModelPickerOpen, setIsComposerModelPickerOpen] = useState(false);
905 const [isComposerFocused, setIsComposerFocused] = useState(false);965 const [isComposerFocused, setIsComposerFocused] = useState(false);
966 const [composerMenuAnchor, setComposerMenuAnchor] = useState<HTMLDivElement | null>(null);
906 const isMobileViewport = useMediaQuery("max-sm");967 const isMobileViewport = useMediaQuery("max-sm");
907 const isComposerCollapsedMobile =968 const isComposerCollapsedMobile =
908 isMobileViewport && !forceExpandedOnMobile && !isComposerFocused;969 isMobileViewport && !forceExpandedOnMobile && !isComposerFocused;
...@@ -2337,6 +2398,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2337,6 +2398,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2337 ) : null}2398 ) : null}
23382399
2339 <div2400 <div
2401 ref={setComposerMenuAnchor}
2340 className={cn(2402 className={cn(
2341 "relative px-3 pb-2 sm:px-4",2403 "relative px-3 pb-2 sm:px-4",
2342 hasComposerHeader ? "pt-2.5 sm:pt-3" : "pt-3.5 sm:pt-4",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,7 +2406,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2344 )}2406 )}
2345 >2407 >
2346 {composerMenuOpen && !isComposerApprovalState && (2408 {composerMenuOpen && !isComposerApprovalState && (
2347 <div className="absolute inset-x-0 bottom-full z-20 mb-2">2409 <ComposerCommandMenuLayer anchor={composerMenuAnchor}>
2348 <ComposerCommandMenu2410 <ComposerCommandMenu
2349 items={composerMenuItems}2411 items={composerMenuItems}
2350 resolvedTheme={resolvedTheme}2412 resolvedTheme={resolvedTheme}
...@@ -2359,7 +2421,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2359,7 +2421,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2359 onHighlightedItemChange={onComposerMenuItemHighlighted}2421 onHighlightedItemChange={onComposerMenuItemHighlighted}
2360 onSelect={onSelectComposerItem}2422 onSelect={onSelectComposerItem}
2361 />2423 />
2362 </div>2424 </ComposerCommandMenuLayer>
2363 )}2425 )}
23642426
2365 {!isComposerCollapsedMobile &&2427 {!isComposerCollapsedMobile &&
apps/web/src/components/ui/alert-dialog.tsx+9-7
...@@ -3,6 +3,11 @@...@@ -3,6 +3,11 @@
3import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";3import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
44
5import { cn } from "~/lib/utils";5import { cn } from "~/lib/utils";
6import {
7 DIALOG_BACKDROP_CLASS,
8 DIALOG_MOBILE_SHEET_CLASS,
9 DIALOG_POPUP_CLASS,
10} from "~/components/ui/dialog-styles";
611
7const AlertDialogCreateHandle = AlertDialogPrimitive.createHandle;12const AlertDialogCreateHandle = AlertDialogPrimitive.createHandle;
813
...@@ -18,10 +23,7 @@ function AlertDialogBackdrop({ className, ...props }: AlertDialogPrimitive.Backd...@@ -18,10 +23,7 @@ function AlertDialogBackdrop({ className, ...props }: AlertDialogPrimitive.Backd
18 return (23 return (
19 <AlertDialogPrimitive.Backdrop24 <AlertDialogPrimitive.Backdrop
20 forceRender25 forceRender
21 className={cn(26 className={cn(DIALOG_BACKDROP_CLASS, className)}
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 )}
25 data-slot="alert-dialog-backdrop"27 data-slot="alert-dialog-backdrop"
26 {...props}28 {...props}
27 />29 />
...@@ -56,9 +58,9 @@ function AlertDialogPopup({...@@ -56,9 +58,9 @@ function AlertDialogPopup({
56 >58 >
57 <AlertDialogPrimitive.Popup59 <AlertDialogPrimitive.Popup
58 className={cn(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",61 DIALOG_POPUP_CLASS,
60 bottomStickOnMobile &&62 "row-start-2 max-h-full max-w-lg text-popover-foreground",
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",63 bottomStickOnMobile && DIALOG_MOBILE_SHEET_CLASS,
62 className,64 className,
63 )}65 )}
64 data-slot="alert-dialog-popup"66 data-slot="alert-dialog-popup"
apps/web/src/components/ui/command.tsx+4-5
...@@ -15,6 +15,7 @@ import {...@@ -15,6 +15,7 @@ import {
15 AutocompleteList,15 AutocompleteList,
16 AutocompleteSeparator,16 AutocompleteSeparator,
17} from "~/components/ui/autocomplete";17} from "~/components/ui/autocomplete";
18import { DIALOG_BACKDROP_CLASS, DIALOG_POPUP_CLASS } from "~/components/ui/dialog-styles";
1819
19const CommandDialog = CommandDialogPrimitive.Root;20const CommandDialog = CommandDialogPrimitive.Root;
2021
...@@ -29,10 +30,7 @@ function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) {...@@ -29,10 +30,7 @@ function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) {
29function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.Backdrop.Props) {30function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.Backdrop.Props) {
30 return (31 return (
31 <CommandDialogPrimitive.Backdrop32 <CommandDialogPrimitive.Backdrop
32 className={cn(33 className={cn(DIALOG_BACKDROP_CLASS, className)}
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 )}
36 data-slot="command-dialog-backdrop"34 data-slot="command-dialog-backdrop"
37 {...props}35 {...props}
38 />36 />
...@@ -66,7 +64,8 @@ function CommandDialogPopup({...@@ -66,7 +64,8 @@ function CommandDialogPopup({
66 <CommandDialogViewport>64 <CommandDialogViewport>
67 <CommandDialogPrimitive.Popup65 <CommandDialogPrimitive.Popup
68 className={cn(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 className,69 className,
71 )}70 )}
72 data-slot="command-dialog-popup"71 data-slot="command-dialog-popup"
apps/web/src/components/ui/dialog-styles.ts created+10
...@@ -0,0 +1,10 @@
1const 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
4const 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
7const 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
10export { 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,6 +4,11 @@ import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
4import { XIcon } from "lucide-react";4import { XIcon } from "lucide-react";
5import { cn } from "~/lib/utils";5import { cn } from "~/lib/utils";
6import { Button } from "~/components/ui/button";6import { Button } from "~/components/ui/button";
7import {
8 DIALOG_BACKDROP_CLASS,
9 DIALOG_MOBILE_SHEET_CLASS,
10 DIALOG_POPUP_CLASS,
11} from "~/components/ui/dialog-styles";
7import { ScrollArea } from "~/components/ui/scroll-area";12import { ScrollArea } from "~/components/ui/scroll-area";
813
9const DialogCreateHandle = DialogPrimitive.createHandle;14const DialogCreateHandle = DialogPrimitive.createHandle;
...@@ -24,10 +29,7 @@ function DialogBackdrop({ className, ...props }: DialogPrimitive.Backdrop.Props)...@@ -24,10 +29,7 @@ function DialogBackdrop({ className, ...props }: DialogPrimitive.Backdrop.Props)
24 return (29 return (
25 <DialogPrimitive.Backdrop30 <DialogPrimitive.Backdrop
26 forceRender31 forceRender
27 className={cn(32 className={cn(DIALOG_BACKDROP_CLASS, className)}
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 )}
31 data-slot="dialog-backdrop"33 data-slot="dialog-backdrop"
32 {...props}34 {...props}
33 />35 />
...@@ -49,27 +51,25 @@ function DialogViewport({ className, ...props }: DialogPrimitive.Viewport.Props)...@@ -49,27 +51,25 @@ function DialogViewport({ className, ...props }: DialogPrimitive.Viewport.Props)
4951
50function DialogPopup({52function DialogPopup({
51 className,53 className,
52 backdropClassName,
53 children,54 children,
54 showCloseButton = true,55 showCloseButton = true,
55 bottomStickOnMobile = true,56 bottomStickOnMobile = true,
56 ...props57 ...props
57}: DialogPrimitive.Popup.Props & {58}: DialogPrimitive.Popup.Props & {
58 backdropClassName?: string;
59 showCloseButton?: boolean;59 showCloseButton?: boolean;
60 bottomStickOnMobile?: boolean;60 bottomStickOnMobile?: boolean;
61}) {61}) {
62 return (62 return (
63 <DialogPortal>63 <DialogPortal>
64 <DialogBackdrop className={backdropClassName} />64 <DialogBackdrop />
65 <DialogViewport65 <DialogViewport
66 className={cn(bottomStickOnMobile && "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12")}66 className={cn(bottomStickOnMobile && "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12")}
67 >67 >
68 <DialogPrimitive.Popup68 <DialogPrimitive.Popup
69 className={cn(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",70 DIALOG_POPUP_CLASS,
71 bottomStickOnMobile &&71 "row-start-2 max-h-full max-w-lg text-popover-foreground",
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",72 bottomStickOnMobile && DIALOG_MOBILE_SHEET_CLASS,
73 className,73 className,
74 )}74 )}
75 data-slot="dialog-popup"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,15 +381,15 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
381 --chat-composer-glass-surface: var(--card);381 --chat-composer-glass-surface: var(--card);
382382
383 position: relative;383 position: relative;
384 isolation: isolate;
385 box-shadow:384 box-shadow:
386 0 0 0 1px rgb(0 0 0 / 8%),385 0 0 0 1px rgb(0 0 0 / 8%),
387 0 12px 28px -18px rgb(0 0 0 / 40%);386 0 12px 28px -18px rgb(0 0 0 / 40%);
388 }387 }
389388
390 .chat-composer-glass-host::before {389 .chat-composer-glass-host::before {
390 pointer-events: none;
391 position: absolute;391 position: absolute;
392 z-index: -1;392 z-index: 0;
393 inset: 0;393 inset: 0;
394 border-radius: inherit;394 border-radius: inherit;
395 background: color-mix(395 background: color-mix(
...@@ -469,7 +469,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -469,7 +469,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
469 }469 }
470470
471 .dark .dialog-glass {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 box-shadow:473 box-shadow:
474 inset 0 1px rgb(255 255 255 / 4%),474 inset 0 1px rgb(255 255 255 / 4%),
475 0 24px 72px -20px rgb(0 0 0 / 90%);475 0 24px 72px -20px rgb(0 0 0 / 90%);