From 9885a845c97325b1099b095011da8385485616f5 Mon Sep 17 00:00:00 2001 From: "t3-code[bot]" <269035359+t3-code[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 23:30:13 +0200 Subject: [PATCH] refactor(web): simplify global styling (#6381) Co-authored-by: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Co-authored-by: Julius Marminge --- .../check-run-agents/ui-consistency.md | 82 ++ apps/web/src/components/AgentsPanel.tsx | 18 +- .../BranchToolbarBranchSelector.tsx | 9 +- apps/web/src/components/ChatMarkdown.tsx | 23 +- apps/web/src/components/ChatView.tsx | 18 +- .../src/components/ComposerPromptEditor.tsx | 12 +- apps/web/src/components/DiffPanel.tsx | 9 +- apps/web/src/components/DiffPanelShell.tsx | 2 +- apps/web/src/components/LegacySidebar.tsx | 7 +- .../src/components/NoActiveThreadState.tsx | 4 +- apps/web/src/components/RightPanelTabs.tsx | 13 +- apps/web/src/components/Sidebar.tsx | 13 +- .../src/components/ThreadTerminalDrawer.tsx | 9 +- .../components/chat/ComposerCommandMenu.tsx | 2 +- .../ComposerPreviewAnnotationCards.test.tsx | 14 + .../chat/ComposerPreviewAnnotationCards.tsx | 10 +- .../components/chat/ComposerStashBadge.tsx | 2 +- .../src/components/chat/ComposerStashMenu.tsx | 2 +- .../components/chat/ContextWindowMeter.tsx | 15 +- .../components/chat/MessagesTimeline.test.tsx | 4 +- .../src/components/chat/MessagesTimeline.tsx | 2 +- .../components/chat/ModelPickerContent.tsx | 11 +- .../components/chat/ProviderStatusBanner.tsx | 10 +- apps/web/src/components/composerInlineChip.ts | 3 + .../diffs/StyledDiffCodeView.test.tsx | 4 +- .../components/diffs/StyledDiffCodeView.tsx | 4 +- .../src/components/files/FileBrowserPanel.tsx | 7 +- .../src/components/files/FilePreviewPanel.tsx | 5 +- .../components/preview/PreviewChromeRow.tsx | 8 +- .../pullRequest/PullRequestCodeTab.tsx | 24 +- .../pullRequest/PullRequestDetailPanel.tsx | 10 +- .../pullRequest/PullRequestListFilters.tsx | 26 +- .../pullRequest/PullRequestReviewerPicker.tsx | 5 +- .../search/ProjectContentSearchDialog.tsx | 15 +- .../settings/DiagnosticsSettings.tsx | 26 +- .../settings/KeybindingsSettings.tsx | 98 +- .../settings/ProjectSettingsPanel.tsx | 2 +- .../settings/ProviderInstanceCard.tsx | 9 +- .../settings/ProviderModelsSection.tsx | 33 +- .../settings/ProviderSettingsPanel.tsx | 10 +- .../settings/ResourceTelemetryDiagnostics.tsx | 14 +- .../settings/SettingsSidebarNav.tsx | 4 +- .../settings/SourceControlSettings.tsx | 45 +- .../components/settings/ThemeImportDialog.tsx | 8 +- .../settings/ThemeSearchSection.tsx | 30 +- .../components/settings/settingsLayout.tsx | 18 +- .../src/components/sidebar/SidebarChrome.tsx | 2 +- .../sidebar/SidebarProviderUpdatePill.tsx | 12 +- .../src/components/threadSidebarWidth.test.ts | 19 +- apps/web/src/components/ui/button.test.tsx | 15 + apps/web/src/components/ui/button.tsx | 8 + apps/web/src/components/ui/combobox.tsx | 2 +- apps/web/src/components/ui/input-group.tsx | 4 +- apps/web/src/components/ui/input.tsx | 6 +- apps/web/src/components/ui/menu.tsx | 10 +- apps/web/src/components/ui/popover.tsx | 2 + apps/web/src/components/ui/scroll-area.tsx | 19 +- apps/web/src/components/ui/select.tsx | 4 +- apps/web/src/components/ui/sidebar.tsx | 2 + apps/web/src/components/ui/skeleton.tsx | 2 +- apps/web/src/components/ui/toast.tsx | 27 +- apps/web/src/components/ui/toggle.tsx | 2 + apps/web/src/components/usage/UsagePage.tsx | 11 +- apps/web/src/index.css | 1099 ++++++----------- .../web/src/routes/-chatIndexTitlebar.test.ts | 7 +- apps/web/src/routes/_chat.index.tsx | 2 +- apps/web/src/routes/_chat.pull-requests.tsx | 7 +- apps/web/src/routes/settings.tsx | 2 +- apps/web/src/terminal/ghostty/surface.ts | 9 +- 69 files changed, 850 insertions(+), 1122 deletions(-) create mode 100644 .macroscope/check-run-agents/ui-consistency.md diff --git a/.macroscope/check-run-agents/ui-consistency.md b/.macroscope/check-run-agents/ui-consistency.md new file mode 100644 index 0000000000000000000000000000000000000000..8ec720742759fdd234425cea920a32ecbba865db --- /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 05ed533acbc2e0013f1bac9968ce44b5d6e888b1..b3c1c08eb7301f902e695f476581adadf46081f0 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 e9390ed0a8aa7bf436acf7a8c267180137359ed5..53b043f3a8ff32f551bc9701ff8bc0f6c277c810 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 0489e8c79cdf5a929733936659803fd92ef0887a..f6dfef2489b47487132fdddb7511f460b1478e57 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 385d67b6b70165858d19604e37889b1069016bbe..b929d05a719b3777050d5d8bd211fae2fdffbf3f 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 68a5855c1a28759a25d4a3053f91699a785e44da..82dddd8f41e0e45b2390031b6c3661f959f9f7e7 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 df65aa60d520af6dfcd1bedca92dca1b880a9e0b..b91e81bc7a0b7bc6cde65f4247663430a7713a07 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 dc8be07dcad1b4a1713030c84f72eeb7f43d8f79..2f0c5a22140559c6883c9f7c15a4e58a1f3b0aff 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 87f0ed4ae7068b255085237dc29417c32dd12821..1266e5ed7e949ea790cc8846b96bb063b4537daa 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 3ed2a9432e48ad9c72876266e8ed5225bc1ba239..4f32211c10cc0d01ca782767acc32d715eae2868 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 5bb28054e7d7ba3d119786dd82522273cbdeb262..46af299073c56cc88964cc017c8079a8567fca39 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 5e9e43dcf21e40502964a9ff2293683d1c78d142..19f6a5ca308e9de512c9498c38e067b3ad37af98 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 79ed301a5d56017df8a2625c0650aeeb6d6b8305..a2599ebc9f9f196161d52b964a684bb29fbffeee 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 9e9238515332b5a35e371cac50a32dc482374fe2..fc8be327da182c599800f6334bfe78e6e36589c2 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 752ec4013768d7ffb51b6982cf4258be51ada616..f377c893ae2c72567a13af4803eafd0b5ac46573 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 9cd1c78aa4d5fe9b7304606a6bd1256347f17421..e190f47569b2afd5b817310af012cc5a11143fb1 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 7c86ec6301413a2663459a1b29897e2a10e9c484..7ffb2bf077da859d309757d33026a3b96808b393 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 f82c17b13ddde00e989abc3efc972eeb11b8c9f9..1c7571b962f999ad07297fb429dbde42480d2b17 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 c17b3ddab3c04dac4eba8a37271dc9fcfaac10d1..3f0e8ca1ac007420ff76542922425f99e000efcd 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 dbdd10d194f684e7a0c39c6b4e871db2f1946914..f0cd49abc41d9dbbfd5f736a3d3c5d52cf4f570a 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 7dbd5358a0ff8703a30880fb1ca77b8dee4e8a89..14939de098200f3e26b0e85000d3400adab48327 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 ff658693a70c2e0bc45a3ff43e714b50b5042738..e3280c99caa3b680f46418628a398e70eadf7697 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 7237b4357481922931aec5fe9712eb8685276e58..2f4e84dc3fd2bc010afba7470215022b7e6c2597 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 dd4a9d161cdced985cf4cd99995429530e77f555..3066eafc38a19ab34adc9aa5a682aaba01d4cf02 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 25c663794e9785bb7d7dba72a4407b3fed544ac6..8330c87a9294dd0bee6a4e4b89076eda5dbf14d5 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 1890aab7fa79d04f3e77a4411e0913baef273054..d877d6537bda36c468827d4945707e31f3fd92cf 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 5bd4fdc08c4a9e6066284ea0ad0983d90e8f6688..a472c6a8d3d7bef1f6cbbfb6013562d7e68dc7af 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 175a1c3d062015481bea3dedf38cfcf8728b34a4..f4a98dec86c7366b7aed6a9885baabe15c139b02 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 3beb2a8f5130f0f80d4d6cd5338606bc5cf1bb86..e38d5c3749bc365481248a4fad2edd52e7e52b52 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 341d85b42bbcde64e2d46e68cd2e861187ac4d8a..e1bd89d94cdb23c2de0b28377d38007ff1ddcf21 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 778574ba01079764316e9c5b018aa77646f1274d..9f0b4d049eda487fcb8be33ad4e74c92399e50c2 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 324b67e64d90471debd28e23d83b321b5e841e24..cf3a46142ad294470abcaec6aff17d16c86f333f 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 2ac9ee1ed41ab4ab4d02a872ce52ec02458e6dec..04e34e5611ee85d3f8cf8e5c553a9d0b22bbda85 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 6edc8d4a62fc56cf2aecc8a969dede7c1d2e0a20..cae3dfe628527958057845ea7a55029acee902cb 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 9f7cfc8c0670d4415d630b9f85d4d26201ec24f0..803d6c1987c10f5fd4a28969ffe4a03b00f92579 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 6169e1a643cacd478bbc249ad17d2b206e046a56..4e636eb4ff0f0b181dd0c5f9c3c02dcde25e9b47 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 5e74103a5421320de03d3f70d4a20a42492f31c4..803ba787116bc4d1d1b74e24fe1bbcaedc18bfba 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 12bbdf666c43b78f5aba9e21adf15b37275e1f14..4f4da0c751effcc86f03e89cc71823fcdc219ce2 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 aee41fb696d0f1499f7ff7258c37b1f74116b01e..66d9f0caa5da32342a78775aa208e630c03b7385 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 f14793ba54465e902d61f51dd4366817e78f35b0..a4b248c84ed985fa7e902c5319a93d1064604638 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 8a9c796b948b23cb9f30607605d8c1398d9c5fab..0bb33875568f8f0469e1816e44545b014cd598aa 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); -- 2.54.0