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) {
55615561 ref={attachDraftHeroTransitionGroupRef}
55625562 className="chat-composer-horizontal-inset w-full"
55635563 >
5564 <div className="pointer-events-auto relative z-10 isolate">
5564 <div className="pointer-events-auto relative z-10">
55655565 {isDraftHeroState ? (
55665566 <div className="absolute inset-x-0 bottom-full z-0">
55675567 <div
......@@ -5593,7 +5593,7 @@ function ChatViewContent(props: ChatViewProps) {
55935593 }
55945594 >
55955595 <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">
55975597 <ChatComposer
55985598 composerRef={composerRef}
55995599 composerDraftTarget={composerDraftTarget}
apps/web/src/components/chat/ChatComposer.tsx+64-2
......@@ -26,6 +26,7 @@ import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger";
2626import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model";
2727import {
2828 memo,
29 type ReactNode,
2930 useCallback,
3031 useEffect,
3132 useImperativeHandle,
......@@ -34,6 +35,7 @@ import {
3435 useRef,
3536 useState,
3637} from "react";
38import { createPortal } from "react-dom";
3739import {
3840 clampCollapsedComposerCursor,
3941 type ComposerTrigger,
......@@ -93,6 +95,64 @@ import { buildExpandedImagePreview, type ExpandedImagePreview } from "./Expanded
9395import { basenameOfPath } from "../../pierre-icons";
9496import { cn, randomUUID } from "~/lib/utils";
9597import { 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}
96156import { Button } from "../ui/button";
97157import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
98158import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
......@@ -903,6 +963,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
903963 const [isComposerPrimaryActionsCompact, setIsComposerPrimaryActionsCompact] = useState(false);
904964 const [isComposerModelPickerOpen, setIsComposerModelPickerOpen] = useState(false);
905965 const [isComposerFocused, setIsComposerFocused] = useState(false);
966 const [composerMenuAnchor, setComposerMenuAnchor] = useState<HTMLDivElement | null>(null);
906967 const isMobileViewport = useMediaQuery("max-sm");
907968 const isComposerCollapsedMobile =
908969 isMobileViewport && !forceExpandedOnMobile && !isComposerFocused;
......@@ -2337,6 +2398,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
23372398 ) : null}
23382399
23392400 <div
2401 ref={setComposerMenuAnchor}
23402402 className={cn(
23412403 "relative px-3 pb-2 sm:px-4",
23422404 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)
23442406 )}
23452407 >
23462408 {composerMenuOpen && !isComposerApprovalState && (
2347 <div className="absolute inset-x-0 bottom-full z-20 mb-2">
2409 <ComposerCommandMenuLayer anchor={composerMenuAnchor}>
23482410 <ComposerCommandMenu
23492411 items={composerMenuItems}
23502412 resolvedTheme={resolvedTheme}
......@@ -2359,7 +2421,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
23592421 onHighlightedItemChange={onComposerMenuItemHighlighted}
23602422 onSelect={onSelectComposerItem}
23612423 />
2362 </div>
2424 </ComposerCommandMenuLayer>
23632425 )}
23642426
23652427 {!isComposerCollapsedMobile &&
apps/web/src/components/ui/alert-dialog.tsx+9-7
......@@ -3,6 +3,11 @@
33import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
44
55import { 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
712const AlertDialogCreateHandle = AlertDialogPrimitive.createHandle;
813
......@@ -18,10 +23,7 @@ function AlertDialogBackdrop({ className, ...props }: AlertDialogPrimitive.Backd
1823 return (
1924 <AlertDialogPrimitive.Backdrop
2025 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)}
2527 data-slot="alert-dialog-backdrop"
2628 {...props}
2729 />
......@@ -56,9 +58,9 @@ function AlertDialogPopup({
5658 >
5759 <AlertDialogPrimitive.Popup
5860 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,
6264 className,
6365 )}
6466 data-slot="alert-dialog-popup"
apps/web/src/components/ui/command.tsx+4-5
......@@ -15,6 +15,7 @@ import {
1515 AutocompleteList,
1616 AutocompleteSeparator,
1717} from "~/components/ui/autocomplete";
18import { DIALOG_BACKDROP_CLASS, DIALOG_POPUP_CLASS } from "~/components/ui/dialog-styles";
1819
1920const CommandDialog = CommandDialogPrimitive.Root;
2021
......@@ -29,10 +30,7 @@ function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) {
2930function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.Backdrop.Props) {
3031 return (
3132 <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)}
3634 data-slot="command-dialog-backdrop"
3735 {...props}
3836 />
......@@ -66,7 +64,8 @@ function CommandDialogPopup({
6664 <CommandDialogViewport>
6765 <CommandDialogPrimitive.Popup
6866 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",
7069 className,
7170 )}
7271 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";
44import { XIcon } from "lucide-react";
55import { cn } from "~/lib/utils";
66import { 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";
712import { ScrollArea } from "~/components/ui/scroll-area";
813
914const DialogCreateHandle = DialogPrimitive.createHandle;
......@@ -24,10 +29,7 @@ function DialogBackdrop({ className, ...props }: DialogPrimitive.Backdrop.Props)
2429 return (
2530 <DialogPrimitive.Backdrop
2631 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)}
3133 data-slot="dialog-backdrop"
3234 {...props}
3335 />
......@@ -49,27 +51,25 @@ function DialogViewport({ className, ...props }: DialogPrimitive.Viewport.Props)
4951
5052function DialogPopup({
5153 className,
52 backdropClassName,
5354 children,
5455 showCloseButton = true,
5556 bottomStickOnMobile = true,
5657 ...props
5758}: DialogPrimitive.Popup.Props & {
58 backdropClassName?: string;
5959 showCloseButton?: boolean;
6060 bottomStickOnMobile?: boolean;
6161}) {
6262 return (
6363 <DialogPortal>
64 <DialogBackdrop className={backdropClassName} />
64 <DialogBackdrop />
6565 <DialogViewport
6666 className={cn(bottomStickOnMobile && "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12")}
6767 >
6868 <DialogPrimitive.Popup
6969 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,
7373 className,
7474 )}
7575 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
381381 --chat-composer-glass-surface: var(--card);
382382
383383 position: relative;
384 isolation: isolate;
385384 box-shadow:
386385 0 0 0 1px rgb(0 0 0 / 8%),
387386 0 12px 28px -18px rgb(0 0 0 / 40%);
388387 }
389388
390389 .chat-composer-glass-host::before {
390 pointer-events: none;
391391 position: absolute;
392 z-index: -1;
392 z-index: 0;
393393 inset: 0;
394394 border-radius: inherit;
395395 background: color-mix(
......@@ -469,7 +469,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
469469 }
470470
471471 .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);
473473 box-shadow:
474474 inset 0 1px rgb(255 255 255 / 4%),
475475 0 24px 72px -20px rgb(0 0 0 / 90%);