diff --git a/.macroscope/check-run-agents/ui-consistency.md b/.macroscope/check-run-agents/ui-consistency.md new file mode 100644 index 000000000000..8ec720742759 --- /dev/null +++ b/.macroscope/check-run-agents/ui-consistency.md @@ -0,0 +1,82 @@ +--- +title: UI Consistency +model: claude-opus-5 +effort: high +input: full_diff +tools: + - browse_code + - git_tools + - github_api_read_only + - modify_pr +include: + - "apps/web/src/**/*.ts" + - "apps/web/src/**/*.tsx" + - "apps/web/src/**/*.css" +conclusion: failure +showToolCalls: true +--- + +# UI consistency review + +Review changed web UI code and directly affected call sites for consistency with the shared component system, Tailwind ownership, and the behavioral constraints below. Apply these rules when a pull request creates, moves, or modifies controls or styling. Do not demand unrelated repository-wide cleanup. + +The goal is not to minimize CSS or class counts at any cost. The goal is to put each behavior in the smallest correct owner while preserving interaction, theming, accessibility, layout, and browser behavior. + +## Shared controls and variants + +- Prefer the core UI primitives in `apps/web/src/components/ui` over native controls or locally reconstructed primitives. In ordinary product UI, a raw ` +
{result._tag === "Success" ? ( @@ -417,14 +419,14 @@ function ExpandedWorkflowSection({ {settled}/{members.length} settled - +
{scriptOpen && canShowScript ? ( diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx index 05ed533acbc2..b3c1c08eb730 100644 --- a/apps/web/src/components/BranchToolbarBranchSelector.tsx +++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx @@ -51,6 +51,7 @@ import { } from "./ThreadStatusIndicators"; import { Button } from "./ui/button"; import { Switch } from "./ui/switch"; +import { getVirtualizedScrollFadeClassName } from "./ui/scroll-area"; import { Combobox, ComboboxEmpty, @@ -814,9 +815,11 @@ export function BranchToolbarBranchSelector({ maybeFetchNextBranchPage(); }} className={cn( - "scrollbar-gutter-stable overflow-x-hidden overscroll-y-contain ps-1 pe-0 pt-2 pb-1 [--fade-size:1.5rem]", - showTopBranchScrollFade && "mask-t-from-[calc(100%-var(--fade-size))]", - showBottomBranchScrollFade && "mask-b-from-[calc(100%-var(--fade-size))]", + "scrollbar-gutter-stable overflow-x-hidden overscroll-y-contain ps-1 pe-0 pt-2 pb-1", + getVirtualizedScrollFadeClassName({ + top: showTopBranchScrollFade, + bottom: showBottomBranchScrollFade, + }), )} style={{ maxHeight: "14rem" }} /> diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index e9390ed0a8aa..53b043f3a8ff 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -449,7 +449,7 @@ function MarkdownTable({ children, ...props }: React.ComponentProps<"table">) { {children} -
+
-
- +
+ (); const MarkdownLinkFavicon = memo(function MarkdownLinkFavicon({ host }: { host: string }) { const [failedHost, setFailedHost] = useState(null); return ( - + {failedHost === host || failedFaviconHosts.has(host) ? ( ) : ( @@ -1044,7 +1047,7 @@ function MarkdownExternalLinkContent({ const leadingLength = leadingExternalLinkTextLength(plainText); return ( <> - + {plainText.slice(0, leadingLength)} @@ -1060,7 +1063,7 @@ function MarkdownExternalLinkContent({ const leadingLength = leadingExternalLinkTextLength(firstChild); return ( <> - + {firstChild.slice(0, leadingLength)} @@ -1072,7 +1075,7 @@ function MarkdownExternalLinkContent({ return ( <> - + {firstChild} @@ -1289,7 +1292,7 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ side="top" className="max-w-[min(40rem,calc(100vw-2rem))] font-mono text-[11px] leading-tight" > -
+
{displayPath}
@@ -1699,7 +1702,7 @@ function ChatMarkdown({ return (
{rightPanelOpen && !shouldUseRightPanelSheet ? ( @@ -6273,16 +6274,17 @@ function ChatViewContent(props: ChatViewProps) { className="pointer-events-none absolute left-1/2 z-30 flex -translate-x-1/2 justify-center py-1.5" style={{ bottom: composerOverlayHeight + 4 }} > - +
)}
@@ -6299,7 +6301,7 @@ function ChatViewContent(props: ChatViewProps) { >
{isDraftHeroState ? ( diff --git a/apps/web/src/components/ComposerPromptEditor.tsx b/apps/web/src/components/ComposerPromptEditor.tsx index 0489e8c79cdf..f6dfef2489b4 100644 --- a/apps/web/src/components/ComposerPromptEditor.tsx +++ b/apps/web/src/components/ComposerPromptEditor.tsx @@ -71,6 +71,7 @@ import { import { cn, isMacPlatform } from "~/lib/utils"; import { basenameOfPath } from "~/pierre-icons"; import { + COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME, COMPOSER_INLINE_CHIP_ICON_CLASS_NAME, COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME, COMPOSER_INLINE_SKILL_CHIP_LABEL_CLASS_NAME, @@ -188,7 +189,7 @@ class ComposerMentionNode extends DecoratorNode { override createDOM(): HTMLElement { const dom = document.createElement("span"); - dom.className = "composer-inline-chip relative inline-flex align-[-0.125em] leading-none"; + dom.className = COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME; return dom; } @@ -326,7 +327,7 @@ class ComposerSkillNode extends DecoratorNode { override createDOM(): HTMLElement { const dom = document.createElement("span"); - dom.className = "composer-inline-chip relative inline-flex align-[-0.125em] leading-none"; + dom.className = COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME; return dom; } @@ -397,7 +398,7 @@ class ComposerTerminalContextNode extends DecoratorNode { override createDOM(): HTMLElement { const dom = document.createElement("span"); - dom.className = "composer-inline-chip relative inline-flex align-[-0.125em] leading-none"; + dom.className = COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME; return dom; } @@ -1747,13 +1748,12 @@ function ComposerPromptEditorInner({ return ( -
+
Appearance - // can drive it; keep everything else here. + // The wrapper owns the appearance preference; keep everything else here. "block max-h-50 min-h-17.5 w-full overflow-y-auto whitespace-pre-wrap wrap-break-word bg-transparent leading-relaxed text-foreground focus:outline-none", className, )} diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index 385d67b6b701..b929d05a719b 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -825,7 +825,7 @@ export default function DiffPanel({
) : ( <> -
+
{isSelectedPatchTruncated && (

This diff was truncated because it exceeded the preview limit. The changes shown are @@ -907,10 +907,11 @@ export default function DiffPanel({ } diff --git a/apps/web/src/components/NoActiveThreadState.tsx b/apps/web/src/components/NoActiveThreadState.tsx index 68a5855c1a28..82dddd8f41e0 100644 --- a/apps/web/src/components/NoActiveThreadState.tsx +++ b/apps/web/src/components/NoActiveThreadState.tsx @@ -11,7 +11,9 @@ export function NoActiveThreadState() {

diff --git a/apps/web/src/components/RightPanelTabs.tsx b/apps/web/src/components/RightPanelTabs.tsx index df65aa60d520..b91e81bc7a0b 100644 --- a/apps/web/src/components/RightPanelTabs.tsx +++ b/apps/web/src/components/RightPanelTabs.tsx @@ -26,6 +26,7 @@ import type { DesktopPreviewOverlay } from "~/previewStateStore"; import type { RightPanelSurface } from "~/rightPanelStore"; import { cn } from "~/lib/utils"; import { readLocalApi } from "~/localApi"; +import { Button } from "~/components/ui/button"; import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip"; import { Kbd } from "~/components/ui/kbd"; import { Menu, MenuItem, MenuPopup, MenuTrigger } from "~/components/ui/menu"; @@ -603,7 +604,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) { >
0 ? ( + } > diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index dc8be07dcad1..2f0c5a221405 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -3298,9 +3298,9 @@ export default function Sidebar() { {isSearchingThreads ? ( + ); })} diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index 87f0ed4ae706..1266e5ed7e94 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -31,6 +31,7 @@ import { useState, } from "react"; import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover"; +import { Button } from "~/components/ui/button"; import { writeTextToClipboard } from "~/hooks/useCopyToClipboard"; import { cn } from "~/lib/utils"; import { type TerminalContextSelection } from "~/lib/terminalContext"; @@ -1273,13 +1274,9 @@ export default function ThreadTerminalDrawer({ ) : null}

No terminal sessions for this thread yet.

- +
); diff --git a/apps/web/src/components/chat/ComposerCommandMenu.tsx b/apps/web/src/components/chat/ComposerCommandMenu.tsx index 3ed2a9432e48..4f32211c10cc 100644 --- a/apps/web/src/components/chat/ComposerCommandMenu.tsx +++ b/apps/web/src/components/chat/ComposerCommandMenu.tsx @@ -141,7 +141,7 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { >
{props.items.length > 0 ? ( diff --git a/apps/web/src/components/chat/ComposerPreviewAnnotationCards.test.tsx b/apps/web/src/components/chat/ComposerPreviewAnnotationCards.test.tsx index 5bb28054e7d7..46af299073c5 100644 --- a/apps/web/src/components/chat/ComposerPreviewAnnotationCards.test.tsx +++ b/apps/web/src/components/chat/ComposerPreviewAnnotationCards.test.tsx @@ -43,4 +43,18 @@ describe("ComposerPreviewAnnotationCards", () => { expect(markup).not.toContain("localhost:3000"); expect(markup).not.toContain("Preview annotation"); }); + + it("uses the shared button contract for removal", () => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain('aria-label="Remove preview annotation"'); + expect(markup).toContain('data-slot="button"'); + }); }); diff --git a/apps/web/src/components/chat/ComposerPreviewAnnotationCards.tsx b/apps/web/src/components/chat/ComposerPreviewAnnotationCards.tsx index 5e9e43dcf21e..19f6a5ca308e 100644 --- a/apps/web/src/components/chat/ComposerPreviewAnnotationCards.tsx +++ b/apps/web/src/components/chat/ComposerPreviewAnnotationCards.tsx @@ -5,6 +5,7 @@ import type { ReactNode } from "react"; import type { ComposerImageAttachment } from "~/composerDraftStore"; import { formatElementContextLabel, normalizeElementContextSelection } from "~/lib/elementContext"; import { cn } from "~/lib/utils"; +import { Button } from "../ui/button"; interface ComposerPreviewAnnotationCardsProps { annotations: ReadonlyArray; @@ -128,14 +129,15 @@ export function ComposerPreviewAnnotationCards({
- + ); })} diff --git a/apps/web/src/components/chat/ComposerStashBadge.tsx b/apps/web/src/components/chat/ComposerStashBadge.tsx index 79ed301a5d56..a2599ebc9f9f 100644 --- a/apps/web/src/components/chat/ComposerStashBadge.tsx +++ b/apps/web/src/components/chat/ComposerStashBadge.tsx @@ -46,7 +46,7 @@ export const ComposerStashBadge = memo(function ComposerStashBadge(props: { className={cn( "rounded-full px-1.5 text-[10px] font-medium tabular-nums", props.pulsing - ? "prompt-stash-count-enter bg-primary text-primary-foreground" + ? "animate-[prompt-stash-count-enter_180ms_ease-out_both] bg-primary text-primary-foreground motion-reduce:animate-none" : "bg-muted text-muted-foreground", )} > diff --git a/apps/web/src/components/chat/ComposerStashMenu.tsx b/apps/web/src/components/chat/ComposerStashMenu.tsx index 9e9238515332..fc8be327da18 100644 --- a/apps/web/src/components/chat/ComposerStashMenu.tsx +++ b/apps/web/src/components/chat/ComposerStashMenu.tsx @@ -91,7 +91,7 @@ export const ComposerStashMenu = memo(function ComposerStashMenu(props: { return ( -
+
diff --git a/apps/web/src/components/chat/ContextWindowMeter.tsx b/apps/web/src/components/chat/ContextWindowMeter.tsx index 752ec4013768..f377c893ae2c 100644 --- a/apps/web/src/components/chat/ContextWindowMeter.tsx +++ b/apps/web/src/components/chat/ContextWindowMeter.tsx @@ -1,4 +1,4 @@ -import { cn } from "~/lib/utils"; +import { Button } from "../ui/button"; import { type ContextWindowSnapshot, formatContextWindowTokens } from "~/lib/contextWindow"; import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; @@ -36,13 +36,10 @@ export function ContextWindowMeter(props: { delay={150} closeDelay={0} render={ - + } /> { ); expect(compactMarkup).toContain('class="h-3 sm:h-4"'); - expect(compactMarkup).not.toContain("chat-timeline-scroll-fade"); + expect(compactMarkup).not.toContain("topbar-scroll-fade"); expect(fadedMarkup).toContain('class="h-10 sm:h-12"'); - expect(fadedMarkup).toContain("chat-timeline-scroll-fade"); + expect(fadedMarkup).toContain("topbar-scroll-fade"); }); it("keeps assistant changed-files headers sticky below the thread header", () => { diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 9cd1c78aa4d5..e190f47569b2 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -591,7 +591,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ onScroll={handleScroll} className={cn( "scrollbar-gutter-both h-full min-h-0 overflow-x-hidden overscroll-y-contain px-3 [overflow-anchor:none] sm:px-5", - topFadeEnabled && "chat-timeline-scroll-fade", + topFadeEnabled && "topbar-scroll-fade", )} ListHeaderComponent={ loadEarlier !== null ? ( diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index 7c86ec630141..7ffb2bf077da 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -33,6 +33,7 @@ import { } from "../../keybindings"; import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings"; import { cn } from "~/lib/utils"; +import { getVirtualizedScrollFadeClassName } from "../ui/scroll-area"; import { TooltipProvider } from "../ui/tooltip"; import { isProviderInstancePickerReady, @@ -598,7 +599,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { return (
{/* Sidebar */} @@ -781,9 +782,11 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { onLayout={updateModelListScrollFades} onScroll={updateModelListScrollFades} className={cn( - "model-picker-list scrollbar-gutter-stable h-full overflow-x-hidden overscroll-y-contain py-1.5 [--fade-size:1.5rem]", - showTopScrollFade && "model-picker-list-scroll-fade-top", - showBottomScrollFade && "model-picker-list-scroll-fade-bottom", + "scrollbar-gutter-stable h-full overflow-x-hidden overscroll-y-contain py-1.5 [&::-webkit-scrollbar-track]:my-2", + getVirtualizedScrollFadeClassName({ + top: showTopScrollFade, + bottom: showBottomScrollFade, + }), )} /> diff --git a/apps/web/src/components/chat/ProviderStatusBanner.tsx b/apps/web/src/components/chat/ProviderStatusBanner.tsx index f82c17b13ddd..1c7571b962f9 100644 --- a/apps/web/src/components/chat/ProviderStatusBanner.tsx +++ b/apps/web/src/components/chat/ProviderStatusBanner.tsx @@ -2,6 +2,7 @@ import { type ServerProvider } from "@t3tools/contracts"; import { memo } from "react"; import { InfoIcon, XIcon } from "lucide-react"; import { cn } from "~/lib/utils"; +import { Button } from "../ui/button"; import { formatProviderDriverKindLabel } from "../../providerModels"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; @@ -66,14 +67,15 @@ export const ProviderStatusBanner = memo(function ProviderStatusBanner({
- +
); diff --git a/apps/web/src/components/composerInlineChip.ts b/apps/web/src/components/composerInlineChip.ts index c17b3ddab3c0..3f0e8ca1ac00 100644 --- a/apps/web/src/components/composerInlineChip.ts +++ b/apps/web/src/components/composerInlineChip.ts @@ -8,6 +8,9 @@ export const CHAT_INLINE_CHIP_CLASS_NAME = `${INLINE_CHIP_CLASS_NAME} text-[12px export const COMPOSER_INLINE_CHIP_CLASS_NAME = `${INLINE_CHIP_CLASS_NAME} text-[0.86em] select-none`; +export const COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME = + "relative inline-flex align-[-0.125em] leading-none data-[composer-chip-selected]:after:pointer-events-none data-[composer-chip-selected]:after:absolute data-[composer-chip-selected]:after:inset-0 data-[composer-chip-selected]:after:rounded-[6px] data-[composer-chip-selected]:after:bg-[Highlight] data-[composer-chip-selected]:after:opacity-30 data-[composer-chip-selected]:after:content-['']"; + export const COMPOSER_INLINE_CHIP_ICON_CLASS_NAME = "size-[1.17em] shrink-0 opacity-85"; export const CHAT_INLINE_CHIP_LABEL_CLASS_NAME = "truncate leading-tight"; diff --git a/apps/web/src/components/diffs/StyledDiffCodeView.test.tsx b/apps/web/src/components/diffs/StyledDiffCodeView.test.tsx index dbdd10d194f6..f0cd49abc41d 100644 --- a/apps/web/src/components/diffs/StyledDiffCodeView.test.tsx +++ b/apps/web/src/components/diffs/StyledDiffCodeView.test.tsx @@ -35,7 +35,9 @@ describe("StyledDiffCodeView", () => { />, ); - expect(testState.codeViewClassName).toBe("diff-render-surface outline-none min-h-0"); + expect(testState.codeViewClassName).toBe( + "diff-render-surface [--code-background:var(--background)] outline-none min-h-0", + ); expect(testState.codeViewOptions).toMatchObject({ theme: "pierre-dark", stickyHeaders: true, diff --git a/apps/web/src/components/diffs/StyledDiffCodeView.tsx b/apps/web/src/components/diffs/StyledDiffCodeView.tsx index 7dbd5358a0ff..14939de09820 100644 --- a/apps/web/src/components/diffs/StyledDiffCodeView.tsx +++ b/apps/web/src/components/diffs/StyledDiffCodeView.tsx @@ -292,8 +292,8 @@ export function StyledDiffCodeView({ // outside the panel clipping boundary; actual controls inside retain their own indicators. className={ className - ? `diff-render-surface outline-none ${className}` - : "diff-render-surface outline-none" + ? `diff-render-surface [--code-background:var(--background)] outline-none ${className}` + : "diff-render-surface [--code-background:var(--background)] outline-none" } options={{ ...options, diff --git a/apps/web/src/components/files/FileBrowserPanel.tsx b/apps/web/src/components/files/FileBrowserPanel.tsx index ff658693a70c..e3280c99caa3 100644 --- a/apps/web/src/components/files/FileBrowserPanel.tsx +++ b/apps/web/src/components/files/FileBrowserPanel.tsx @@ -78,7 +78,7 @@ function FileSearchField(props: { value: string; }) { return ( - + -
+
{relativePath ? ( -
+
-
+
- + { event.stopPropagation(); toggleFile(item.id); @@ -685,7 +684,7 @@ export function PullRequestCodeTab({ ) : ( )} - + ); }, [toggleFile], @@ -899,7 +898,7 @@ export function PullRequestCodeTab({ review.verdicts.length === 0 ? null : (
{reviewOpen ? ( -
+
+ )}
); @@ -959,7 +959,7 @@ export function PullRequestCodeTab({ * diff API offers it. */ const toolbar = ( -
+
{/* A host that reports no commits has nothing to scope by, and a dropdown whose only entry is the scope already showing is a control that does nothing. */} diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx index 7237b4357481..2f4e84dc3fd2 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx @@ -1137,8 +1137,14 @@ export function PullRequestDetailPanel({ <> + } > diff --git a/apps/web/src/components/pullRequest/PullRequestListFilters.tsx b/apps/web/src/components/pullRequest/PullRequestListFilters.tsx index dd4a9d161cdc..3066eafc38a1 100644 --- a/apps/web/src/components/pullRequest/PullRequestListFilters.tsx +++ b/apps/web/src/components/pullRequest/PullRequestListFilters.tsx @@ -24,6 +24,7 @@ import type { ElementType } from "react"; import { cn } from "~/lib/utils"; import { getSourceControlPresentationForKind } from "~/sourceControlPresentation"; import { ProjectFavicon } from "../ProjectFavicon"; +import { InputGroup, InputGroupAddon, InputGroupInput } from "../ui/input-group"; import { Menu, @@ -79,29 +80,18 @@ export function PullRequestSearchInput({ onChange: (value: string) => void; }) { return ( -
- {busy ? ( - - ) : ( - - )} - + + {busy ? : } + + onChange(event.currentTarget.value)} placeholder="Search pull requests, or label:bug" aria-label="Search pull requests" - // Tracks the shared input's height at both widths, so it stays level with the icon - // button beside it rather than towering over it on wide screens. - className="h-9 w-full rounded-lg border border-input bg-background pr-3 pl-9 text-sm outline-none placeholder:text-muted-foreground/72 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/24 sm:h-8" /> -
+ ); } diff --git a/apps/web/src/components/pullRequest/PullRequestReviewerPicker.tsx b/apps/web/src/components/pullRequest/PullRequestReviewerPicker.tsx index 25c663794e97..8330c87a9294 100644 --- a/apps/web/src/components/pullRequest/PullRequestReviewerPicker.tsx +++ b/apps/web/src/components/pullRequest/PullRequestReviewerPicker.tsx @@ -19,6 +19,7 @@ import { useAtomCommand } from "~/state/use-atom-command"; import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; import { Button } from "../ui/button"; +import { Input } from "../ui/input"; import { Menu, MenuPopup, MenuTrigger } from "../ui/menu"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { toastManager } from "../ui/toast"; @@ -131,13 +132,13 @@ export function PullRequestReviewerPicker({ />
- setQuery(event.currentTarget.value)} placeholder="Search people with access" aria-label="Search people with access" - className="h-7 w-full rounded-md border border-input bg-background px-2 text-xs outline-none placeholder:text-muted-foreground/72 focus-visible:border-ring" + size="compact" />
diff --git a/apps/web/src/components/search/ProjectContentSearchDialog.tsx b/apps/web/src/components/search/ProjectContentSearchDialog.tsx index 1890aab7fa79..d877d6537bda 100644 --- a/apps/web/src/components/search/ProjectContentSearchDialog.tsx +++ b/apps/web/src/components/search/ProjectContentSearchDialog.tsx @@ -11,6 +11,7 @@ import { useProjectContentSearch } from "~/state/queries"; import { PierreEntryIcon } from "../chat/PierreEntryIcon"; import { CommandPaletteContent } from "../CommandPaletteContent"; import { ScrollArea } from "../ui/scroll-area"; +import { Toggle } from "../ui/toggle"; import { HighlightedSearchLine } from "./HighlightedSearchLine"; interface ProjectContentSearchDialogProps { @@ -58,19 +59,17 @@ function SearchOptionButton(props: { readonly children: ReactNode; }) { return ( - + ); } diff --git a/apps/web/src/components/settings/DiagnosticsSettings.tsx b/apps/web/src/components/settings/DiagnosticsSettings.tsx index 5bd4fdc08c4a..a472c6a8d3d7 100644 --- a/apps/web/src/components/settings/DiagnosticsSettings.tsx +++ b/apps/web/src/components/settings/DiagnosticsSettings.tsx @@ -273,14 +273,14 @@ function TraceIdCell({ traceId }: { traceId: string }) { copyToClipboard(traceId)} > - + } /> {copied ? "Copied" : "Copy full trace ID"} @@ -322,14 +322,14 @@ function ProcessNameCell({ style={{ paddingLeft: `${Math.min(process.depth, 6) * 10}px` }} > {hasChildren ? ( - + ) : (
- - @@ -702,17 +681,11 @@ function WhenExpressionBuilder({ ) : (
- - @@ -864,8 +837,7 @@ function KeybindingTableRow({ )} {isDirty ? (
+ ) : ( )} @@ -975,9 +975,8 @@ export function ResourceTelemetryDiagnostics() { - } - /> - - {children} - - - ); -} - function optionLabel(value: Option.Option): string | null { return Option.getOrNull(value); } @@ -316,9 +301,8 @@ function DiscoveryItemRow({
{hasDetails ? ( + } /> @@ -216,11 +212,10 @@ export function SettingResetButton({ { event.stopPropagation(); onClick(); @@ -251,7 +246,10 @@ export function SettingsPageContainer({ return ( -
+
{children}
diff --git a/apps/web/src/components/sidebar/SidebarChrome.tsx b/apps/web/src/components/sidebar/SidebarChrome.tsx index 175a1c3d0620..f4a98dec86c7 100644 --- a/apps/web/src/components/sidebar/SidebarChrome.tsx +++ b/apps/web/src/components/sidebar/SidebarChrome.tsx @@ -83,7 +83,7 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) { startExit(displayedView.key, null, displayedView.key)} > - + } /> Dismiss until provider status changes diff --git a/apps/web/src/components/threadSidebarWidth.test.ts b/apps/web/src/components/threadSidebarWidth.test.ts index 3beb2a8f5130..e38d5c3749bc 100644 --- a/apps/web/src/components/threadSidebarWidth.test.ts +++ b/apps/web/src/components/threadSidebarWidth.test.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics nodeBuiltinImport:off - Regression coverage compares shipped CSS with the sidebar width contract. +// @effect-diagnostics nodeBuiltinImport:off - Regression coverage compares the sidebar component with its width contract. import * as NodeFS from "node:fs"; import { describe, expect, it } from "vite-plus/test"; @@ -36,20 +36,13 @@ describe("thread sidebar width", () => { }); it("shows the desktop wordmark across the sidebar's full legal width range", () => { - const sidebarStyles = NodeFS.readFileSync(new URL("../index.css", import.meta.url), "utf8"); - const desktopHeaderStyles = sidebarStyles.slice( - sidebarStyles.indexOf("@media (min-width: 48rem)"), - sidebarStyles.indexOf("/* Stage-channel sidebar art"), + const sidebarSource = NodeFS.readFileSync( + new URL("./sidebar/SidebarChrome.tsx", import.meta.url), + "utf8", ); - const stageLabelThreshold = desktopHeaderStyles.match( - /@container sidebar-header \(min-width: ([\d.]+)rem\) \{\s*\.sidebar-brand-stage \{\s*display: inline-flex;/, - )?.[1]; - expect(sidebarStyles).toMatch(/\.sidebar-brand \{\s*display: none;/); - expect(desktopHeaderStyles).toMatch( - /@media \(min-width: 48rem\) \{\s*\.sidebar-brand \{\s*display: flex;/, - ); + expect(sidebarSource).toContain("hidden h-7 w-fit min-w-0 shrink-0 items-center gap-1"); + expect(sidebarSource).toContain("md:flex"); expect(THREAD_SIDEBAR_MIN_WIDTH).toBe(13 * 16); - expect(Number(stageLabelThreshold) * 16).toBeGreaterThan(THREAD_SIDEBAR_MIN_WIDTH); }); }); diff --git a/apps/web/src/components/ui/button.test.tsx b/apps/web/src/components/ui/button.test.tsx index 341d85b42bbc..e1bd89d94cdb 100644 --- a/apps/web/src/components/ui/button.test.tsx +++ b/apps/web/src/components/ui/button.test.tsx @@ -28,4 +28,19 @@ describe("button geometry tokens", () => { expect(html).toContain("size-7"); expect(html).toContain("sm:size-6"); }); + + it("owns shared compact and micro control geometry", () => { + const compact = renderToStaticMarkup(); + const micro = renderToStaticMarkup( + , + ); + + expect(compact).toContain("h-7"); + expect(compact).toContain("rounded-md"); + expect(micro).toContain("size-5"); + expect(micro).toContain("rounded-sm"); + expect(micro).toContain("text-muted-foreground"); + }); }); diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx index 778574ba0107..9f0b4d049eda 100644 --- a/apps/web/src/components/ui/button.tsx +++ b/apps/web/src/components/ui/button.tsx @@ -16,9 +16,13 @@ const buttonVariants = cva( }, variants: { size: { + compact: + "h-7 gap-1 rounded-md px-[calc(--spacing(2)-1px)] text-xs before:rounded-[calc(var(--radius-md)-1px)] [&_svg:not([class*='size-'])]:size-3.5", default: "h-9 px-[calc(--spacing(3)-1px)] sm:h-8", icon: "size-9 sm:size-8", "icon-lg": "size-10 sm:size-9", + "icon-micro": + "size-5 rounded-sm p-0 before:rounded-[calc(var(--radius-sm)-1px)] [&_svg:not([class*='size-'])]:size-3", "icon-sm": "size-8 sm:size-7", "icon-xl": "size-11 sm:size-10 [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5", @@ -38,6 +42,10 @@ const buttonVariants = cva( "border-input bg-popover not-dark:bg-clip-padding text-destructive-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:border-destructive/32 [:hover,[data-pressed]]:bg-destructive/4", ghost: "[--control-icon-color:var(--muted-foreground)] border-transparent text-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent", + "ghost-muted": + "[--control-icon-color:var(--muted-foreground)] border-transparent text-muted-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent [:hover,[data-pressed]]:text-foreground", + glass: + "surface-glass [--control-icon-color:var(--muted-foreground)] border-border/60 text-foreground shadow-sm [:hover,[data-pressed]]:border-border", link: "border-transparent underline-offset-4 [:hover,[data-pressed]]:underline", outline: "[--control-icon-color:var(--muted-foreground)] border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-accent/50 dark:[:hover,[data-pressed]]:bg-input/64", diff --git a/apps/web/src/components/ui/combobox.tsx b/apps/web/src/components/ui/combobox.tsx index 324b67e64d90..cf3a46142ad2 100644 --- a/apps/web/src/components/ui/combobox.tsx +++ b/apps/web/src/components/ui/combobox.tsx @@ -170,7 +170,7 @@ function ComboboxPopup({ > diff --git a/apps/web/src/components/ui/input-group.tsx b/apps/web/src/components/ui/input-group.tsx index 2ac9ee1ed41a..04e34e5611ee 100644 --- a/apps/web/src/components/ui/input-group.tsx +++ b/apps/web/src/components/ui/input-group.tsx @@ -8,7 +8,7 @@ import { Input, type InputProps } from "~/components/ui/input"; import { Textarea, type TextareaProps } from "~/components/ui/textarea"; const inputGroupVariants = cva( - "relative inline-flex w-full min-w-0 items-center rounded-lg border text-base text-foreground ring-ring/24 transition-shadow has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/64 has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/16 has-[textarea]:h-auto has-data-[align=block-end]:h-auto has-data-[align=block-start]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col has-[input:focus-visible,textarea:focus-visible]:border-ring has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/36 has-autofill:bg-foreground/4 has-[input:disabled,textarea:disabled]:opacity-64 has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,input[aria-invalid],textarea[aria-invalid]]:shadow-none has-[input:focus-visible,textarea:focus-visible]:ring-[3px] sm:text-sm dark:has-autofill:bg-foreground/8 dark:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/24 has-data-[align=inline-start]:**:[[data-size=sm]_input]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_input]:pe-1.5 *:[[data-slot=input-control],[data-slot=textarea-control]]:contents *:[[data-slot=input-control],[data-slot=textarea-control]]:before:hidden has-[[data-align=block-start],[data-align=block-end]]:**:[input]:h-auto has-data-[align=inline-start]:**:[input]:ps-2 has-data-[align=inline-end]:**:[input]:pe-2 has-data-[align=block-end]:**:[input]:pt-1.5 has-data-[align=block-start]:**:[input]:pb-1.5 **:[textarea]:min-h-20.5 **:[textarea]:resize-none **:[textarea]:py-[calc(--spacing(3)-1px)] **:[textarea]:max-sm:min-h-23.5 **:[textarea_button]:rounded-[calc(var(--radius-md)-1px)]", + "relative inline-flex w-full min-w-0 items-center rounded-[var(--control-radius)] border text-base text-foreground ring-ring/24 transition-shadow has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/64 has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/16 has-[textarea]:h-auto has-data-[align=block-end]:h-auto has-data-[align=block-start]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col has-[input:focus-visible,textarea:focus-visible]:border-ring has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/36 has-autofill:bg-foreground/4 has-[input:disabled,textarea:disabled]:opacity-64 has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,input[aria-invalid],textarea[aria-invalid]]:shadow-none has-[input:focus-visible,textarea:focus-visible]:ring-[3px] sm:text-sm dark:has-autofill:bg-foreground/8 dark:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/24 has-data-[align=inline-start]:**:[[data-size=sm]_input]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_input]:pe-1.5 *:[[data-slot=input-control],[data-slot=textarea-control]]:contents *:[[data-slot=input-control],[data-slot=textarea-control]]:before:hidden has-[[data-align=block-start],[data-align=block-end]]:**:[input]:h-auto has-data-[align=inline-start]:**:[input]:ps-2 has-data-[align=inline-end]:**:[input]:pe-2 has-data-[align=block-end]:**:[input]:pt-1.5 has-data-[align=block-start]:**:[input]:pb-1.5 **:[textarea]:min-h-20.5 **:[textarea]:resize-none **:[textarea]:py-[calc(--spacing(3)-1px)] **:[textarea]:max-sm:min-h-23.5 **:[textarea_button]:rounded-[calc(var(--control-radius)-1px)]", { defaultVariants: { variant: "default", @@ -16,7 +16,7 @@ const inputGroupVariants = cva( variants: { variant: { default: - "border-input bg-background not-dark:bg-clip-padding shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_-1px_--theme(--color-white/6%)]", + "border-input bg-background not-dark:bg-clip-padding shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--control-radius)-1px)] not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_-1px_--theme(--color-white/6%)]", ghost: "border-transparent bg-transparent shadow-none hover:bg-muted/40 has-[input:focus-visible,textarea:focus-visible]:bg-background", }, diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx index 6edc8d4a62fc..cae3dfe62852 100644 --- a/apps/web/src/components/ui/input.tsx +++ b/apps/web/src/components/ui/input.tsx @@ -6,7 +6,7 @@ import type * as React from "react"; import { cn } from "~/lib/utils"; type InputProps = Omit, "size"> & { - size?: "sm" | "default" | "lg" | number; + size?: "sm" | "compact" | "default" | "lg" | number; unstyled?: boolean; nativeInput?: boolean; }; @@ -20,6 +20,7 @@ function Input({ }: InputProps) { const inputClassName = cn( "h-8.5 w-full min-w-0 rounded-[inherit] px-[calc(--spacing(3)-1px)] leading-8.5 outline-none placeholder:text-placeholder sm:h-7.5 sm:leading-7.5 [transition:background-color_5000000s_ease-in-out_0s]", + size === "compact" && "h-7 px-[calc(--spacing(2.5)-1px)] text-xs leading-7 sm:h-7 sm:leading-7", size === "sm" && "h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5", size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5", props.type === "search" && @@ -59,6 +60,9 @@ function Input({ cn( !unstyled && "relative inline-flex w-full rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 has-aria-invalid:border-destructive/36 has-focus-visible:border-ring has-autofill:bg-foreground/4 has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none has-focus-visible:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]", + !unstyled && + size === "compact" && + "rounded-md before:rounded-[calc(var(--radius-md)-1px)]", className, ) || undefined } diff --git a/apps/web/src/components/ui/menu.tsx b/apps/web/src/components/ui/menu.tsx index 9f7cfc8c0670..803d6c1987c1 100644 --- a/apps/web/src/components/ui/menu.tsx +++ b/apps/web/src/components/ui/menu.tsx @@ -36,6 +36,13 @@ function MenuPopup({ side?: MenuPrimitive.Positioner.Props["side"]; anchor?: MenuPrimitive.Positioner.Props["anchor"]; }) { + const hasExplicitWidthClass = + typeof className === "string" && + className.split(/\s+/).some((classToken) => { + const utility = classToken.split(":").at(-1) ?? classToken; + return /^(?:min-|max-)?w-/.test(utility); + }); + return (
) { return (
copyToClipboard(text)} - type="button" /> } > @@ -381,24 +382,22 @@ function ToastBodyContent({ > {copyErrorText !== null ? : null} {additionalActions.map(({ id, props: { className, ...props } }) => ( - ))}
- +
diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 6169e1a643ca..4e636eb4ff0f 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1,6 +1,7 @@ @import "tailwindcss"; @custom-variant dark (&:is(.dark, .dark *)); +@custom-variant light (&:not(.dark, .dark *)); /* Window Controls Overlay: active when Electron exposes native titlebar control geometry. */ @custom-variant wco (&:is(.wco, .wco *)); @@ -102,20 +103,13 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil --workspace-native-controls-inset: 0px; --workspace-titlebar-control-size: 1.75rem; --workspace-titlebar-control-gap: 0.75rem; -} - -.dark { - --app-scrollbar-thumb: rgb(255 255 255 / 8%); - --app-scrollbar-thumb-hover: rgb(255 255 255 / 12%); - --glass-blur: 16px; - --glass-saturation: 1.08; -} -[data-slot="sidebar-wrapper"] { - --workspace-titlebar-content-left: calc( - var(--workspace-controls-left) + var(--workspace-titlebar-control-size) + - var(--workspace-titlebar-control-gap) - ); + @variant dark { + --app-scrollbar-thumb: rgb(255 255 255 / 8%); + --app-scrollbar-thumb-hover: rgb(255 255 255 / 12%); + --glass-blur: 16px; + --glass-saturation: 1.08; + } } .wco { @@ -264,6 +258,179 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil } } +@utility surface-glass { + background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + + @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) { + background: var(--background) !important; + } +} + +@utility alert-glass { + --alert-glass-tint: transparent; + background: + linear-gradient( + color-mix(in srgb, var(--alert-glass-tint) 4%, transparent), + color-mix(in srgb, var(--alert-glass-tint) 4%, transparent) + ), + color-mix(in srgb, var(--background) var(--glass-opacity), transparent) !important; + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + + &[data-variant="error"] { + --alert-glass-tint: var(--destructive); + } + + &[data-variant="info"] { + --alert-glass-tint: var(--info); + } + + &[data-variant="success"] { + --alert-glass-tint: var(--success); + } + + &[data-variant="warning"] { + --alert-glass-tint: var(--warning); + } + + @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) { + background: var(--background) !important; + } +} + +@utility dialog-glass { + background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + border-color: color-mix(in srgb, var(--foreground) 10%, transparent); + box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%); + + @variant dark { + border-color: color-mix(in srgb, var(--color-white) 8%, transparent); + box-shadow: + inset 0 1px rgb(255 255 255 / 4%), + 0 24px 72px -20px rgb(0 0 0 / 90%); + } + + @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) { + background: var(--popover) !important; + } +} + +@utility dialog-backdrop { + background: color-mix(in srgb, var(--background) 60%, transparent); + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + + @variant dark { + background: color-mix(in srgb, var(--background) 64%, transparent); + } +} + +@utility dropdown-glass { + background: color-mix( + in srgb, + var(--popover) 18%, + color-mix(in srgb, var(--popover) var(--glass-opacity), transparent) + ); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent); + + @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) { + background: var(--popover) !important; + } +} + +@utility topbar-scroll-fade { + --topbar-scroll-fade-height: 2.5rem; + -webkit-mask-image: + linear-gradient( + to bottom, + transparent 0%, + rgb(0 0 0 / 10%) 10%, + rgb(0 0 0 / 30%) 24%, + rgb(0 0 0 / 58%) 42%, + rgb(0 0 0 / 82%) 62%, + rgb(0 0 0 / 96%) 82%, + black 100% + ), + linear-gradient(black, black), linear-gradient(black, black); + -webkit-mask-position: top, bottom, right; + -webkit-mask-repeat: no-repeat; + -webkit-mask-size: + 100% var(--topbar-scroll-fade-height), + 100% calc(100% - var(--topbar-scroll-fade-height)), + var(--app-scrollbar-width) 100%; + mask-image: + linear-gradient( + to bottom, + transparent 0%, + rgb(0 0 0 / 10%) 10%, + rgb(0 0 0 / 30%) 24%, + rgb(0 0 0 / 58%) 42%, + rgb(0 0 0 / 82%) 62%, + rgb(0 0 0 / 96%) 82%, + black 100% + ), + linear-gradient(black, black), linear-gradient(black, black); + mask-position: top, bottom, right; + mask-repeat: no-repeat; + mask-size: + 100% var(--topbar-scroll-fade-height), + 100% calc(100% - var(--topbar-scroll-fade-height)), + var(--app-scrollbar-width) 100%; + + @variant sm { + --topbar-scroll-fade-height: 3rem; + } +} + +/* Virtualizers own their native scroll element, so they cannot use ScrollArea's + viewport fade. Keep the scrollbar lane opaque while sharing the same fade + contract across those lists. */ +@utility virtualized-scroll-fade { + -webkit-mask-image: var(--virtualized-scroll-fade-mask), linear-gradient(black, black); + mask-image: var(--virtualized-scroll-fade-mask), linear-gradient(black, black); + -webkit-mask-position: left, right; + mask-position: left, right; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: + calc(100% - var(--app-scrollbar-width)) 100%, + var(--app-scrollbar-width) 100%; + mask-size: + calc(100% - var(--app-scrollbar-width)) 100%, + var(--app-scrollbar-width) 100%; +} + +/* Stage-channel art needs a mask and pseudo-element gradient, so keep the + behavior composable without tying it to the global components layer. */ +@utility sidebar-stage-backdrop { + --stage-fade: var(--sidebar-stage-fade, var(--app-chrome-background)); + mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%); + -webkit-mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%); + + &::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient( + to bottom, + transparent 0%, + transparent 28%, + color-mix(in srgb, var(--stage-fade) 10%, transparent) 40%, + color-mix(in srgb, var(--stage-fade) 30%, transparent) 52%, + color-mix(in srgb, var(--stage-fade) 58%, transparent) 64%, + color-mix(in srgb, var(--stage-fade) 82%, transparent) 75%, + color-mix(in srgb, var(--stage-fade) 96%, transparent) 85%, + var(--stage-fade) 93% + ); + } +} + @layer base { :root { /* Keep the original T3 Code artwork palettes as the defaults. Built-in @@ -292,12 +459,12 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil --stage-night-glow-highlight: oklch(0.553749 0.176543 271.958); --stage-night-glow-secondary: oklch(0.345571 0.117466 273.568); --stage-night-sparkle: oklch(0.880867 0.057747 269.011); - } - .dark { - --stage-art-top: oklch(0.581473 0.149124 256.9); - --stage-art-mid: oklch(0.456509 0.159377 261.945); - --stage-art-bottom: oklch(0.291327 0.136578 267.649); + @variant dark { + --stage-art-top: oklch(0.581473 0.149124 256.9); + --stage-art-mid: oklch(0.456509 0.159377 261.945); + --stage-art-bottom: oklch(0.291327 0.136578 267.649); + } } * { @@ -311,63 +478,16 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil ):focus-visible { @apply outline-none ring-0; } - html { + html, + body { background-color: var(--app-chrome-background); } body { @apply text-foreground relative; - background-color: var(--app-chrome-background); } } @layer components { - .sidebar-brand { - display: none; - } - - .sidebar-brand-stage { - display: none; - } - - @media (min-width: 48rem) { - .sidebar-brand { - display: flex; - } - - @container sidebar-header (min-width: 15.75rem) { - .sidebar-brand-stage { - display: inline-flex; - } - } - } - - /* Stage-channel sidebar art; ::after ramps to the sidebar bg color and the - mask lets the surface grain show through at the boundary. Panels whose - background differs from the app chrome (e.g. sidebar v2) override - --sidebar-stage-fade so the art fades into their own surface color. */ - .sidebar-stage-backdrop { - --stage-fade: var(--sidebar-stage-fade, var(--app-chrome-background)); - mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%); - -webkit-mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%); - } - - .sidebar-stage-backdrop::after { - content: ""; - position: absolute; - inset: 0; - background: linear-gradient( - to bottom, - transparent 0%, - transparent 28%, - color-mix(in srgb, var(--stage-fade) 10%, transparent) 40%, - color-mix(in srgb, var(--stage-fade) 30%, transparent) 52%, - color-mix(in srgb, var(--stage-fade) 58%, transparent) 64%, - color-mix(in srgb, var(--stage-fade) 82%, transparent) 75%, - color-mix(in srgb, var(--stage-fade) 96%, transparent) 85%, - var(--stage-fade) 93% - ); - } - /* Each maintainer palette gives the same line art its own material: rose vellum, forest drafting paper, marine cyanotype, copper, and violet ink. These colors stay deliberately deep at the top edge so the white stage @@ -380,16 +500,16 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil --stage-art-secondary: oklch(0.763402 0.163836 352.525); --stage-art-tertiary: oklch(0.70819 0.180285 311.949); --stage-art-line: oklch(0.952158 0.034194 336.179); - } - html.dark[data-theme-id="t3-chat"] { - --stage-art-top: oklch(0.540689 0.143665 347.587); - --stage-art-mid: oklch(0.396586 0.126592 347.6); - --stage-art-bottom: oklch(0.249959 0.079694 340.523); - --stage-art-highlight: oklch(0.921297 0.051708 343.229); - --stage-art-secondary: oklch(0.667398 0.165674 352.549); - --stage-art-tertiary: oklch(0.609315 0.163722 306.315); - --stage-art-line: oklch(0.945349 0.036045 341.433); + @variant dark { + --stage-art-top: oklch(0.540689 0.143665 347.587); + --stage-art-mid: oklch(0.396586 0.126592 347.6); + --stage-art-bottom: oklch(0.249959 0.079694 340.523); + --stage-art-highlight: oklch(0.921297 0.051708 343.229); + --stage-art-secondary: oklch(0.667398 0.165674 352.549); + --stage-art-tertiary: oklch(0.609315 0.163722 306.315); + --stage-art-line: oklch(0.945349 0.036045 341.433); + } } html[data-theme-id="grove"] { @@ -407,23 +527,23 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil --stage-night-secondary: oklch(0.665652 0.109731 156.599); --stage-night-tertiary: oklch(0.698651 0.103024 89.828); --stage-night-line: oklch(0.945336 0.041923 157.222); - } - html.dark[data-theme-id="grove"] { - --stage-art-top: oklch(0.58719 0.09869 157.426); - --stage-art-mid: oklch(0.454979 0.079031 159.756); - --stage-art-bottom: oklch(0.297856 0.050355 161.167); - --stage-art-highlight: oklch(0.952407 0.053872 158.44); - --stage-art-secondary: oklch(0.732591 0.120606 155.853); - --stage-art-tertiary: oklch(0.716282 0.116547 80.563); - --stage-art-line: oklch(0.961577 0.035285 157.03); - --stage-night-top: oklch(0.398632 0.065534 158.601); - --stage-night-mid: oklch(0.290561 0.049694 160.456); - --stage-night-bottom: oklch(0.210147 0.03173 169.818); - --stage-night-highlight: oklch(0.866303 0.057526 156.796); - --stage-night-secondary: oklch(0.586553 0.093722 157.365); - --stage-night-tertiary: oklch(0.6364 0.101769 82.985); - --stage-night-line: oklch(0.913292 0.035718 156.976); + @variant dark { + --stage-art-top: oklch(0.58719 0.09869 157.426); + --stage-art-mid: oklch(0.454979 0.079031 159.756); + --stage-art-bottom: oklch(0.297856 0.050355 161.167); + --stage-art-highlight: oklch(0.952407 0.053872 158.44); + --stage-art-secondary: oklch(0.732591 0.120606 155.853); + --stage-art-tertiary: oklch(0.716282 0.116547 80.563); + --stage-art-line: oklch(0.961577 0.035285 157.03); + --stage-night-top: oklch(0.398632 0.065534 158.601); + --stage-night-mid: oklch(0.290561 0.049694 160.456); + --stage-night-bottom: oklch(0.210147 0.03173 169.818); + --stage-night-highlight: oklch(0.866303 0.057526 156.796); + --stage-night-secondary: oklch(0.586553 0.093722 157.365); + --stage-night-tertiary: oklch(0.6364 0.101769 82.985); + --stage-night-line: oklch(0.913292 0.035718 156.976); + } } html[data-theme-id="ocean"] { @@ -434,16 +554,16 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil --stage-art-secondary: oklch(0.788391 0.090856 215.684); --stage-art-tertiary: oklch(0.76441 0.099607 187.893); --stage-art-line: oklch(0.976025 0.019647 212.543); - } - html.dark[data-theme-id="ocean"] { - --stage-art-top: oklch(0.59663 0.089167 233.427); - --stage-art-mid: oklch(0.461094 0.084904 243.478); - --stage-art-bottom: oklch(0.294818 0.05947 250.526); - --stage-art-highlight: oklch(0.952907 0.032224 221.27); - --stage-art-secondary: oklch(0.732079 0.09296 224.414); - --stage-art-tertiary: oklch(0.720885 0.095495 190.903); - --stage-art-line: oklch(0.961039 0.027355 219.756); + @variant dark { + --stage-art-top: oklch(0.59663 0.089167 233.427); + --stage-art-mid: oklch(0.461094 0.084904 243.478); + --stage-art-bottom: oklch(0.294818 0.05947 250.526); + --stage-art-highlight: oklch(0.952907 0.032224 221.27); + --stage-art-secondary: oklch(0.732079 0.09296 224.414); + --stage-art-tertiary: oklch(0.720885 0.095495 190.903); + --stage-art-line: oklch(0.961039 0.027355 219.756); + } } html[data-theme-id="ember"] { @@ -461,23 +581,23 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil --stage-night-secondary: oklch(0.641705 0.126508 44.376); --stage-night-tertiary: oklch(0.538694 0.129931 25.865); --stage-night-line: oklch(0.926348 0.046029 58.73); - } - html.dark[data-theme-id="ember"] { - --stage-art-top: oklch(0.597533 0.120694 43.455); - --stage-art-mid: oklch(0.437763 0.101287 34.86); - --stage-art-bottom: oklch(0.264269 0.055858 26.548); - --stage-art-highlight: oklch(0.929214 0.042638 55.801); - --stage-art-secondary: oklch(0.705592 0.137369 43.176); - --stage-art-tertiary: oklch(0.629583 0.158322 24.088); - --stage-art-line: oklch(0.945058 0.033906 58.824); - --stage-night-top: oklch(0.392352 0.081287 36.444); - --stage-night-mid: oklch(0.271305 0.056352 31.135); - --stage-night-bottom: oklch(0.182126 0.028154 27.774); - --stage-night-highlight: oklch(0.851007 0.061294 53.805); - --stage-night-secondary: oklch(0.560789 0.10645 42.953); - --stage-night-tertiary: oklch(0.476228 0.106656 24.165); - --stage-night-line: oklch(0.884931 0.046607 56.556); + @variant dark { + --stage-art-top: oklch(0.597533 0.120694 43.455); + --stage-art-mid: oklch(0.437763 0.101287 34.86); + --stage-art-bottom: oklch(0.264269 0.055858 26.548); + --stage-art-highlight: oklch(0.929214 0.042638 55.801); + --stage-art-secondary: oklch(0.705592 0.137369 43.176); + --stage-art-tertiary: oklch(0.629583 0.158322 24.088); + --stage-art-line: oklch(0.945058 0.033906 58.824); + --stage-night-top: oklch(0.392352 0.081287 36.444); + --stage-night-mid: oklch(0.271305 0.056352 31.135); + --stage-night-bottom: oklch(0.182126 0.028154 27.774); + --stage-night-highlight: oklch(0.851007 0.061294 53.805); + --stage-night-secondary: oklch(0.560789 0.10645 42.953); + --stage-night-tertiary: oklch(0.476228 0.106656 24.165); + --stage-night-line: oklch(0.884931 0.046607 56.556); + } } html[data-theme-id="iris"] { @@ -488,16 +608,16 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil --stage-art-secondary: oklch(0.745085 0.125892 298.647); --stage-art-tertiary: oklch(0.73066 0.167815 340.964); --stage-art-line: oklch(0.960278 0.024064 306.969); - } - html.dark[data-theme-id="iris"] { - --stage-art-top: oklch(0.57297 0.145973 295.185); - --stage-art-mid: oklch(0.419499 0.13752 292.131); - --stage-art-bottom: oklch(0.274235 0.095798 286.608); - --stage-art-highlight: oklch(0.916698 0.047206 300.224); - --stage-art-secondary: oklch(0.670994 0.13095 296.689); - --stage-art-tertiary: oklch(0.679357 0.165376 340.439); - --stage-art-line: oklch(0.940582 0.032921 299.076); + @variant dark { + --stage-art-top: oklch(0.57297 0.145973 295.185); + --stage-art-mid: oklch(0.419499 0.13752 292.131); + --stage-art-bottom: oklch(0.274235 0.095798 286.608); + --stage-art-highlight: oklch(0.916698 0.047206 300.224); + --stage-art-secondary: oklch(0.670994 0.13095 296.689); + --stage-art-tertiary: oklch(0.679357 0.165376 340.439); + --stage-art-line: oklch(0.940582 0.032921 299.076); + } } :is(html[data-theme-id="t3-chat"], html[data-theme-id="ocean"], html[data-theme-id="iris"]) { @@ -538,65 +658,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil --stage-night-sparkle: var(--stage-night-line); } - .workspace-topbar { - display: flex; - height: var(--workspace-topbar-height); - min-height: var(--workspace-topbar-height); - flex-shrink: 0; - align-items: center; - } - - /* Fade rows themselves as they pass beneath the top chrome. A mask remains - visible even when the header and timeline share the same background. */ - .chat-timeline-scroll-fade, - .settings-page-scroll-fade, - .pull-requests-scroll-fade { - --topbar-scroll-fade-height: 2.5rem; - -webkit-mask-image: - linear-gradient( - to bottom, - transparent 0%, - rgb(0 0 0 / 10%) 10%, - rgb(0 0 0 / 30%) 24%, - rgb(0 0 0 / 58%) 42%, - rgb(0 0 0 / 82%) 62%, - rgb(0 0 0 / 96%) 82%, - black 100% - ), - linear-gradient(black, black), linear-gradient(black, black); - -webkit-mask-position: top, bottom, right; - -webkit-mask-repeat: no-repeat; - -webkit-mask-size: - 100% var(--topbar-scroll-fade-height), - 100% calc(100% - var(--topbar-scroll-fade-height)), - var(--app-scrollbar-width) 100%; - mask-image: - linear-gradient( - to bottom, - transparent 0%, - rgb(0 0 0 / 10%) 10%, - rgb(0 0 0 / 30%) 24%, - rgb(0 0 0 / 58%) 42%, - rgb(0 0 0 / 82%) 62%, - rgb(0 0 0 / 96%) 82%, - black 100% - ), - linear-gradient(black, black), linear-gradient(black, black); - mask-position: top, bottom, right; - mask-repeat: no-repeat; - mask-size: - 100% var(--topbar-scroll-fade-height), - 100% calc(100% - var(--topbar-scroll-fade-height)), - var(--app-scrollbar-width) 100%; - } - - /* The pull request list sits directly under its topbar, so the tall band the chat and - settings pages fade under would read as empty padding here. A shorter band keeps the - fade while letting the controls start near the chrome. */ - .pull-requests-scroll-fade { - --topbar-scroll-fade-height: 1.5rem; - } - @keyframes settings-search-target-pulse { 0%, 100% { @@ -607,56 +668,29 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil } } - .settings-page-scroll-fade div.settings-search-target-pulse, - .settings-page-scroll-fade section.settings-search-target-pulse > div:first-child { + [data-settings-page-scroll] div.settings-search-target-pulse, + [data-settings-page-scroll] section.settings-search-target-pulse > div:first-child { animation: settings-search-target-pulse 650ms ease-in-out 2; border-radius: 0.75rem; } /* The pulse is the destination indicator; without it (reduced motion), the focus outline takes over, so exactly one indicator shows at a time. */ - .settings-page-scroll-fade .settings-search-target-pulse:focus { + [data-settings-page-scroll] .settings-search-target-pulse:focus { outline: none; } - .workspace-titlebar-controls { - position: absolute; - top: var(--workspace-controls-top); - right: var(--workspace-controls-right); - display: flex; - height: var(--workspace-topbar-height); - align-items: center; - -webkit-app-region: no-drag; - } - - .surface-subheader { - @apply flex h-10 min-h-10 shrink-0 items-center border-b border-border/60 bg-background; - } - - [data-preview-panel-mode="inline"] [data-right-panel-surface-content] [data-surface-subheader] { - height: calc(var(--spacing) * 7); - min-height: calc(var(--spacing) * 7); - margin-bottom: calc(var(--spacing) * 3); - border-bottom-color: transparent; - } - - .chat-composer-horizontal-inset { - padding-inline-start: calc(env(safe-area-inset-left) + 0.75rem); - padding-inline-end: calc(env(safe-area-inset-right) + 0.75rem); - } - - .chat-composer-glass { - background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent); - -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); - backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); - } - .chat-composer-glass-shell { --chat-composer-glass-surface: var(--card); --chat-composer-outline: rgb(0 0 0 / 8%); - position: relative; isolation: isolate; + + @variant dark { + --chat-composer-glass-surface: color-mix(in srgb, var(--background) 96%, var(--color-white)); + --chat-composer-outline: color-mix(in srgb, var(--color-white) 5%, transparent); + --chat-composer-highlight: rgb(255 255 255 / 3%); + } } .chat-composer-glass-shell::before { @@ -716,8 +750,15 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil } .chat-composer-glass-host { - position: relative; box-shadow: 0 12px 28px -18px rgb(0 0 0 / 40%); + + @variant dark { + box-shadow: none; + + &::after { + box-shadow: inset 0 1px var(--chat-composer-highlight); + } + } } .chat-composer-glass-host::after { @@ -747,6 +788,21 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil .chat-composer-context-strip { position: relative; isolation: isolate; + + @variant dark { + &::before { + border-color: rgb(255 255 255 / 7%); + background: + linear-gradient( + to bottom, + transparent 0 1rem, + rgb(0 0 0 / 18%) 1rem, + transparent calc(1rem + 10px) + ), + rgb(255 255 255 / 2%); + box-shadow: 0 14px 32px -18px rgb(0 0 0 / 75%); + } + } } .chat-composer-context-strip::before { @@ -762,33 +818,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil content: ""; } - .dark .chat-composer-glass-shell { - --chat-composer-glass-surface: color-mix(in srgb, var(--background) 96%, var(--color-white)); - --chat-composer-outline: color-mix(in srgb, var(--color-white) 5%, transparent); - --chat-composer-highlight: rgb(255 255 255 / 3%); - } - - .dark .chat-composer-glass-host { - box-shadow: none; - } - - .dark .chat-composer-glass-host::after { - box-shadow: inset 0 1px var(--chat-composer-highlight); - } - - .dark .chat-composer-context-strip::before { - border-color: rgb(255 255 255 / 7%); - background: - linear-gradient( - to bottom, - transparent 0 1rem, - rgb(0 0 0 / 18%) 1rem, - transparent calc(1rem + 10px) - ), - rgb(255 255 255 / 2%); - box-shadow: 0 14px 32px -18px rgb(0 0 0 / 75%); - } - @supports not (clip-path: shape(from 0 0, line to 1px 1px)) { .chat-composer-glass-shell-with-context::before { inset-block-end: var(--chat-composer-context-extension); @@ -806,106 +835,23 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); } - .dark .chat-composer-context-strip::before { - background: - linear-gradient( - to bottom, - transparent 0 1rem, - rgb(0 0 0 / 18%) 1rem, - transparent calc(1rem + 10px) - ), - linear-gradient(rgb(255 255 255 / 2%), rgb(255 255 255 / 2%)), - color-mix(in srgb, var(--chat-composer-glass-surface) var(--glass-opacity), transparent); + .chat-composer-context-strip { + @variant dark { + &::before { + background: + linear-gradient( + to bottom, + transparent 0 1rem, + rgb(0 0 0 / 18%) 1rem, + transparent calc(1rem + 10px) + ), + linear-gradient(rgb(255 255 255 / 2%), rgb(255 255 255 / 2%)), + color-mix(in srgb, var(--chat-composer-glass-surface) var(--glass-opacity), transparent); + } + } } } - .alert-glass { - --alert-glass-tint: transparent; - - background: - linear-gradient( - color-mix(in srgb, var(--alert-glass-tint) 4%, transparent), - color-mix(in srgb, var(--alert-glass-tint) 4%, transparent) - ), - color-mix(in srgb, var(--background) var(--glass-opacity), transparent) !important; - -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); - backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); - } - - .alert-glass[data-variant="error"] { - --alert-glass-tint: var(--destructive); - } - - .alert-glass[data-variant="info"] { - --alert-glass-tint: var(--info); - } - - .alert-glass[data-variant="success"] { - --alert-glass-tint: var(--success); - } - - .alert-glass[data-variant="warning"] { - --alert-glass-tint: var(--warning); - } - - .dialog-glass { - background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent); - -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); - backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); - } - - .dialog-backdrop { - background: color-mix(in srgb, var(--background) 60%, transparent); - -webkit-backdrop-filter: blur(4px); - backdrop-filter: blur(4px); - } - - .dropdown-glass { - /* - * Elevated glass needs a denser tint than broad ambient surfaces. Nesting - * the user-controlled mix inside an 18% popover tint preserves the full - * opacity setting range (40% -> 51%, 80% -> 84%, 100% -> 100%) while - * keeping high-contrast page content from blooming through menus. - */ - background: color-mix( - in srgb, - var(--popover) 18%, - color-mix(in srgb, var(--popover) var(--glass-opacity), transparent) - ); - -webkit-backdrop-filter: blur(var(--glass-blur)); - backdrop-filter: blur(var(--glass-blur)); - border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent); - box-shadow: 0 16px 40px -18px rgb(0 0 0 / 55%); - } - - .dialog-glass { - border-color: color-mix(in srgb, var(--foreground) 10%, transparent); - box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%); - } - - .dark .dropdown-glass { - box-shadow: 0 18px 44px -18px rgb(0 0 0 / 80%); - } - - .dark .model-picker-surface.model-picker-surface { - background: color-mix( - in srgb, - var(--popover) 18%, - color-mix(in srgb, var(--popover) var(--glass-opacity), transparent) - ); - } - - .dark .dialog-glass { - border-color: color-mix(in srgb, var(--color-white) 8%, transparent); - box-shadow: - inset 0 1px rgb(255 255 255 / 4%), - 0 24px 72px -20px rgb(0 0 0 / 90%); - } - - .dark .dialog-backdrop { - background: color-mix(in srgb, var(--background) 64%, transparent); - } - .settings-slider { --settings-slider-progress: 0%; --settings-slider-fill-offset: 0.5rem; @@ -1021,32 +967,10 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil } } - @media (min-width: 40rem) { - .chat-timeline-scroll-fade, - .settings-page-scroll-fade { - --topbar-scroll-fade-height: 3rem; - } - - .chat-composer-horizontal-inset { - padding-inline-start: calc(env(safe-area-inset-left) + 1.25rem); - padding-inline-end: calc(env(safe-area-inset-right) + 1.25rem); - } - } - @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) { - .chat-composer-glass, - .alert-glass { - background: var(--background) !important; - } - .chat-composer-glass-shell::before { background: var(--chat-composer-glass-surface); } - - .dialog-glass, - .dropdown-glass { - background: var(--popover) !important; - } } } @@ -1152,15 +1076,12 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil --terminal-foreground: var(--foreground); --terminal-cursor: rgb(38 56 78); --terminal-selection-background: rgb(37 63 99 / 20%); - --terminal-scrollbar: rgb(0 0 0 / 15%); - --terminal-scrollbar-hover: rgb(0 0 0 / 25%); @variant dark { color-scheme: dark; /* Keep the workspace in the same neutral-black family as sidebar v2. Surfaces lift from this base instead of starting from a milky gray. */ --background: var(--color-neutral-950); - --app-chrome-background: var(--background); --surface-raised: var(--secondary); --foreground: var(--color-neutral-100); --card: color-mix(in srgb, var(--background) 97%, var(--color-white)); @@ -1168,54 +1089,31 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil --popover: color-mix(in srgb, var(--background) 94%, var(--color-white)); --popover-foreground: var(--color-neutral-100); --primary: oklch(0.571 0.21 264); - --primary-foreground: var(--color-white); --secondary: --alpha(var(--color-white) / 4%); --secondary-foreground: var(--color-neutral-100); --muted: --alpha(var(--color-white) / 4%); --muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-white)); - --placeholder: var(--muted-foreground); - --secondary-label: var(--muted-foreground); - --icon-muted: var(--muted-foreground); - --message-surface: var(--accent); - --message-foreground: var(--foreground); - --message-action: var(--primary); - --message-action-foreground: var(--primary-foreground); - --message-action-hover: color-mix(in srgb, var(--primary) 90%, var(--background)); --accent: --alpha(var(--color-white) / 4%); --accent-foreground: var(--color-neutral-100); --error: color-mix(in srgb, var(--color-red-500) 90%, var(--color-white)); --error-foreground: var(--color-red-400); --error-surface: color-mix(in srgb, var(--error) 16%, transparent); - --destructive: var(--error); --border: --alpha(var(--color-white) / 6%); --input: --alpha(var(--color-white) / 8%); - --ring: var(--primary); - --destructive-foreground: var(--error-foreground); - --info: var(--color-blue-500); --info-foreground: var(--color-blue-400); - --success: var(--color-emerald-500); --success-foreground: var(--color-emerald-400); - --warning: var(--color-amber-500); --warning-foreground: var(--color-amber-400); --warning-surface: color-mix(in srgb, var(--warning) 16%, transparent); - --update: var(--primary); --update-foreground: var(--color-blue-400); --update-surface: color-mix(in srgb, var(--update) 18%, transparent); --sidebar: var(--card); - --sidebar-foreground: var(--foreground); - --sidebar-muted-foreground: var(--muted-foreground); --sidebar-control-surface: var(--muted); --sidebar-row-hover: var(--accent); --sidebar-row-active: var(--accent); --sidebar-row-selected: var(--muted); - --sidebar-border: var(--border); --sidebar-stage-fade: var(--card); - --terminal-background: var(--background); - --terminal-foreground: var(--foreground); --terminal-cursor: rgb(180 203 255); --terminal-selection-background: rgb(180 203 255 / 25%); - --terminal-scrollbar: rgb(255 255 255 / 10%); - --terminal-scrollbar-hover: rgb(255 255 255 / 18%); } } @@ -1242,32 +1140,28 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil --sidebar-row-selected: var(--color-white); --sidebar-border: var(--color-zinc-200); --sidebar-stage-fade: var(--sidebar); - background-color: var(--sidebar); -} - -.dark [data-app-sidebar] { - --background: #000; - --foreground: #f1f3f7; - --card: #000; - --card-foreground: var(--foreground); - --accent: #191a1d; - --accent-foreground: #f7f9ff; - --muted: #0a0a0a; - --muted-foreground: #a3a3a3; - --border: rgb(255 255 255 / 8%); - --input: rgb(255 255 255 / 18%); - --sidebar: var(--card); - --sidebar-foreground: var(--foreground); - --sidebar-muted-foreground: var(--muted-foreground); - --sidebar-control-surface: var(--muted); - --sidebar-row-hover: color-mix(in srgb, var(--foreground) 8%, transparent); - --sidebar-row-active: color-mix(in srgb, var(--foreground) 11%, transparent); - --sidebar-row-selected: color-mix(in srgb, var(--foreground) 7%, transparent); - --sidebar-border: var(--border); - /* The stage-channel header art must ramp to THIS panel's surface, not the - global chrome background, or the fade shows a seam (same rule as the - light palette above). */ - --sidebar-stage-fade: var(--card); + + @variant dark { + --background: #000; + --foreground: #f1f3f7; + --card: #000; + --card-foreground: var(--foreground); + --accent: #191a1d; + --accent-foreground: #f7f9ff; + --muted: #0a0a0a; + --muted-foreground: #a3a3a3; + --border: rgb(255 255 255 / 8%); + --input: rgb(255 255 255 / 18%); + --sidebar: var(--card); + --sidebar-foreground: var(--foreground); + --sidebar-muted-foreground: var(--muted-foreground); + --sidebar-control-surface: var(--muted); + --sidebar-row-hover: color-mix(in srgb, var(--foreground) 8%, transparent); + --sidebar-row-active: color-mix(in srgb, var(--foreground) 11%, transparent); + --sidebar-row-selected: color-mix(in srgb, var(--foreground) 7%, transparent); + --sidebar-border: var(--border); + --sidebar-stage-fade: var(--card); + } } /* Theme files are expressed in app color roles and mapped to the existing @@ -1275,8 +1169,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil compatibility overrides so both navigation implementations receive the same palette. Success, info, provider, and channel identity colors remain independent; error, warning, and update roles are themeable below. */ -html[data-theme-id], -html.dark[data-theme-id] { +html[data-theme-id] { --background: var(--app-theme-canvas); --app-chrome-background: var(--app-theme-chrome); --toolbar-background: var(--app-theme-toolbar); @@ -1340,8 +1233,6 @@ html.dark[data-theme-id] { --terminal-foreground: var(--app-theme-terminal-foreground); --terminal-cursor: var(--app-theme-terminal-cursor); --terminal-selection-background: var(--app-theme-terminal-selection-background); - --terminal-scrollbar: var(--app-theme-terminal-scrollbar); - --terminal-scrollbar-hover: var(--app-theme-terminal-scrollbar-hover); } /* T3 Chat's composer is a translucent lift over --chat-background. Route its @@ -1349,35 +1240,21 @@ html.dark[data-theme-id] { another tint from the canvas, which made the dark composer too red. */ html[data-theme-id] .chat-composer-glass-shell { --chat-composer-glass-surface: var(--app-theme-surface-raised); -} - -html[data-theme-id]:not(.dark) .chat-composer-glass-shell { --chat-composer-outline: var(--app-theme-toolbar-border); -} -html.dark[data-theme-id] .chat-composer-glass-shell { - --chat-composer-outline: color-mix(in srgb, var(--app-theme-input) 30%, var(--background)); - --chat-composer-highlight: color-mix(in srgb, var(--app-theme-input) 12%, transparent); + @variant dark { + --chat-composer-outline: color-mix(in srgb, var(--app-theme-input) 30%, var(--background)); + --chat-composer-highlight: color-mix(in srgb, var(--app-theme-input) 12%, transparent); + } } -html.dark[data-theme-id="t3-chat"] .chat-composer-glass-shell { +html[data-theme-id="t3-chat"] .chat-composer-glass-shell { /* T3 Chat's visible composer edge is a dark plum, not the stock translucent white outline. Its highlight is derived from --chat-input-gradient. */ - --chat-composer-outline: #241e28; - --chat-composer-highlight: color-mix(in srgb, #432d48 12%, transparent); -} - -html[data-theme-id]:not(.dark) { - color-scheme: light; -} - -html.dark[data-theme-id] { - color-scheme: dark; -} - -html[data-theme-id] body { - background-color: var(--app-chrome-background); - color: var(--foreground); + @variant dark { + --chat-composer-outline: #241e28; + --chat-composer-highlight: color-mix(in srgb, #432d48 12%, transparent); + } } /* Theme-token dependency probes are restored synchronously, before paint. Keep @@ -1477,8 +1354,8 @@ html[data-theme-id] [data-chat-header] [data-toolbar-control] { toggle's when the trigger renders the toggle, so match both. */ html[data-theme-id] [data-panel-layout-controls] [data-slot="toggle"], html[data-theme-id] [data-panel-layout-controls] [data-slot="tooltip-trigger"], -html[data-theme-id] .workspace-titlebar-controls [data-slot="toggle"], -html[data-theme-id] .workspace-titlebar-controls [data-slot="tooltip-trigger"] { +html[data-theme-id] [data-workspace-titlebar-controls] [data-slot="toggle"], +html[data-theme-id] [data-workspace-titlebar-controls] [data-slot="tooltip-trigger"] { --control-icon-color: var(--toolbar-foreground); color: var(--toolbar-foreground); } @@ -1522,19 +1399,23 @@ html[data-theme-id] .chat-markdown .chat-markdown-chrome-action { /* T3 Chat renders inline code and compact chat artifacts with its translucent secondary surface flattened over the light chat canvas. The raw muted and secondary tokens are substantially darker than those visible pixels. */ -html[data-theme-id="t3-chat"]:not(.dark) .chat-markdown :not(pre) > code, -html[data-theme-id="t3-chat"]:not(.dark) [data-changed-files-state], -html[data-theme-id="t3-chat"]:not(.dark) [data-changed-files-header] { - background-color: var(--message-surface); -} +html[data-theme-id="t3-chat"] { + @variant light { + & .chat-markdown :not(pre) > code, + & [data-changed-files-state], + & [data-changed-files-header] { + background-color: var(--message-surface); + } -html[data-theme-id="t3-chat"]:not(.dark) .chat-markdown :not(pre) > code, -html[data-theme-id="t3-chat"]:not(.dark) [data-changed-files-state] { - border-color: transparent; -} + & .chat-markdown :not(pre) > code, + & [data-changed-files-state] { + border-color: transparent; + } -html[data-theme-id="t3-chat"]:not(.dark) .chat-markdown :not(pre) > code { - color: var(--message-foreground); + & .chat-markdown :not(pre) > code { + color: var(--message-foreground); + } + } } html[data-theme-id] .chat-markdown .chat-markdown-chrome-action:hover, @@ -1562,40 +1443,19 @@ html[data-theme-id] [data-app-sidebar] { --sidebar-row-selected: var(--app-theme-sidebar-row-selected); --sidebar-border: var(--app-theme-sidebar-border); --sidebar-stage-fade: var(--app-theme-sidebar); - background-color: var(--sidebar); -} - -/* Keep the navigation edge as quiet as the standard palettes. Theme files may - still use sidebarBorder for controls and internal separators, but the outer - divider should not become more prominent just because a palette is vivid. */ -html[data-theme-id] [data-app-sidebar] { border-color: color-mix(in srgb, var(--sidebar-foreground) 10%, transparent); -} -html.dark[data-theme-id] [data-app-sidebar] { - border-color: color-mix(in srgb, var(--sidebar-foreground) 8%, transparent); + @variant dark { + border-color: color-mix(in srgb, var(--sidebar-foreground) 8%, transparent); + } } /* T3 Chat's panel divider is deliberately pink, and its resize affordance keeps that color while hovered. Do not neutralize this branded edge. */ -html.dark[data-theme-id="t3-chat"] [data-app-sidebar] { - border-color: var(--sidebar-border); -} - -.theme-json-key { - color: var(--app-theme-accent, var(--color-blue-600)); -} - -.theme-json-string { - color: var(--app-theme-message-action, var(--color-emerald-600)); -} - -.theme-json-number { - color: var(--app-theme-secondary-foreground, var(--color-amber-600)); -} - -.theme-json-constant { - color: var(--app-theme-accent-surface-foreground, var(--color-violet-600)); +html[data-theme-id="t3-chat"] [data-app-sidebar] { + @variant dark { + border-color: var(--sidebar-border); + } } body { @@ -1715,125 +1575,7 @@ code { background: var(--app-scrollbar-thumb-hover); } -/* Settings -> Appearance can point the composer at its own face (for example a - mono font); default follows the sans stack. Applied on the surface wrapper so - the editor and its placeholder inherit together. */ -.composer-editor-surface { - font-family: var(--font-composer, var(--font-sans)); - font-size: var(--font-size-prompt, 0.875rem); -} - -/* Touch browsers zoom the page when a focused field is under 16px, so keep - the floor there regardless of the preference. Gated on a coarse pointer: - the zoom quirk does not exist on desktop, where a narrow window must not - silently override a smaller chosen prompt size. */ -@media (max-width: 39.999rem) and (pointer: coarse) { - .composer-editor-surface { - font-size: max(var(--font-size-prompt, 1rem), 16px); - } -} - -.t3-ghostty-canvas { - cursor: text; -} - -.t3-ghostty-scrollbar { - position: absolute; - z-index: 1; - top: 4px; - right: 1px; - bottom: 4px; - width: var(--app-scrollbar-width); - cursor: default; - touch-action: none; -} - -.t3-ghostty-scrollbar-thumb { - position: absolute; - top: 0; - right: 1px; - left: 1px; - border-radius: 3px; - background: var(--app-scrollbar-thumb); - transition: background-color 120ms ease-out; -} - -.t3-ghostty-scrollbar:hover .t3-ghostty-scrollbar-thumb, -.t3-ghostty-scrollbar:focus-visible .t3-ghostty-scrollbar-thumb { - background: var(--app-scrollbar-thumb-hover); -} - -.model-picker-list::-webkit-scrollbar-track { - margin-block: 0.5rem; -} - -.model-picker-list-scroll-fade-top, -.model-picker-list-scroll-fade-bottom { - -webkit-mask-image: var(--model-picker-list-scroll-mask), linear-gradient(black, black); - -webkit-mask-position: left, right; - -webkit-mask-repeat: no-repeat; - -webkit-mask-size: - calc(100% - var(--app-scrollbar-width)) 100%, - var(--app-scrollbar-width) 100%; - mask-image: var(--model-picker-list-scroll-mask), linear-gradient(black, black); - mask-position: left, right; - mask-repeat: no-repeat; - mask-size: - calc(100% - var(--app-scrollbar-width)) 100%, - var(--app-scrollbar-width) 100%; -} - -.model-picker-list-scroll-fade-top { - --model-picker-list-scroll-mask: linear-gradient(to bottom, transparent, black var(--fade-size)); -} - -.model-picker-list-scroll-fade-bottom { - --model-picker-list-scroll-mask: linear-gradient( - to bottom, - black calc(100% - var(--fade-size)), - transparent - ); -} - -.model-picker-list-scroll-fade-top.model-picker-list-scroll-fade-bottom { - --model-picker-list-scroll-mask: linear-gradient( - to bottom, - transparent, - black var(--fade-size), - black calc(100% - var(--fade-size)), - transparent - ); -} - -.turn-chip-strip { - scrollbar-width: none; - -ms-overflow-style: none; - overscroll-behavior-x: contain; -} - -.turn-chip-strip::-webkit-scrollbar { - display: none; -} - -/* Reasoning select -- clickable label surface */ -label:has(> select#reasoning-effort) { - position: relative; -} -label:has(> select#reasoning-effort) select { - position: absolute; - inset: 0; - opacity: 0; - cursor: pointer; - width: 100%; - height: 100%; -} - /* Chat markdown rendering */ -.chat-markdown { - min-width: 0; - overflow-wrap: anywhere; - word-break: break-word; -} .chat-markdown > :first-child { margin-top: 0; @@ -1945,18 +1687,6 @@ label:has(> select#reasoning-effort) select { background-size: 4px 2px; } -.chat-markdown .chat-markdown-link-favicon { - @apply inline-flex; - width: 14px; - height: 14px; - margin-inline: 0.25em 0.2em; - vertical-align: -0.125em; -} - -.chat-markdown .chat-markdown-link-leading { - white-space: nowrap; -} - .chat-markdown blockquote { border-left: 2px solid var(--border); padding-left: 0.8rem; @@ -2001,11 +1731,7 @@ label:has(> select#reasoning-effort) select { font-size: 0.75rem; } -.chat-markdown a.chat-markdown-file-link { - color: var(--foreground); - text-decoration: none; -} - +.chat-markdown a.chat-markdown-file-link, .chat-markdown a.chat-markdown-file-link:hover { color: var(--foreground); text-decoration: none; @@ -2023,75 +1749,26 @@ label:has(> select#reasoning-effort) select { border-radius: 0.75rem; background: var(--muted); padding: 0.8rem 0.9rem; + scrollbar-width: thin; + scrollbar-color: color-mix(in srgb, var(--border) 78%, transparent) transparent; } .chat-markdown pre code { border: none; background: transparent; padding: 0; - font-size: 0.75rem; -} - -.chat-markdown pre { - scrollbar-width: thin; - scrollbar-color: color-mix(in srgb, var(--border) 78%, transparent) transparent; } .chat-markdown pre::-webkit-scrollbar { height: 7px; } -.chat-markdown pre::-webkit-scrollbar-track { - background: transparent; -} - .chat-markdown pre::-webkit-scrollbar-thumb { border-radius: 999px; background: color-mix(in srgb, var(--border) 78%, transparent); } -.markdown-file-link-tooltip-scroll { - scrollbar-width: thin; - scrollbar-color: color-mix(in srgb, var(--border) 78%, transparent) transparent; -} - -.markdown-file-link-tooltip-scroll::-webkit-scrollbar { - height: 6px; -} - -.markdown-file-link-tooltip-scroll::-webkit-scrollbar-track { - background: transparent; -} - -.markdown-file-link-tooltip-scroll::-webkit-scrollbar-thumb { - border-radius: 999px; - background: color-mix(in srgb, var(--border) 78%, transparent); -} - -.chat-markdown .chat-markdown-codeblock { - margin: 0.65rem 0; - overflow: hidden; - border-radius: var(--radius); -} - -.chat-markdown .chat-markdown-codeblock-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.5rem; - padding: 0.375rem 0.375rem 0 0.75rem; - color: color-mix(in srgb, var(--foreground) 72%, transparent); -} - -.chat-markdown .chat-markdown-codeblock-title { - display: inline-flex; - min-width: 0; - align-items: center; - gap: 0.4rem; - font-family: var(--font-mono, ui-monospace, SFMono-Regular, monospace); - font-size: 0.6875rem; -} - +.chat-markdown .chat-markdown-codeblock-header, .chat-markdown .chat-markdown-chrome-action { color: color-mix(in srgb, var(--foreground) 72%, transparent); } @@ -2167,13 +1844,6 @@ label:has(> select#reasoning-effort) select { overflow-wrap: anywhere; } -.chat-markdown .chat-markdown-table-footer { - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 0.125rem; -} - /* Prompt-stash save acknowledgement: the new count fades up from just below its resting position, once, then stops. One-shot and event-driven (React remounts the element by key on each stash) — no continuous animation. */ @@ -2188,19 +1858,6 @@ label:has(> select#reasoning-effort) select { } } -.prompt-stash-count-enter { - animation: prompt-stash-count-enter 180ms ease-out both; -} - -@media (prefers-reduced-motion: reduce) { - .prompt-stash-count-enter { - animation: none; - } - [data-slot="skeleton"]::after { - content: none; - } -} - @keyframes provider-update-pill-countdown { from { transform: scaleX(1); @@ -2210,23 +1867,6 @@ label:has(> select#reasoning-effort) select { } } -.provider-update-pill-progress { - animation: provider-update-pill-countdown var(--provider-update-pill-dismiss-ms) linear forwards; -} - -/* Diffs theme bridge (match diff surfaces to app palette) */ -.diff-panel-viewport { - background: var(--background); -} - -/* Diffs live directly on the panel canvas. Normal chat code blocks may use a - raised code surface, but carrying that fill into the diff creates a card-like - rectangle that does not belong in the panel. */ -.diff-render-surface { - --code-background: var(--background); -} - -.diff-render-file, .diff-render-surface diffs-container { border: 0; border-radius: 0; @@ -2307,40 +1947,3 @@ label:has(> select#reasoning-effort) select { .ultrathink-chroma { animation: ultrathink-chroma-shift 10s linear infinite; } - -.ultrathink-pill { - background: - linear-gradient(var(--card), var(--card)) padding-box, - var(--ultrathink-spectrum) border-box; - background-size: - 100% 100%, - 220% 220%; - background-position: - 0 0, - 0% 50%; - animation: ultrathink-rainbow 10s linear infinite; - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--card) 82%, transparent); -} - -.ultrathink-word { - display: inline-block; - color: transparent; - background-image: var(--ultrathink-spectrum); - background-size: 220% 220%; - background-position: 0% 50%; - background-clip: text; - -webkit-background-clip: text; - animation: ultrathink-rainbow 10s linear infinite; -} - -/* Composer chips are non-editable decorators, so the browser skips them when - painting text selection; this overlay stands in for the native highlight. */ -.composer-inline-chip[data-composer-chip-selected]::after { - content: ""; - position: absolute; - inset: 0; - border-radius: 6px; - background-color: Highlight; - opacity: 0.3; - pointer-events: none; -} diff --git a/apps/web/src/routes/-chatIndexTitlebar.test.ts b/apps/web/src/routes/-chatIndexTitlebar.test.ts index 5e74103a5421..803ba787116b 100644 --- a/apps/web/src/routes/-chatIndexTitlebar.test.ts +++ b/apps/web/src/routes/-chatIndexTitlebar.test.ts @@ -1,4 +1,5 @@ -// @effect-diagnostics nodeBuiltinImport:off - Regression coverage compares the onboarding header with the shared titlebar contract. +// @effect-diagnostics nodeBuiltinImport:off +// Regression coverage compares the onboarding header with the shared titlebar contract. import * as NodeFS from "node:fs"; import { describe, expect, it } from "vite-plus/test"; @@ -14,7 +15,9 @@ describe("hosted static onboarding header", () => { const onboardingHeader = routeSource.slice(onboardingStart, onboardingEnd); - expect(onboardingHeader).toContain("workspace-topbar"); + expect(onboardingHeader).toContain("h-[var(--workspace-topbar-height)]"); + expect(onboardingHeader).toContain("min-h-[var(--workspace-topbar-height)]"); + expect(onboardingHeader).toContain("COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS"); expect(onboardingHeader).not.toMatch(/(?:^|\s)(?:[\w-]+:)*py-/); }); }); diff --git a/apps/web/src/routes/_chat.index.tsx b/apps/web/src/routes/_chat.index.tsx index 12bbdf666c43..4f4da0c751ef 100644 --- a/apps/web/src/routes/_chat.index.tsx +++ b/apps/web/src/routes/_chat.index.tsx @@ -145,7 +145,7 @@ function HostedStaticOnboardingState() {
diff --git a/apps/web/src/routes/_chat.pull-requests.tsx b/apps/web/src/routes/_chat.pull-requests.tsx index aee41fb696d0..66d9f0caa5da 100644 --- a/apps/web/src/routes/_chat.pull-requests.tsx +++ b/apps/web/src/routes/_chat.pull-requests.tsx @@ -1308,7 +1308,8 @@ function PullRequestsRouteView() { // anchor the thread view's controls and the sidebar trigger use, so // every titlebar cluster in the app sits one shared inset from its // edge. - className="workspace-titlebar-controls z-50 mr-px gap-1 [-webkit-app-region:no-drag]" + className="absolute top-[var(--workspace-controls-top)] right-[var(--workspace-controls-right)] z-50 mr-px flex h-[var(--workspace-topbar-height)] items-center gap-1 [-webkit-app-region:no-drag]" + data-workspace-titlebar-controls > {panelToggleControls}
@@ -1828,7 +1829,7 @@ function PullRequestsColumn({
{/* The top padding is the fade band's own height (1.5rem here), the same pairing the settings page makes: at rest the controls sit fully below the mask, and only diff --git a/apps/web/src/routes/settings.tsx b/apps/web/src/routes/settings.tsx index f14793ba5446..a4b248c84ed9 100644 --- a/apps/web/src/routes/settings.tsx +++ b/apps/web/src/routes/settings.tsx @@ -75,7 +75,7 @@ function SettingsContentLayout() { {!isElectron && (
diff --git a/apps/web/src/terminal/ghostty/surface.ts b/apps/web/src/terminal/ghostty/surface.ts index 8a9c796b948b..0bb33875568f 100644 --- a/apps/web/src/terminal/ghostty/surface.ts +++ b/apps/web/src/terminal/ghostty/surface.ts @@ -578,8 +578,7 @@ export class GhosttyTerminalSurface { options: GhosttyTerminalSurfaceOptions, ): Promise { const canvas = document.createElement("canvas"); - canvas.className = "t3-ghostty-canvas"; - canvas.style.cssText = "display:block;width:100%;height:100%;"; + canvas.className = "block size-full cursor-text"; canvas.setAttribute("aria-hidden", "true"); const input = document.createElement("textarea"); @@ -592,14 +591,16 @@ export class GhosttyTerminalSurface { "position:absolute;left:4px;top:4px;width:1px;height:1px;opacity:0;padding:0;border:0;resize:none;pointer-events:none;"; const scrollbar = document.createElement("div"); - scrollbar.className = "t3-ghostty-scrollbar"; + scrollbar.className = + "group absolute top-1 right-px bottom-1 z-1 w-[var(--app-scrollbar-width)] cursor-default touch-none"; scrollbar.setAttribute("role", "scrollbar"); scrollbar.setAttribute("aria-label", "Terminal scrollback"); scrollbar.setAttribute("aria-orientation", "vertical"); scrollbar.tabIndex = 0; scrollbar.hidden = true; const scrollbarThumb = document.createElement("div"); - scrollbarThumb.className = "t3-ghostty-scrollbar-thumb"; + scrollbarThumb.className = + "absolute inset-x-px top-0 rounded-[3px] bg-[var(--app-scrollbar-thumb)] transition-[background-color] duration-[120ms] ease-[ease-out] group-hover:bg-[var(--app-scrollbar-thumb-hover)] group-focus-visible:bg-[var(--app-scrollbar-thumb-hover)]"; scrollbar.append(scrollbarThumb); mount.replaceChildren(canvas, input, scrollbar);