authorgravatar for 269035359+t3-code[bot]@users.noreply.github.comt3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> 2026-08-14 23:30:13+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-14 23:30:13+02:00
log9885a845c97325b1099b095011da8385485616f5
tree31e6c8fca18195f58e1584a223323bca4ed2c734
parent196c8ea0d642acd1db66bd57f5d98abe81d8da6e
signature Signed by PGP key B5690EEEBB952194

refactor(web): simplify global styling (#6381)

Co-authored-by: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Co-authored-by: Julius Marminge <julius0216@outlook.com>

69 files changed, 851 insertions(+), 1123 deletions(-)

.macroscope/check-run-agents/ui-consistency.md created+82
...@@ -0,0 +1,82 @@
1---
2title: UI Consistency
3model: claude-opus-5
4effort: high
5input: full_diff
6tools:
7 - browse_code
8 - git_tools
9 - github_api_read_only
10 - modify_pr
11include:
12 - "apps/web/src/**/*.ts"
13 - "apps/web/src/**/*.tsx"
14 - "apps/web/src/**/*.css"
15conclusion: failure
16showToolCalls: true
17---
18
19# UI consistency review
20
21Review 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.
22
23The 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.
24
25## Shared controls and variants
26
27- Prefer the core UI primitives in `apps/web/src/components/ui` over native controls or locally reconstructed primitives. In ordinary product UI, a raw `<button>` that recreates `Button`, a raw input that recreates `Input` or `InputGroup`, or a local trigger that recreates `Select`, `Toggle`, or `Menu` is a concrete finding.
28- Do not flag raw elements that intentionally implement a semantic row, tab, resize handle, swatch, image target, editor surface, or another interaction whose behavior or geometry differs from the core primitive.
29- When multiple call sites repeat the same durable geometry or treatment, prefer a named primitive size or variant. Examples include compact controls, micro icon actions, muted ghost actions, and glass actions. Keep contextual layout, width, and color at the call site.
30- Flag large call-site class strings that override a primitive's core height, radius, padding, focus ring, cursor, hit target, or base state colors. Prefer extending the primitive contract when the same pattern is genuinely shared.
31- Preserve accessibility and interaction semantics during migrations: focus-visible rings, disabled behavior, loading state, keyboard behavior, pointer cursor, `aria-*`, Base UI or Radix render/close props, and coarse-pointer hit targets.
32- Do not require tests for a tiny visual-only class migration. Require focused tests when primitive composition changes behavior, prop forwarding, state transitions, keyboard handling, or width/defaulting logic.
33
34## CSS and Tailwind ownership
35
36- Ordinary one-owner presentation belongs in the owning TS or TSX module as static Tailwind classes or owner-level CSS variables. Examples include local geometry, spacing, typography, backgrounds, borders, simple vendor pseudo-elements, and component-only positioning.
37- Keep global CSS when it is genuinely reusable or behaviorally complex: generated markdown or imperative DOM, custom elements and shadow roots, masks, shared or complex pseudo-elements, animations, glass composition, runtime theme variables, safe-area calculations, scrollbar-lane preservation, Electron drag regions, and browser/vendor integration. Simple owner-local pseudo-elements may still belong in the owning module.
38- Before calling a selector dead, trace literal, dynamic, generated, imperative, test, custom-element, and shadow-root consumers. Search both the emitted class string and any class-valued field names through their final DOM sink. A helper returning a class is not proof that it is rendered, and a missed downstream property read can make a deletion unsafe.
39- Flag duplicate declarations only after comparing cascade layer, selector specificity, inheritance, runtime theme scope, media/variant scope, and the final owning element. Textually identical declarations are not necessarily behaviorally redundant.
40- When moving CSS into Tailwind, preserve selector scope and cascade ownership. A utility at the owner is preferable to a fragile global override that depends on stylesheet order.
41- Do not request moving complex global behavior into arbitrary Tailwind merely to reduce `index.css`. Do not preserve ordinary one-owner CSS merely because it already exists globally.
42
43## Themes and generated CSS
44
45- Use the project variants for theme-only declarations:
46 - dark-only declarations use `@variant dark`;
47 - light-only declarations use `@variant light`;
48 - raw `.dark` should remain only in the `dark` and `light` custom-variant definitions.
49- Preserve custom themes and runtime token bridges. Removing a variable or selector is safe only when all runtime, inspector, generated, and theme-palette consumers are accounted for.
50- Inspect emitted production CSS after unusual variants, arbitrary selectors, nested pseudo-elements, or attribute matching. Source syntax that looks valid is insufficient.
51- Flag malformed or empty emitted selectors such as empty `:is()` or `:not(:is())`, selector branches that can never match their own class attribute, and transformations that silently drop the intended rule.
52- Prefer source-level logic over clever selectors when behavior depends on consumer-provided class strings. Preserve `MenuPopup`'s current defaulting contract: a string `className` containing a `w-*`, `min-w-*`, or `max-w-*` utility after variant prefixes are stripped suppresses `min-w-32`; a string without one and a functional/non-string `className` keep the default. Arbitrary width values count as width utilities, and the consumer class must be merged last so it retains control. Do not replace this with a raw class-attribute substring selector.
53- Do not fail solely because a valid emitted selector is verbose or because a source rule uses an intentional custom property.
54
55## Scroll and virtualized lists
56
57- `ScrollArea` owns and masks its Base UI viewport. A virtualized list usually owns a native scrolling element and cannot automatically reuse viewport-specific `ScrollArea` behavior.
58- Repeated native or virtualized overflow fades should use the shared virtualized-scroll-fade contract rather than component-named mask selectors.
59- Preserve runtime top and bottom overflow state. Do not replace dynamic fades with an always-on static mask.
60- Preserve fade geometry and keep the native scrollbar lane opaque so the track and thumb stay visible and usable. A visually similar mask that fades the scrollbar is a regression.
61- Verify actual scroll behavior when changing virtualizers, masks, overflow ownership, or scrollbar selectors. Source-level class comparison is not enough.
62
63## Visual and layout preservation
64
65- Preserve responsive geometry, titlebar insets, panel and inline-preview modes, desktop Electron layout, light and dark contrast, clipping, radius, and composable shadows.
66- For meaningful visual changes, prefer available real-app evidence using the actual component and state. A mock recreation does not validate the real component. Light and dark evidence is useful when theme-sensitive styles change, but missing or inaccessible evidence alone is not a finding; report only a concrete regression supported by the diff, code, or available artifacts.
67- Do not treat a screenshot as proof of keyboard, overflow, scrollbar, responsive, or runtime-theme behavior. Pair visual evidence with source, computed-style, emitted-CSS, or interaction checks as appropriate.
68- Be alert to shared primitive color indirection. When a primitive routes icon color through a CSS variable, ensure migrated contextual icons retain their intended tone, including pressed and disabled states.
69
70## Change discipline
71
72- Review the pull request's changed scope and directly affected consumers. Do not turn a focused PR into a demand for unrelated legacy cleanup.
73- Prefer the smallest durable contract over a component-specific workaround or a broad abstraction with one consumer.
74- Preserve intentional exceptions and comments that explain browser, virtualizer, theme, or Electron constraints.
75- If a proposed cleanup cannot prove ownership or semantic equivalence, ask for evidence or leave it unchanged rather than guessing.
76- Select verification gates according to the changed behavior: typecheck or focused tests for typing and interaction contracts, production build and emitted-CSS inspection for Tailwind or selector transformations, and real-app evidence for meaningful visual behavior when available. These gates are complementary when applicable, but do not require every gate for tiny visual-only migrations or fail solely because an artifact the configured tools cannot produce is absent.
77
78## Reporting
79
80Report only concrete violations introduced by changed lines or behavior, plus pre-existing behavior that the patch directly makes relevant or worsens. Touching a large file does not make unrelated retained issues reportable. Prefer precise inline comments on the smallest relevant line range. Explain the broken behavior or ownership rule, not merely the preferred syntax, and state the smallest expected fix. A clear consistency or regression risk may fail the check. Do not fail for optional aesthetic preferences, harmless class ordering, or unrelated legacy code.
81
82This check defaults to failure. When there are no findings, stop immediately and make the entire final response exactly `All clear` on one line. Do not add a title, explanation, punctuation, Markdown, JSON, or trailing analysis, and do not continue reasoning after deciding the review is clean.
apps/web/src/components/AgentsPanel.tsx+10-8
...@@ -28,6 +28,7 @@ import { useEffect, useRef, useState } from "react";...@@ -28,6 +28,7 @@ import { useEffect, useRef, useState } from "react";
28import { cn } from "~/lib/utils";28import { cn } from "~/lib/utils";
29import { orchestrationEnvironment } from "~/state/orchestration";29import { orchestrationEnvironment } from "~/state/orchestration";
30import { ScrollArea } from "~/components/ui/scroll-area";30import { ScrollArea } from "~/components/ui/scroll-area";
31import { Button } from "~/components/ui/button";
3132
32/**33/**
33 * In-flight states all present as Working (one steady state, per the34 * In-flight states all present as Working (one steady state, per the
...@@ -282,14 +283,15 @@ function WorkflowScriptView({...@@ -282,14 +283,15 @@ function WorkflowScriptView({
282 <span className="truncate font-mono text-[.65rem] text-muted-foreground">283 <span className="truncate font-mono text-[.65rem] text-muted-foreground">
283 {scriptPath.split("/").at(-1)}284 {scriptPath.split("/").at(-1)}
284 </span>285 </span>
285 <button286 <Button
286 type="button"287 size="icon-micro"
288 variant="ghost-muted"
287 onClick={onClose}289 onClick={onClose}
288 aria-label="Close script"290 aria-label="Close script"
289 className="ml-auto text-muted-foreground hover:text-foreground"291 className="ml-auto"
290 >292 >
291 <X aria-hidden className="size-3" />293 <X aria-hidden className="size-3" />
292 </button>294 </Button>
293 </div>295 </div>
294 <div className="max-h-72 overflow-auto p-2">296 <div className="max-h-72 overflow-auto p-2">
295 {result._tag === "Success" ? (297 {result._tag === "Success" ? (
...@@ -417,14 +419,14 @@ function ExpandedWorkflowSection({...@@ -417,14 +419,14 @@ function ExpandedWorkflowSection({
417 <span className="ml-auto font-mono normal-case text-muted-foreground/80">419 <span className="ml-auto font-mono normal-case text-muted-foreground/80">
418 {settled}/{members.length} settled420 {settled}/{members.length} settled
419 </span>421 </span>
420 <button422 <Button
421 type="button"423 size="icon-micro"
424 variant="ghost-muted"
422 onClick={onCollapse}425 onClick={onCollapse}
423 aria-label="Collapse workflow"426 aria-label="Collapse workflow"
424 className="text-muted-foreground hover:text-foreground"
425 >427 >
426 <ChevronDown aria-hidden className="size-3" />428 <ChevronDown aria-hidden className="size-3" />
427 </button>429 </Button>
428 </div>430 </div>
429 <PhaseRail group={group} />431 <PhaseRail group={group} />
430 {scriptOpen && canShowScript ? (432 {scriptOpen && canShowScript ? (
apps/web/src/components/BranchToolbarBranchSelector.tsx+6-3
...@@ -51,6 +51,7 @@ import {...@@ -51,6 +51,7 @@ import {
51} from "./ThreadStatusIndicators";51} from "./ThreadStatusIndicators";
52import { Button } from "./ui/button";52import { Button } from "./ui/button";
53import { Switch } from "./ui/switch";53import { Switch } from "./ui/switch";
54import { getVirtualizedScrollFadeClassName } from "./ui/scroll-area";
54import {55import {
55 Combobox,56 Combobox,
56 ComboboxEmpty,57 ComboboxEmpty,
...@@ -814,9 +815,11 @@ export function BranchToolbarBranchSelector({...@@ -814,9 +815,11 @@ export function BranchToolbarBranchSelector({
814 maybeFetchNextBranchPage();815 maybeFetchNextBranchPage();
815 }}816 }}
816 className={cn(817 className={cn(
817 "scrollbar-gutter-stable overflow-x-hidden overscroll-y-contain ps-1 pe-0 pt-2 pb-1 [--fade-size:1.5rem]",818 "scrollbar-gutter-stable overflow-x-hidden overscroll-y-contain ps-1 pe-0 pt-2 pb-1",
818 showTopBranchScrollFade && "mask-t-from-[calc(100%-var(--fade-size))]",819 getVirtualizedScrollFadeClassName({
819 showBottomBranchScrollFade && "mask-b-from-[calc(100%-var(--fade-size))]",820 top: showTopBranchScrollFade,
821 bottom: showBottomBranchScrollFade,
822 }),
820 )}823 )}
821 style={{ maxHeight: "14rem" }}824 style={{ maxHeight: "14rem" }}
822 />825 />
apps/web/src/components/ChatMarkdown.tsx+13-10
...@@ -449,7 +449,7 @@ function MarkdownTable({ children, ...props }: React.ComponentProps<"table">) {...@@ -449,7 +449,7 @@ function MarkdownTable({ children, ...props }: React.ComponentProps<"table">) {
449 {children}449 {children}
450 </table>450 </table>
451 </ScrollArea>451 </ScrollArea>
452 <div className="chat-markdown-table-footer select-none">452 <div className="mt-0.5 flex items-center justify-between select-none">
453 <Tooltip>453 <Tooltip>
454 <TooltipTrigger454 <TooltipTrigger
455 render={455 render={
...@@ -642,12 +642,12 @@ function MarkdownCodeBlock({...@@ -642,12 +642,12 @@ function MarkdownCodeBlock({
642642
643 return (643 return (
644 <div644 <div
645 className="chat-markdown-codeblock border border-border/70 bg-secondary leading-snug dark:border-transparent dark:bg-input/32"645 className="chat-markdown-codeblock my-[0.65rem] overflow-hidden rounded-[var(--radius)] border border-border/70 bg-secondary leading-snug dark:border-transparent dark:bg-input/32"
646 data-language={language}646 data-language={language}
647 data-wrap={wrapped ? "true" : "false"}647 data-wrap={wrapped ? "true" : "false"}
648 >648 >
649 <div className="chat-markdown-codeblock-header select-none">649 <div className="chat-markdown-codeblock-header flex items-center justify-between gap-2 pt-1.5 pr-1.5 pb-0 pl-3 select-none">
650 <span className="chat-markdown-codeblock-title">650 <span className="inline-flex min-w-0 items-center gap-[0.4rem] [font-family:var(--font-mono,ui-monospace,SFMono-Regular,monospace)] [font-size:0.6875rem]">
651 <MarkdownCodeBlockTitleContent651 <MarkdownCodeBlockTitleContent
652 fenceTitle={fenceTitle}652 fenceTitle={fenceTitle}
653 language={language}653 language={language}
...@@ -897,7 +897,10 @@ const failedFaviconHosts = new Set<string>();...@@ -897,7 +897,10 @@ const failedFaviconHosts = new Set<string>();
897const MarkdownLinkFavicon = memo(function MarkdownLinkFavicon({ host }: { host: string }) {897const MarkdownLinkFavicon = memo(function MarkdownLinkFavicon({ host }: { host: string }) {
898 const [failedHost, setFailedHost] = useState<string | null>(null);898 const [failedHost, setFailedHost] = useState<string | null>(null);
899 return (899 return (
900 <span className="chat-markdown-link-favicon" aria-hidden>900 <span
901 className="ms-[0.25em] me-[0.2em] inline-flex size-[14px] [vertical-align:-0.125em]"
902 aria-hidden
903 >
901 {failedHost === host || failedFaviconHosts.has(host) ? (904 {failedHost === host || failedFaviconHosts.has(host) ? (
902 <GlobeIcon className={MARKDOWN_LINK_FAVICON_CLASS_NAME} />905 <GlobeIcon className={MARKDOWN_LINK_FAVICON_CLASS_NAME} />
903 ) : (906 ) : (
...@@ -1044,7 +1047,7 @@ function MarkdownExternalLinkContent({...@@ -1044,7 +1047,7 @@ function MarkdownExternalLinkContent({
1044 const leadingLength = leadingExternalLinkTextLength(plainText);1047 const leadingLength = leadingExternalLinkTextLength(plainText);
1045 return (1048 return (
1046 <>1049 <>
1047 <span className="chat-markdown-link-leading">1050 <span className="whitespace-nowrap">
1048 <MarkdownLinkFavicon host={host} />1051 <MarkdownLinkFavicon host={host} />
1049 {plainText.slice(0, leadingLength)}1052 {plainText.slice(0, leadingLength)}
1050 </span>1053 </span>
...@@ -1060,7 +1063,7 @@ function MarkdownExternalLinkContent({...@@ -1060,7 +1063,7 @@ function MarkdownExternalLinkContent({
1060 const leadingLength = leadingExternalLinkTextLength(firstChild);1063 const leadingLength = leadingExternalLinkTextLength(firstChild);
1061 return (1064 return (
1062 <>1065 <>
1063 <span className="chat-markdown-link-leading">1066 <span className="whitespace-nowrap">
1064 <MarkdownLinkFavicon host={host} />1067 <MarkdownLinkFavicon host={host} />
1065 {firstChild.slice(0, leadingLength)}1068 {firstChild.slice(0, leadingLength)}
1066 </span>1069 </span>
...@@ -1072,7 +1075,7 @@ function MarkdownExternalLinkContent({...@@ -1072,7 +1075,7 @@ function MarkdownExternalLinkContent({
10721075
1073 return (1076 return (
1074 <>1077 <>
1075 <span className="chat-markdown-link-leading">1078 <span className="whitespace-nowrap">
1076 <MarkdownLinkFavicon host={host} />1079 <MarkdownLinkFavicon host={host} />
1077 {firstChild}1080 {firstChild}
1078 </span>1081 </span>
...@@ -1289,7 +1292,7 @@ const MarkdownFileLink = memo(function MarkdownFileLink({...@@ -1289,7 +1292,7 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
1289 side="top"1292 side="top"
1290 className="max-w-[min(40rem,calc(100vw-2rem))] font-mono text-[11px] leading-tight"1293 className="max-w-[min(40rem,calc(100vw-2rem))] font-mono text-[11px] leading-tight"
1291 >1294 >
1292 <div className="markdown-file-link-tooltip-scroll overflow-x-auto whitespace-nowrap">1295 <div className="overflow-x-auto whitespace-nowrap [scrollbar-color:color-mix(in_srgb,var(--border)_78%,transparent)_transparent] [scrollbar-width:thin] [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-[color-mix(in_srgb,var(--border)_78%,transparent)] [&::-webkit-scrollbar-track]:bg-transparent">
1293 {displayPath}1296 {displayPath}
1294 </div>1297 </div>
1295 </TooltipPopup>1298 </TooltipPopup>
...@@ -1699,7 +1702,7 @@ function ChatMarkdown({...@@ -1699,7 +1702,7 @@ function ChatMarkdown({
1699 return (1702 return (
1700 <div1703 <div
1701 className={cn(1704 className={cn(
1702 "chat-markdown w-full min-w-0 text-sm leading-relaxed text-foreground/80",1705 "chat-markdown w-full min-w-0 text-sm leading-relaxed text-foreground/80 [overflow-wrap:anywhere] [word-break:break-word]",
1703 className,1706 className,
1704 )}1707 )}
1705 onCopy={handleCopy}1708 onCopy={handleCopy}
apps/web/src/components/ChatView.tsx+10-8
...@@ -6023,8 +6023,9 @@ function ChatViewContent(props: ChatViewProps) {...@@ -6023,8 +6023,9 @@ function ChatViewContent(props: ChatViewProps) {
6023 // One inset in both states: the controls move between containers when6023 // One inset in both states: the controls move between containers when
6024 // the right panel opens, and a different right offset made them jump6024 // the right panel opens, and a different right offset made them jump
6025 // sideways on every toggle.6025 // sideways on every toggle.
6026 "workspace-titlebar-controls z-50 mr-px gap-1 [-webkit-app-region:no-drag]",6026 "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]",
6027 )}6027 )}
6028 data-workspace-titlebar-controls
6028 >6029 >
6029 {rightPanelOpen && !shouldUseRightPanelSheet ? (6030 {rightPanelOpen && !shouldUseRightPanelSheet ? (
6030 <RightPanelMaximizeControl6031 <RightPanelMaximizeControl
...@@ -6165,12 +6166,12 @@ function ChatViewContent(props: ChatViewProps) {...@@ -6165,12 +6166,12 @@ function ChatViewContent(props: ChatViewProps) {
6165 "bg-background transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none",6166 "bg-background transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none",
6166 isElectron6167 isElectron
6167 ? cn(6168 ? cn(
6168 "workspace-topbar drag-region relative px-3 sm:px-5",6169 "drag-region relative flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center px-3 sm:px-5",
6169 reserveTitleBarControlInset &&6170 reserveTitleBarControlInset &&
6170 !inlineRightPanelOwnsTitleBar &&6171 !inlineRightPanelOwnsTitleBar &&
6171 "wco:pr-[var(--workspace-native-controls-inset)]",6172 "wco:pr-[var(--workspace-native-controls-inset)]",
6172 )6173 )
6173 : "workspace-topbar pl-[calc(env(safe-area-inset-left)+0.75rem)] pr-[calc(env(safe-area-inset-right)+0.75rem)] sm:pl-[calc(env(safe-area-inset-left)+1.25rem)] sm:pr-[calc(env(safe-area-inset-right)+1.25rem)]",6174 : "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center pl-[calc(env(safe-area-inset-left)+0.75rem)] pr-[calc(env(safe-area-inset-right)+0.75rem)] sm:pl-[calc(env(safe-area-inset-left)+1.25rem)] sm:pr-[calc(env(safe-area-inset-right)+1.25rem)]",
6174 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,6175 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
6175 )}6176 )}
6176 >6177 >
...@@ -6273,16 +6274,17 @@ function ChatViewContent(props: ChatViewProps) {...@@ -6273,16 +6274,17 @@ function ChatViewContent(props: ChatViewProps) {
6273 className="pointer-events-none absolute left-1/2 z-30 flex -translate-x-1/2 justify-center py-1.5"6274 className="pointer-events-none absolute left-1/2 z-30 flex -translate-x-1/2 justify-center py-1.5"
6274 style={{ bottom: composerOverlayHeight + 4 }}6275 style={{ bottom: composerOverlayHeight + 4 }}
6275 >6276 >
6276 <button6277 <Button
6277 type="button"
6278 aria-label="Scroll to end"6278 aria-label="Scroll to end"
6279 title="Scroll to end"6279 title="Scroll to end"
6280 onClick={() => scrollToEnd(true)}6280 onClick={() => scrollToEnd(true)}
6281 className="chat-composer-glass pointer-events-auto flex items-center gap-1.5 rounded-full border border-border/60 px-3 py-1 text-muted-foreground text-xs shadow-sm transition-colors hover:border-border hover:text-foreground hover:cursor-pointer"6281 className="pointer-events-auto gap-1.5 rounded-full px-3 text-muted-foreground hover:text-foreground"
6282 size="xs"
6283 variant="glass"
6282 >6284 >
6283 <ChevronDownIcon className="size-3.5" />6285 <ChevronDownIcon className="size-3.5" />
6284 Scroll to end6286 Scroll to end
6285 </button>6287 </Button>
6286 </div>6288 </div>
6287 )}6289 )}
6288 </div>6290 </div>
...@@ -6299,7 +6301,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -6299,7 +6301,7 @@ function ChatViewContent(props: ChatViewProps) {
6299 >6301 >
6300 <div6302 <div
6301 ref={attachDraftHeroTransitionGroupRef}6303 ref={attachDraftHeroTransitionGroupRef}
6302 className="chat-composer-horizontal-inset w-full"6304 className="w-full ps-[calc(env(safe-area-inset-left)+0.75rem)] pe-[calc(env(safe-area-inset-right)+0.75rem)] sm:ps-[calc(env(safe-area-inset-left)+1.25rem)] sm:pe-[calc(env(safe-area-inset-right)+1.25rem)]"
6303 >6305 >
6304 <div className="pointer-events-auto relative z-10">6306 <div className="pointer-events-auto relative z-10">
6305 {isDraftHeroState ? (6307 {isDraftHeroState ? (
apps/web/src/components/ComposerPromptEditor.tsx+6-6
...@@ -71,6 +71,7 @@ import {...@@ -71,6 +71,7 @@ import {
71import { cn, isMacPlatform } from "~/lib/utils";71import { cn, isMacPlatform } from "~/lib/utils";
72import { basenameOfPath } from "~/pierre-icons";72import { basenameOfPath } from "~/pierre-icons";
73import {73import {
74 COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME,
74 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,75 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
75 COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME,76 COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME,
76 COMPOSER_INLINE_SKILL_CHIP_LABEL_CLASS_NAME,77 COMPOSER_INLINE_SKILL_CHIP_LABEL_CLASS_NAME,
...@@ -188,7 +189,7 @@ class ComposerMentionNode extends DecoratorNode<React.ReactElement> {...@@ -188,7 +189,7 @@ class ComposerMentionNode extends DecoratorNode<React.ReactElement> {
188189
189 override createDOM(): HTMLElement {190 override createDOM(): HTMLElement {
190 const dom = document.createElement("span");191 const dom = document.createElement("span");
191 dom.className = "composer-inline-chip relative inline-flex align-[-0.125em] leading-none";192 dom.className = COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME;
192 return dom;193 return dom;
193 }194 }
194195
...@@ -326,7 +327,7 @@ class ComposerSkillNode extends DecoratorNode<React.ReactElement> {...@@ -326,7 +327,7 @@ class ComposerSkillNode extends DecoratorNode<React.ReactElement> {
326327
327 override createDOM(): HTMLElement {328 override createDOM(): HTMLElement {
328 const dom = document.createElement("span");329 const dom = document.createElement("span");
329 dom.className = "composer-inline-chip relative inline-flex align-[-0.125em] leading-none";330 dom.className = COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME;
330 return dom;331 return dom;
331 }332 }
332333
...@@ -397,7 +398,7 @@ class ComposerTerminalContextNode extends DecoratorNode<React.ReactElement> {...@@ -397,7 +398,7 @@ class ComposerTerminalContextNode extends DecoratorNode<React.ReactElement> {
397398
398 override createDOM(): HTMLElement {399 override createDOM(): HTMLElement {
399 const dom = document.createElement("span");400 const dom = document.createElement("span");
400 dom.className = "composer-inline-chip relative inline-flex align-[-0.125em] leading-none";401 dom.className = COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME;
401 return dom;402 return dom;
402 }403 }
403404
...@@ -1747,13 +1748,12 @@ function ComposerPromptEditorInner({...@@ -1747,13 +1748,12 @@ function ComposerPromptEditorInner({
17471748
1748 return (1749 return (
1749 <ComposerTerminalContextActionsContext value={terminalContextActions}>1750 <ComposerTerminalContextActionsContext value={terminalContextActions}>
1750 <div className="composer-editor-surface relative">1751 <div className="relative [font-family:var(--font-composer,var(--font-sans))] [font-size:var(--font-size-prompt,0.875rem)] [@media(max-width:39.999rem)_and_(pointer:coarse)]:[font-size:max(var(--font-size-prompt,1rem),16px)]">
1751 <PlainTextPlugin1752 <PlainTextPlugin
1752 contentEditable={1753 contentEditable={
1753 <ContentEditable1754 <ContentEditable
1754 className={cn(1755 className={cn(
1755 // The size comes from .composer-editor-surface so Settings -> Appearance1756 // The wrapper owns the appearance preference; keep everything else here.
1756 // can drive it; keep everything else here.
1757 "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",1757 "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",
1758 className,1758 className,
1759 )}1759 )}
apps/web/src/components/DiffPanel.tsx+5-4
...@@ -825,7 +825,7 @@ export default function DiffPanel({...@@ -825,7 +825,7 @@ export default function DiffPanel({
825 </div>825 </div>
826 ) : (826 ) : (
827 <>827 <>
828 <div className="diff-panel-viewport flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">828 <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden bg-background">
829 {isSelectedPatchTruncated && (829 {isSelectedPatchTruncated && (
830 <p className="shrink-0 border-b border-border/70 bg-muted/40 px-3 py-1.5 text-[11px] text-muted-foreground">830 <p className="shrink-0 border-b border-border/70 bg-muted/40 px-3 py-1.5 text-[11px] text-muted-foreground">
831 This diff was truncated because it exceeded the preview limit. The changes shown are831 This diff was truncated because it exceeded the preview limit. The changes shown are
...@@ -907,10 +907,11 @@ export default function DiffPanel({...@@ -907,10 +907,11 @@ export default function DiffPanel({
907 <Tooltip>907 <Tooltip>
908 <TooltipTrigger908 <TooltipTrigger
909 render={909 render={
910 <button910 <Button
911 type="button"911 size="icon-micro"
912 variant="ghost"
912 className={cn(913 className={cn(
913 "-ms-0.5 inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 bg-transparent p-0 transition-colors hover:bg-foreground/10 focus-visible:outline-hidden",914 "-ms-0.5 [--control-icon-color:currentColor] bg-transparent hover:bg-foreground/10",
914 getDiffCollapseIconClassName(fileDiff),915 getDiffCollapseIconClassName(fileDiff),
915 )}916 )}
916 aria-label={collapsed ? `Expand ${filePath}` : `Collapse ${filePath}`}917 aria-label={collapsed ? `Expand ${filePath}` : `Collapse ${filePath}`}
apps/web/src/components/DiffPanelShell.tsx+1-1
...@@ -14,7 +14,7 @@ function getDiffPanelHeaderRowClassName(mode: DiffPanelMode) {...@@ -14,7 +14,7 @@ function getDiffPanelHeaderRowClassName(mode: DiffPanelMode) {
14 mode === "embedded" ? "px-2" : "px-4",14 mode === "embedded" ? "px-2" : "px-4",
15 shouldUseDragRegion15 shouldUseDragRegion
16 ? "drag-region h-[52px] border-b border-border wco:h-[env(titlebar-area-height)] wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]"16 ? "drag-region h-[52px] border-b border-border wco:h-[env(titlebar-area-height)] wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]"
17 : "surface-subheader",17 : "flex h-10 min-h-10 shrink-0 items-center border-b border-border/60 bg-background in-data-[preview-panel-mode=inline]:mb-3 in-data-[preview-panel-mode=inline]:h-7 in-data-[preview-panel-mode=inline]:min-h-7 in-data-[preview-panel-mode=inline]:border-b-transparent",
18 );18 );
19}19}
2020
apps/web/src/components/LegacySidebar.tsx+4-3
...@@ -2925,11 +2925,12 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(...@@ -2925,11 +2925,12 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
2925 <Tooltip>2925 <Tooltip>
2926 <TooltipTrigger2926 <TooltipTrigger
2927 render={2927 render={
2928 <button2928 <Button
2929 type="button"2929 size="icon-xs"
2930 variant="ghost-muted"
2930 aria-label="Add project"2931 aria-label="Add project"
2931 data-testid="sidebar-add-project-trigger"2932 data-testid="sidebar-add-project-trigger"
2932 className="inline-flex h-6 min-w-6 cursor-pointer items-center justify-center rounded-md px-[calc(--spacing(1)-1px)] text-icon-muted transition-colors hover:bg-accent hover:text-foreground"2933 className="size-6 [--control-icon-color:currentColor] text-icon-muted"
2933 onClick={openAddProject}2934 onClick={openAddProject}
2934 />2935 />
2935 }2936 }
apps/web/src/components/NoActiveThreadState.tsx+3-1
...@@ -11,7 +11,9 @@ export function NoActiveThreadState() {...@@ -11,7 +11,9 @@ export function NoActiveThreadState() {
11 <header11 <header
12 className={cn(12 className={cn(
13 "border-b border-border px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",13 "border-b border-border px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",
14 isElectron ? "workspace-topbar drag-region" : "workspace-topbar",14 isElectron
15 ? "drag-region flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center"
16 : "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center",
15 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,17 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
16 )}18 )}
17 >19 >
apps/web/src/components/RightPanelTabs.tsx+10-3
...@@ -26,6 +26,7 @@ import type { DesktopPreviewOverlay } from "~/previewStateStore";...@@ -26,6 +26,7 @@ import type { DesktopPreviewOverlay } from "~/previewStateStore";
26import type { RightPanelSurface } from "~/rightPanelStore";26import type { RightPanelSurface } from "~/rightPanelStore";
27import { cn } from "~/lib/utils";27import { cn } from "~/lib/utils";
28import { readLocalApi } from "~/localApi";28import { readLocalApi } from "~/localApi";
29import { Button } from "~/components/ui/button";
29import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";30import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
30import { Kbd } from "~/components/ui/kbd";31import { Kbd } from "~/components/ui/kbd";
31import { Menu, MenuItem, MenuPopup, MenuTrigger } from "~/components/ui/menu";32import { Menu, MenuItem, MenuPopup, MenuTrigger } from "~/components/ui/menu";
...@@ -603,7 +604,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) {...@@ -603,7 +604,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
603 >604 >
604 <div605 <div
605 className={cn(606 className={cn(
606 "workspace-topbar gap-1 pl-2",607 "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center gap-1 pl-2",
607 // The sheet overlays from the viewport top, so its tab bar keeps608 // The sheet overlays from the viewport top, so its tab bar keeps
608 // the titlebar's height: a compact row re-centers the layout609 // the titlebar's height: a compact row re-centers the layout
609 // controls a few pixels higher and the cluster jumps on open.610 // controls a few pixels higher and the cluster jumps on open.
...@@ -682,8 +683,14 @@ export function RightPanelTabs(props: RightPanelTabsProps) {...@@ -682,8 +683,14 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
682 {props.surfaces.length > 0 ? (683 {props.surfaces.length > 0 ? (
683 <Menu>684 <Menu>
684 <MenuTrigger685 <MenuTrigger
685 className="cursor-pointer relative inline-flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"686 render={
686 aria-label="Add panel surface"687 <Button
688 aria-label="Add panel surface"
689 className="size-6 shrink-0 text-muted-foreground hover:text-foreground"
690 size="icon-xs"
691 variant="ghost"
692 />
693 }
687 >694 >
688 <Plus className="size-3.5" />695 <Plus className="size-3.5" />
689 </MenuTrigger>696 </MenuTrigger>
apps/web/src/components/Sidebar.tsx+7-6
...@@ -3298,9 +3298,9 @@ export default function Sidebar() {...@@ -3298,9 +3298,9 @@ export default function Sidebar() {
3298 {isSearchingThreads ? (3298 {isSearchingThreads ? (
3299 <Button3299 <Button
3300 type="button"3300 type="button"
3301 size="icon-xs"3301 size="icon-micro"
3302 variant="ghost"3302 variant="ghost"
3303 className="size-5 shrink-0 rounded-sm text-sidebar-muted-foreground hover:bg-sidebar-control-surface hover:text-sidebar-foreground"3303 className="shrink-0 text-sidebar-muted-foreground hover:bg-sidebar-control-surface hover:text-sidebar-foreground"
3304 aria-label="Clear thread search"3304 aria-label="Clear thread search"
3305 onClick={() => {3305 onClick={() => {
3306 clearThreadSearch();3306 clearThreadSearch();
...@@ -3412,18 +3412,19 @@ export default function Sidebar() {...@@ -3412,18 +3412,19 @@ export default function Sidebar() {
3412 className="size-4 shrink-0"3412 className="size-4 shrink-0"
3413 />3413 />
3414 <span className="min-w-0 truncate text-sm">{project.displayName}</span>3414 <span className="min-w-0 truncate text-sm">{project.displayName}</span>
3415 <button3415 <Button
3416 type="button"3416 size="icon-xs"
3417 variant="ghost-muted"
3417 aria-label={`Project settings for ${project.displayName}`}3418 aria-label={`Project settings for ${project.displayName}`}
3418 title={`Project settings for ${project.displayName}`}3419 title={`Project settings for ${project.displayName}`}
3419 className="ml-auto inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-icon-muted outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-ring"3420 className="ml-auto size-6 [--control-icon-color:currentColor] text-icon-muted focus-visible:bg-accent focus-visible:text-foreground"
3420 onPointerDown={(event) => event.stopPropagation()}3421 onPointerDown={(event) => event.stopPropagation()}
3421 onClick={(event) => {3422 onClick={(event) => {
3422 void handleProjectSettings(event, project);3423 void handleProjectSettings(event, project);
3423 }}3424 }}
3424 >3425 >
3425 <SettingsIcon className="size-3.5" />3426 <SettingsIcon className="size-3.5" />
3426 </button>3427 </Button>
3427 </MenuRadioItem>3428 </MenuRadioItem>
3428 );3429 );
3429 })}3430 })}
apps/web/src/components/ThreadTerminalDrawer.tsx+3-6
...@@ -31,6 +31,7 @@ import {...@@ -31,6 +31,7 @@ import {
31 useState,31 useState,
32} from "react";32} from "react";
33import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover";33import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover";
34import { Button } from "~/components/ui/button";
34import { writeTextToClipboard } from "~/hooks/useCopyToClipboard";35import { writeTextToClipboard } from "~/hooks/useCopyToClipboard";
35import { cn } from "~/lib/utils";36import { cn } from "~/lib/utils";
36import { type TerminalContextSelection } from "~/lib/terminalContext";37import { type TerminalContextSelection } from "~/lib/terminalContext";
...@@ -1273,13 +1274,9 @@ export default function ThreadTerminalDrawer({...@@ -1273,13 +1274,9 @@ export default function ThreadTerminalDrawer({
1273 ) : null}1274 ) : null}
1274 <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 px-4 py-6 text-center text-sm text-muted-foreground">1275 <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 px-4 py-6 text-center text-sm text-muted-foreground">
1275 <p>No terminal sessions for this thread yet.</p>1276 <p>No terminal sessions for this thread yet.</p>
1276 <button1277 <Button size="xs" variant="outline" onClick={onNewTerminalAction}>
1277 type="button"
1278 className="rounded-md border border-border/80 bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-accent"
1279 onClick={onNewTerminalAction}
1280 >
1281 {newTerminalActionLabel}1278 {newTerminalActionLabel}
1282 </button>1279 </Button>
1283 </div>1280 </div>
1284 </aside>1281 </aside>
1285 );1282 );
apps/web/src/components/chat/ComposerCommandMenu.tsx+1-1
...@@ -141,7 +141,7 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {...@@ -141,7 +141,7 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {
141 >141 >
142 <div142 <div
143 ref={listRef}143 ref={listRef}
144 className="dropdown-glass relative w-full overflow-hidden rounded-[20px] **:data-[slot=scroll-area-scrollbar]:data-[orientation=vertical]:my-4"144 className="dropdown-glass relative w-full overflow-hidden rounded-[20px] shadow-[0_16px_40px_-18px_rgb(0_0_0/55%)] **:data-[slot=scroll-area-scrollbar]:data-[orientation=vertical]:my-4 dark:shadow-[0_18px_44px_-18px_rgb(0_0_0/80%)]"
145 >145 >
146 {props.items.length > 0 ? (146 {props.items.length > 0 ? (
147 <CommandList className="max-h-72">147 <CommandList className="max-h-72">
apps/web/src/components/chat/ComposerPreviewAnnotationCards.test.tsx+14
...@@ -43,4 +43,18 @@ describe("ComposerPreviewAnnotationCards", () => {...@@ -43,4 +43,18 @@ describe("ComposerPreviewAnnotationCards", () => {
43 expect(markup).not.toContain("localhost:3000");43 expect(markup).not.toContain("localhost:3000");
44 expect(markup).not.toContain("Preview annotation");44 expect(markup).not.toContain("Preview annotation");
45 });45 });
46
47 it("uses the shared button contract for removal", () => {
48 const markup = renderToStaticMarkup(
49 <ComposerPreviewAnnotationCards
50 annotations={[annotation]}
51 images={[]}
52 onRemove={vi.fn()}
53 onExpandImage={vi.fn()}
54 />,
55 );
56
57 expect(markup).toContain('aria-label="Remove preview annotation"');
58 expect(markup).toContain('data-slot="button"');
59 });
46});60});
apps/web/src/components/chat/ComposerPreviewAnnotationCards.tsx+6-4
...@@ -5,6 +5,7 @@ import type { ReactNode } from "react";...@@ -5,6 +5,7 @@ import type { ReactNode } from "react";
5import type { ComposerImageAttachment } from "~/composerDraftStore";5import type { ComposerImageAttachment } from "~/composerDraftStore";
6import { formatElementContextLabel, normalizeElementContextSelection } from "~/lib/elementContext";6import { formatElementContextLabel, normalizeElementContextSelection } from "~/lib/elementContext";
7import { cn } from "~/lib/utils";7import { cn } from "~/lib/utils";
8import { Button } from "../ui/button";
89
9interface ComposerPreviewAnnotationCardsProps {10interface ComposerPreviewAnnotationCardsProps {
10 annotations: ReadonlyArray<PreviewAnnotationPayload>;11 annotations: ReadonlyArray<PreviewAnnotationPayload>;
...@@ -128,14 +129,15 @@ export function ComposerPreviewAnnotationCards({...@@ -128,14 +129,15 @@ export function ComposerPreviewAnnotationCards({
128 </div>129 </div>
129 </div>130 </div>
130 </div>131 </div>
131 <button132 <Button
132 type="button"133 size="icon-micro"
134 variant="ghost-muted"
133 aria-label="Remove preview annotation"135 aria-label="Remove preview annotation"
134 className="absolute right-1.5 top-1.5 grid size-5 place-items-center rounded text-icon-muted transition hover:bg-muted hover:text-foreground"136 className="absolute right-1.5 top-1.5 [--control-icon-color:currentColor] rounded text-icon-muted hover:bg-muted"
135 onClick={() => onRemove(annotation.id)}137 onClick={() => onRemove(annotation.id)}
136 >138 >
137 <X className="size-3" />139 <X className="size-3" />
138 </button>140 </Button>
139 </section>141 </section>
140 );142 );
141 })}143 })}
apps/web/src/components/chat/ComposerStashBadge.tsx+1-1
...@@ -46,7 +46,7 @@ export const ComposerStashBadge = memo(function ComposerStashBadge(props: {...@@ -46,7 +46,7 @@ export const ComposerStashBadge = memo(function ComposerStashBadge(props: {
46 className={cn(46 className={cn(
47 "rounded-full px-1.5 text-[10px] font-medium tabular-nums",47 "rounded-full px-1.5 text-[10px] font-medium tabular-nums",
48 props.pulsing48 props.pulsing
49 ? "prompt-stash-count-enter bg-primary text-primary-foreground"49 ? "animate-[prompt-stash-count-enter_180ms_ease-out_both] bg-primary text-primary-foreground motion-reduce:animate-none"
50 : "bg-muted text-muted-foreground",50 : "bg-muted text-muted-foreground",
51 )}51 )}
52 >52 >
apps/web/src/components/chat/ComposerStashMenu.tsx+1-1
...@@ -91,7 +91,7 @@ export const ComposerStashMenu = memo(function ComposerStashMenu(props: {...@@ -91,7 +91,7 @@ export const ComposerStashMenu = memo(function ComposerStashMenu(props: {
9191
92 return (92 return (
93 <Command autoHighlight={false} mode="none">93 <Command autoHighlight={false} mode="none">
94 <div className="dropdown-glass relative w-full overflow-hidden rounded-[20px]">94 <div className="dropdown-glass relative w-full overflow-hidden rounded-[20px] shadow-[0_16px_40px_-18px_rgb(0_0_0/55%)] dark:shadow-[0_18px_44px_-18px_rgb(0_0_0/80%)]">
95 <CommandList className="max-h-72">95 <CommandList className="max-h-72">
96 <CommandGroup>96 <CommandGroup>
97 <CommandGroupLabel className="flex items-center gap-1.5 px-3 pt-2 pb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-secondary-label">97 <CommandGroupLabel className="flex items-center gap-1.5 px-3 pt-2 pb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-secondary-label">
apps/web/src/components/chat/ContextWindowMeter.tsx+6-9
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { cn } from "~/lib/utils";1import { Button } from "../ui/button";
2import { type ContextWindowSnapshot, formatContextWindowTokens } from "~/lib/contextWindow";2import { type ContextWindowSnapshot, formatContextWindowTokens } from "~/lib/contextWindow";
3import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";3import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
44
...@@ -36,13 +36,10 @@ export function ContextWindowMeter(props: {...@@ -36,13 +36,10 @@ export function ContextWindowMeter(props: {
36 delay={150}36 delay={150}
37 closeDelay={0}37 closeDelay={0}
38 render={38 render={
39 <button39 <Button
40 type="button"40 size="icon-sm"
41 className={cn(41 variant="ghost-muted"
42 "inline-flex size-7 cursor-pointer items-center justify-center rounded-full border border-transparent text-muted-foreground outline-none transition-colors",42 className="size-7 rounded-full hover:text-muted-foreground data-pressed:text-muted-foreground"
43 "hover:bg-accent data-[pressed]:bg-accent",
44 "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
45 )}
46 aria-label={43 aria-label={
47 usage.maxTokens !== null && usedPercentage44 usage.maxTokens !== null && usedPercentage
48 ? `Context window ${usedPercentage} used`45 ? `Context window ${usedPercentage} used`
...@@ -77,7 +74,7 @@ export function ContextWindowMeter(props: {...@@ -77,7 +74,7 @@ export function ContextWindowMeter(props: {
77 />74 />
78 </svg>75 </svg>
79 </span>76 </span>
80 </button>77 </Button>
81 }78 }
82 />79 />
83 <PopoverPopup80 <PopoverPopup
apps/web/src/components/chat/MessagesTimeline.test.tsx+2-2
...@@ -237,9 +237,9 @@ describe("MessagesTimeline", () => {...@@ -237,9 +237,9 @@ describe("MessagesTimeline", () => {
237 );237 );
238238
239 expect(compactMarkup).toContain('class="h-3 sm:h-4"');239 expect(compactMarkup).toContain('class="h-3 sm:h-4"');
240 expect(compactMarkup).not.toContain("chat-timeline-scroll-fade");240 expect(compactMarkup).not.toContain("topbar-scroll-fade");
241 expect(fadedMarkup).toContain('class="h-10 sm:h-12"');241 expect(fadedMarkup).toContain('class="h-10 sm:h-12"');
242 expect(fadedMarkup).toContain("chat-timeline-scroll-fade");242 expect(fadedMarkup).toContain("topbar-scroll-fade");
243 });243 });
244244
245 it("keeps assistant changed-files headers sticky below the thread header", () => {245 it("keeps assistant changed-files headers sticky below the thread header", () => {
apps/web/src/components/chat/MessagesTimeline.tsx+1-1
...@@ -591,7 +591,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({...@@ -591,7 +591,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
591 onScroll={handleScroll}591 onScroll={handleScroll}
592 className={cn(592 className={cn(
593 "scrollbar-gutter-both h-full min-h-0 overflow-x-hidden overscroll-y-contain px-3 [overflow-anchor:none] sm:px-5",593 "scrollbar-gutter-both h-full min-h-0 overflow-x-hidden overscroll-y-contain px-3 [overflow-anchor:none] sm:px-5",
594 topFadeEnabled && "chat-timeline-scroll-fade",594 topFadeEnabled && "topbar-scroll-fade",
595 )}595 )}
596 ListHeaderComponent={596 ListHeaderComponent={
597 loadEarlier !== null ? (597 loadEarlier !== null ? (
apps/web/src/components/chat/ModelPickerContent.tsx+7-4
...@@ -33,6 +33,7 @@ import {...@@ -33,6 +33,7 @@ import {
33} from "../../keybindings";33} from "../../keybindings";
34import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings";34import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings";
35import { cn } from "~/lib/utils";35import { cn } from "~/lib/utils";
36import { getVirtualizedScrollFadeClassName } from "../ui/scroll-area";
36import { TooltipProvider } from "../ui/tooltip";37import { TooltipProvider } from "../ui/tooltip";
37import {38import {
38 isProviderInstancePickerReady,39 isProviderInstancePickerReady,
...@@ -598,7 +599,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -598,7 +599,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
598 return (599 return (
599 <TooltipProvider delay={0}>600 <TooltipProvider delay={0}>
600 <div601 <div
601 className="dropdown-glass model-picker-surface relative flex h-screen max-h-86.5 w-screen max-w-90 flex-row overflow-hidden rounded-lg text-popover-foreground [clip-path:inset(0_round_var(--radius-lg))]"602 className="dropdown-glass relative flex h-screen max-h-86.5 w-screen max-w-90 flex-row overflow-hidden rounded-lg text-popover-foreground shadow-[0_16px_40px_-18px_rgb(0_0_0/55%)] [clip-path:inset(0_round_var(--radius-lg))] dark:shadow-[0_18px_44px_-18px_rgb(0_0_0/80%)]"
602 data-model-picker-content="true"603 data-model-picker-content="true"
603 >604 >
604 {/* Sidebar */}605 {/* Sidebar */}
...@@ -781,9 +782,11 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -781,9 +782,11 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
781 onLayout={updateModelListScrollFades}782 onLayout={updateModelListScrollFades}
782 onScroll={updateModelListScrollFades}783 onScroll={updateModelListScrollFades}
783 className={cn(784 className={cn(
784 "model-picker-list scrollbar-gutter-stable h-full overflow-x-hidden overscroll-y-contain py-1.5 [--fade-size:1.5rem]",785 "scrollbar-gutter-stable h-full overflow-x-hidden overscroll-y-contain py-1.5 [&::-webkit-scrollbar-track]:my-2",
785 showTopScrollFade && "model-picker-list-scroll-fade-top",786 getVirtualizedScrollFadeClassName({
786 showBottomScrollFade && "model-picker-list-scroll-fade-bottom",787 top: showTopScrollFade,
788 bottom: showBottomScrollFade,
789 }),
787 )}790 )}
788 />791 />
789 </ComboboxListVirtualized>792 </ComboboxListVirtualized>
apps/web/src/components/chat/ProviderStatusBanner.tsx+6-4
...@@ -2,6 +2,7 @@ import { type ServerProvider } from "@t3tools/contracts";...@@ -2,6 +2,7 @@ import { type ServerProvider } from "@t3tools/contracts";
2import { memo } from "react";2import { memo } from "react";
3import { InfoIcon, XIcon } from "lucide-react";3import { InfoIcon, XIcon } from "lucide-react";
4import { cn } from "~/lib/utils";4import { cn } from "~/lib/utils";
5import { Button } from "../ui/button";
5import { formatProviderDriverKindLabel } from "../../providerModels";6import { formatProviderDriverKindLabel } from "../../providerModels";
6import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";7import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
78
...@@ -66,14 +67,15 @@ export const ProviderStatusBanner = memo(function ProviderStatusBanner({...@@ -66,14 +67,15 @@ export const ProviderStatusBanner = memo(function ProviderStatusBanner({
66 </TooltipPopup>67 </TooltipPopup>
67 </Tooltip>68 </Tooltip>
68 </div>69 </div>
69 <button70 <Button
70 type="button"
71 aria-label={`Dismiss ${providerName} provider ${status.status}`}71 aria-label={`Dismiss ${providerName} provider ${status.status}`}
72 className="absolute top-2 right-2 inline-flex size-6 cursor-pointer items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-foreground/8 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"72 className="absolute top-2 right-2 size-6 text-muted-foreground hover:text-foreground"
73 onClick={onDismiss}73 onClick={onDismiss}
74 size="icon-xs"
75 variant="ghost"
74 >76 >
75 <XIcon aria-hidden className="size-3.5" />77 <XIcon aria-hidden className="size-3.5" />
76 </button>78 </Button>
77 </div>79 </div>
78 </div>80 </div>
79 );81 );
apps/web/src/components/composerInlineChip.ts+3
...@@ -8,6 +8,9 @@ export const CHAT_INLINE_CHIP_CLASS_NAME = `${INLINE_CHIP_CLASS_NAME} text-[12px...@@ -8,6 +8,9 @@ export const CHAT_INLINE_CHIP_CLASS_NAME = `${INLINE_CHIP_CLASS_NAME} text-[12px
88
9export const COMPOSER_INLINE_CHIP_CLASS_NAME = `${INLINE_CHIP_CLASS_NAME} text-[0.86em] select-none`;9export const COMPOSER_INLINE_CHIP_CLASS_NAME = `${INLINE_CHIP_CLASS_NAME} text-[0.86em] select-none`;
1010
11export const COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME =
12 "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-['']";
13
11export const COMPOSER_INLINE_CHIP_ICON_CLASS_NAME = "size-[1.17em] shrink-0 opacity-85";14export const COMPOSER_INLINE_CHIP_ICON_CLASS_NAME = "size-[1.17em] shrink-0 opacity-85";
1215
13export const CHAT_INLINE_CHIP_LABEL_CLASS_NAME = "truncate leading-tight";16export const CHAT_INLINE_CHIP_LABEL_CLASS_NAME = "truncate leading-tight";
apps/web/src/components/diffs/StyledDiffCodeView.test.tsx+3-1
...@@ -35,7 +35,9 @@ describe("StyledDiffCodeView", () => {...@@ -35,7 +35,9 @@ describe("StyledDiffCodeView", () => {
35 />,35 />,
36 );36 );
3737
38 expect(testState.codeViewClassName).toBe("diff-render-surface outline-none min-h-0");38 expect(testState.codeViewClassName).toBe(
39 "diff-render-surface [--code-background:var(--background)] outline-none min-h-0",
40 );
39 expect(testState.codeViewOptions).toMatchObject({41 expect(testState.codeViewOptions).toMatchObject({
40 theme: "pierre-dark",42 theme: "pierre-dark",
41 stickyHeaders: true,43 stickyHeaders: true,
apps/web/src/components/diffs/StyledDiffCodeView.tsx+2-2
...@@ -292,8 +292,8 @@ export function StyledDiffCodeView<LAnnotation = undefined>({...@@ -292,8 +292,8 @@ export function StyledDiffCodeView<LAnnotation = undefined>({
292 // outside the panel clipping boundary; actual controls inside retain their own indicators.292 // outside the panel clipping boundary; actual controls inside retain their own indicators.
293 className={293 className={
294 className294 className
295 ? `diff-render-surface outline-none ${className}`295 ? `diff-render-surface [--code-background:var(--background)] outline-none ${className}`
296 : "diff-render-surface outline-none"296 : "diff-render-surface [--code-background:var(--background)] outline-none"
297 }297 }
298 options={{298 options={{
299 ...options,299 ...options,
apps/web/src/components/files/FileBrowserPanel.tsx+5-2
...@@ -78,7 +78,7 @@ function FileSearchField(props: {...@@ -78,7 +78,7 @@ function FileSearchField(props: {
78 value: string;78 value: string;
79}) {79}) {
80 return (80 return (
81 <InputGroup variant="ghost" className="h-7 min-w-0 flex-1 rounded-md">81 <InputGroup variant="ghost" className="h-7 min-w-0 flex-1">
82 <InputGroupInput82 <InputGroupInput
83 type="search"83 type="search"
84 name={props.name}84 name={props.name}
...@@ -350,7 +350,10 @@ export default function FileBrowserPanel({...@@ -350,7 +350,10 @@ export default function FileBrowserPanel({
350 className="flex min-h-0 flex-1 flex-col bg-background"350 className="flex min-h-0 flex-1 flex-col bg-background"
351 data-file-browser-panel={`${environmentId}:${cwd}`}351 data-file-browser-panel={`${environmentId}:${cwd}`}
352 >352 >
353 <div className="surface-subheader gap-1 px-2" data-surface-subheader>353 <div
354 className="flex h-10 min-h-10 shrink-0 items-center gap-1 border-b border-border/60 bg-background px-2 in-data-[preview-panel-mode=inline]:mb-3 in-data-[preview-panel-mode=inline]:h-7 in-data-[preview-panel-mode=inline]:min-h-7 in-data-[preview-panel-mode=inline]:border-b-transparent"
355 data-surface-subheader
356 >
354 <RefreshFilesButton isPending={entriesQuery.isPending} onRefresh={entriesQuery.refresh} />357 <RefreshFilesButton isPending={entriesQuery.isPending} onRefresh={entriesQuery.refresh} />
355 <FileSearchField358 <FileSearchField
356 name="project-files-search"359 name="project-files-search"
apps/web/src/components/files/FilePreviewPanel.tsx+4-1
...@@ -857,7 +857,10 @@ export default function FilePreviewPanel({...@@ -857,7 +857,10 @@ export default function FilePreviewPanel({
857 return (857 return (
858 <div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background">858 <div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background">
859 {relativePath ? (859 {relativePath ? (
860 <div className="surface-subheader gap-2 px-3" data-surface-subheader>860 <div
861 className="flex h-10 min-h-10 shrink-0 items-center gap-2 border-b border-border/60 bg-background px-3 in-data-[preview-panel-mode=inline]:mb-3 in-data-[preview-panel-mode=inline]:h-7 in-data-[preview-panel-mode=inline]:min-h-7 in-data-[preview-panel-mode=inline]:border-b-transparent"
862 data-surface-subheader
863 >
861 <ScrollArea864 <ScrollArea
862 ref={breadcrumbRef}865 ref={breadcrumbRef}
863 hideScrollbars866 hideScrollbars
apps/web/src/components/preview/PreviewChromeRow.tsx+6-2
...@@ -109,7 +109,11 @@ export function PreviewChromeRow({...@@ -109,7 +109,11 @@ export function PreviewChromeRow({
109109
110 return (110 return (
111 <div className="relative">111 <div className="relative">
112 <form onSubmit={submit} className="surface-subheader gap-1 px-2" data-surface-subheader>112 <form
113 onSubmit={submit}
114 className="flex h-10 min-h-10 shrink-0 items-center gap-1 border-b border-border/60 bg-background px-2 in-data-[preview-panel-mode=inline]:mb-3 in-data-[preview-panel-mode=inline]:h-7 in-data-[preview-panel-mode=inline]:min-h-7 in-data-[preview-panel-mode=inline]:border-b-transparent"
115 data-surface-subheader
116 >
113 <div className="flex items-center gap-0.5" role="group" aria-label="Navigation">117 <div className="flex items-center gap-0.5" role="group" aria-label="Navigation">
114 <Tooltip>118 <Tooltip>
115 <TooltipTrigger119 <TooltipTrigger
...@@ -164,7 +168,7 @@ export function PreviewChromeRow({...@@ -164,7 +168,7 @@ export function PreviewChromeRow({
164 </Tooltip>168 </Tooltip>
165 </div>169 </div>
166170
167 <InputGroup variant="ghost" className="group/address h-7 flex-1 rounded-md">171 <InputGroup variant="ghost" className="group/address h-7 flex-1">
168 <Tooltip>172 <Tooltip>
169 <TooltipTrigger173 <TooltipTrigger
170 render={174 render={
apps/web/src/components/pullRequest/PullRequestCodeTab.tsx+12-12
...@@ -668,13 +668,12 @@ export function PullRequestCodeTab({...@@ -668,13 +668,12 @@ export function PullRequestCodeTab({
668 // chevron follows it rather than recomputing the default here.668 // chevron follows it rather than recomputing the default here.
669 const collapsed = item.collapsed === true;669 const collapsed = item.collapsed === true;
670 return (670 return (
671 <button671 <Button
672 type="button"672 size="icon-micro"
673 variant="ghost-muted"
673 aria-expanded={!collapsed}674 aria-expanded={!collapsed}
674 aria-label={collapsed ? "Expand diff" : "Collapse diff"}675 aria-label={collapsed ? "Expand diff" : "Collapse diff"}
675 className={cn(676 className="mr-1 rounded hover:bg-transparent"
676 "mr-1 inline-flex size-5 items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground",
677 )}
678 onClick={(event) => {677 onClick={(event) => {
679 event.stopPropagation();678 event.stopPropagation();
680 toggleFile(item.id);679 toggleFile(item.id);
...@@ -685,7 +684,7 @@ export function PullRequestCodeTab({...@@ -685,7 +684,7 @@ export function PullRequestCodeTab({
685 ) : (684 ) : (
686 <ChevronDownIcon className="size-4" />685 <ChevronDownIcon className="size-4" />
687 )}686 )}
688 </button>687 </Button>
689 );688 );
690 },689 },
691 [toggleFile],690 [toggleFile],
...@@ -899,7 +898,7 @@ export function PullRequestCodeTab({...@@ -899,7 +898,7 @@ export function PullRequestCodeTab({
899 review.verdicts.length === 0 ? null : (898 review.verdicts.length === 0 ? null : (
900 <div className="pointer-events-none absolute inset-x-0 bottom-0 z-10">899 <div className="pointer-events-none absolute inset-x-0 bottom-0 z-10">
901 {reviewOpen ? (900 {reviewOpen ? (
902 <div className="chat-composer-glass pointer-events-auto absolute inset-x-3 bottom-3 rounded-xl border border-border/60 shadow-lg">901 <div className="surface-glass pointer-events-auto absolute inset-x-3 bottom-3 rounded-xl border border-border/60 shadow-lg">
903 <Button902 <Button
904 type="button"903 type="button"
905 size="icon-sm"904 size="icon-sm"
...@@ -923,10 +922,11 @@ export function PullRequestCodeTab({...@@ -923,10 +922,11 @@ export function PullRequestCodeTab({
923 ) : (922 ) : (
924 // Bottom-right, clear of the vertical scrollbar the diff view keeps to its own right923 // Bottom-right, clear of the vertical scrollbar the diff view keeps to its own right
925 // edge.924 // edge.
926 <button925 <Button
927 type="button"926 className="pointer-events-auto absolute right-4 bottom-3 rounded-full shadow-lg"
928 className="chat-composer-glass pointer-events-auto absolute bottom-3 right-4 flex items-center gap-1.5 rounded-full border border-border/60 px-3 py-1.5 text-xs font-medium shadow-lg"
929 onClick={() => setReviewOpen(true)}927 onClick={() => setReviewOpen(true)}
928 size="compact"
929 variant="glass"
930 >930 >
931 <MessageSquareIcon className="size-3.5" />931 <MessageSquareIcon className="size-3.5" />
932 Review932 Review
...@@ -935,7 +935,7 @@ export function PullRequestCodeTab({...@@ -935,7 +935,7 @@ export function PullRequestCodeTab({
935 {pendingComments.length}935 {pendingComments.length}
936 </span>936 </span>
937 ) : null}937 ) : null}
938 </button>938 </Button>
939 )}939 )}
940 </div>940 </div>
941 );941 );
...@@ -959,7 +959,7 @@ export function PullRequestCodeTab({...@@ -959,7 +959,7 @@ export function PullRequestCodeTab({
959 * diff API offers it.959 * diff API offers it.
960 */960 */
961 const toolbar = (961 const toolbar = (
962 <div className="surface-subheader justify-between gap-2 px-4 text-xs text-muted-foreground">962 <div className="flex h-10 min-h-10 shrink-0 items-center justify-between gap-2 border-b border-border/60 bg-background px-4 text-xs text-muted-foreground">
963 <div className="flex min-w-0 flex-1 items-center gap-3">963 <div className="flex min-w-0 flex-1 items-center gap-3">
964 {/* A host that reports no commits has nothing to scope by, and a dropdown whose only964 {/* A host that reports no commits has nothing to scope by, and a dropdown whose only
965 entry is the scope already showing is a control that does nothing. */}965 entry is the scope already showing is a control that does nothing. */}
apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx+8-2
...@@ -1137,8 +1137,14 @@ export function PullRequestDetailPanel({...@@ -1137,8 +1137,14 @@ export function PullRequestDetailPanel({
1137 <>1137 <>
1138 <Menu>1138 <Menu>
1139 <MenuTrigger1139 <MenuTrigger
1140 className="inline-flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"1140 render={
1141 aria-label="More pull request actions"1141 <Button
1142 aria-label="More pull request actions"
1143 className="size-6"
1144 size="icon-xs"
1145 variant="ghost-muted"
1146 />
1147 }
1142 >1148 >
1143 <MoreHorizontalIcon className="size-4" />1149 <MoreHorizontalIcon className="size-4" />
1144 </MenuTrigger>1150 </MenuTrigger>
apps/web/src/components/pullRequest/PullRequestListFilters.tsx+8-18
...@@ -24,6 +24,7 @@ import type { ElementType } from "react";...@@ -24,6 +24,7 @@ import type { ElementType } from "react";
24import { cn } from "~/lib/utils";24import { cn } from "~/lib/utils";
25import { getSourceControlPresentationForKind } from "~/sourceControlPresentation";25import { getSourceControlPresentationForKind } from "~/sourceControlPresentation";
26import { ProjectFavicon } from "../ProjectFavicon";26import { ProjectFavicon } from "../ProjectFavicon";
27import { InputGroup, InputGroupAddon, InputGroupInput } from "../ui/input-group";
2728
28import {29import {
29 Menu,30 Menu,
...@@ -79,29 +80,18 @@ export function PullRequestSearchInput({...@@ -79,29 +80,18 @@ export function PullRequestSearchInput({
79 onChange: (value: string) => void;80 onChange: (value: string) => void;
80}) {81}) {
81 return (82 return (
82 <div className="relative min-w-0 flex-1">83 <InputGroup className="min-w-0 flex-1 **:[input]:h-9 sm:**:[input]:h-8">
83 {busy ? (84 <InputGroupAddon>
84 <LoaderIcon85 {busy ? <LoaderIcon aria-hidden className="animate-spin" /> : <SearchIcon aria-hidden />}
85 aria-hidden86 </InputGroupAddon>
86 className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 animate-spin text-muted-foreground"87 <InputGroupInput
87 />88 type="search"
88 ) : (
89 <SearchIcon
90 aria-hidden
91 className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground"
92 />
93 )}
94 <input
95 type="text"
96 value={value}89 value={value}
97 onChange={(event) => onChange(event.currentTarget.value)}90 onChange={(event) => onChange(event.currentTarget.value)}
98 placeholder="Search pull requests, or label:bug"91 placeholder="Search pull requests, or label:bug"
99 aria-label="Search pull requests"92 aria-label="Search pull requests"
100 // Tracks the shared input's height at both widths, so it stays level with the icon
101 // button beside it rather than towering over it on wide screens.
102 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"
103 />93 />
104 </div>94 </InputGroup>
105 );95 );
106}96}
10797
apps/web/src/components/pullRequest/PullRequestReviewerPicker.tsx+3-2
...@@ -19,6 +19,7 @@ import { useAtomCommand } from "~/state/use-atom-command";...@@ -19,6 +19,7 @@ import { useAtomCommand } from "~/state/use-atom-command";
19import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";19import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
2020
21import { Button } from "../ui/button";21import { Button } from "../ui/button";
22import { Input } from "../ui/input";
22import { Menu, MenuPopup, MenuTrigger } from "../ui/menu";23import { Menu, MenuPopup, MenuTrigger } from "../ui/menu";
23import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";24import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
24import { toastManager } from "../ui/toast";25import { toastManager } from "../ui/toast";
...@@ -131,13 +132,13 @@ export function PullRequestReviewerPicker({...@@ -131,13 +132,13 @@ export function PullRequestReviewerPicker({
131 />132 />
132 <MenuPopup align="start" side="bottom" className="w-72 p-0">133 <MenuPopup align="start" side="bottom" className="w-72 p-0">
133 <div className="border-b border-border/60 p-2">134 <div className="border-b border-border/60 p-2">
134 <input135 <Input
135 autoFocus136 autoFocus
136 value={query}137 value={query}
137 onChange={(event) => setQuery(event.currentTarget.value)}138 onChange={(event) => setQuery(event.currentTarget.value)}
138 placeholder="Search people with access"139 placeholder="Search people with access"
139 aria-label="Search people with access"140 aria-label="Search people with access"
140 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"141 size="compact"
141 />142 />
142 </div>143 </div>
143 <div className="max-h-72 overflow-y-auto p-1">144 <div className="max-h-72 overflow-y-auto p-1">
apps/web/src/components/search/ProjectContentSearchDialog.tsx+7-8
...@@ -11,6 +11,7 @@ import { useProjectContentSearch } from "~/state/queries";...@@ -11,6 +11,7 @@ import { useProjectContentSearch } from "~/state/queries";
11import { PierreEntryIcon } from "../chat/PierreEntryIcon";11import { PierreEntryIcon } from "../chat/PierreEntryIcon";
12import { CommandPaletteContent } from "../CommandPaletteContent";12import { CommandPaletteContent } from "../CommandPaletteContent";
13import { ScrollArea } from "../ui/scroll-area";13import { ScrollArea } from "../ui/scroll-area";
14import { Toggle } from "../ui/toggle";
14import { HighlightedSearchLine } from "./HighlightedSearchLine";15import { HighlightedSearchLine } from "./HighlightedSearchLine";
1516
16interface ProjectContentSearchDialogProps {17interface ProjectContentSearchDialogProps {
...@@ -58,19 +59,17 @@ function SearchOptionButton(props: {...@@ -58,19 +59,17 @@ function SearchOptionButton(props: {
58 readonly children: ReactNode;59 readonly children: ReactNode;
59}) {60}) {
60 return (61 return (
61 <button62 <Toggle
62 type="button"
63 aria-label={props.label}63 aria-label={props.label}
64 aria-pressed={props.active}64 pressed={props.active}
65 title={props.label}65 title={props.label}
66 className={cn(66 className="size-8 rounded-[5px] font-mono text-muted-foreground data-pressed:text-foreground sm:size-7"
67 "flex size-8 shrink-0 items-center justify-center rounded-[5px] font-mono text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",67 size="compact"
68 props.active && "bg-accent text-foreground shadow-sm",68 variant="ghost"
69 )}
70 onClick={props.onClick}69 onClick={props.onClick}
71 >70 >
72 {props.children}71 {props.children}
73 </button>72 </Toggle>
74 );73 );
75}74}
7675
apps/web/src/components/settings/DiagnosticsSettings.tsx+12-14
...@@ -273,14 +273,14 @@ function TraceIdCell({ traceId }: { traceId: string }) {...@@ -273,14 +273,14 @@ function TraceIdCell({ traceId }: { traceId: string }) {
273 <Tooltip>273 <Tooltip>
274 <TooltipTrigger274 <TooltipTrigger
275 render={275 render={
276 <button276 <Button
277 type="button"277 size="icon-micro"
278 className="cursor-pointer inline-flex size-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"278 variant="ghost-muted"
279 aria-label={copied ? "Copied trace ID" : "Copy trace ID"}279 aria-label={copied ? "Copied trace ID" : "Copy trace ID"}
280 onClick={() => copyToClipboard(traceId)}280 onClick={() => copyToClipboard(traceId)}
281 >281 >
282 <CopyIcon className="size-3" />282 <CopyIcon className="size-3" />
283 </button>283 </Button>
284 }284 }
285 />285 />
286 <TooltipPopup side="top">{copied ? "Copied" : "Copy full trace ID"}</TooltipPopup>286 <TooltipPopup side="top">{copied ? "Copied" : "Copy full trace ID"}</TooltipPopup>
...@@ -322,14 +322,14 @@ function ProcessNameCell({...@@ -322,14 +322,14 @@ function ProcessNameCell({
322 style={{ paddingLeft: `${Math.min(process.depth, 6) * 10}px` }}322 style={{ paddingLeft: `${Math.min(process.depth, 6) * 10}px` }}
323 >323 >
324 {hasChildren ? (324 {hasChildren ? (
325 <button325 <Button
326 type="button"326 size="icon-micro"
327 className="cursor-pointer inline-flex size-5 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"327 variant="ghost-muted"
328 aria-label={isExpanded ? `Collapse ${name}` : `Expand ${name}`}328 aria-label={isExpanded ? `Collapse ${name}` : `Expand ${name}`}
329 onClick={() => onToggle(process.pid)}329 onClick={() => onToggle(process.pid)}
330 >330 >
331 <ChevronIcon className="size-3.5" />331 <ChevronIcon className="size-3.5" />
332 </button>332 </Button>
333 ) : (333 ) : (
334 <span className="size-5 shrink-0" aria-hidden="true" />334 <span className="size-5 shrink-0" aria-hidden="true" />
335 )}335 )}
...@@ -793,9 +793,8 @@ function DiagnosticsRefreshButton({...@@ -793,9 +793,8 @@ function DiagnosticsRefreshButton({
793 <TooltipTrigger793 <TooltipTrigger
794 render={794 render={
795 <Button795 <Button
796 size="icon-xs"796 size="icon-micro"
797 variant="ghost"797 variant="ghost-muted"
798 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
799 disabled={isPending}798 disabled={isPending}
800 onClick={onClick}799 onClick={onClick}
801 aria-label={label}800 aria-label={label}
...@@ -1130,9 +1129,8 @@ export function DiagnosticsSettingsPanel() {...@@ -1130,9 +1129,8 @@ export function DiagnosticsSettingsPanel() {
1130 <TooltipTrigger1129 <TooltipTrigger
1131 render={1130 render={
1132 <Button1131 <Button
1133 size="icon-xs"1132 size="icon-micro"
1134 variant="ghost"1133 variant="ghost-muted"
1135 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
1136 disabled={!observability?.logsDirectoryPath || isOpeningLogsDirectory}1134 disabled={!observability?.logsDirectoryPath || isOpeningLogsDirectory}
1137 onClick={openLogsDirectory}1135 onClick={openLogsDirectory}
1138 aria-label="Open logs folder"1136 aria-label="Open logs folder"
apps/web/src/components/settings/KeybindingsSettings.tsx+31-67
...@@ -125,9 +125,8 @@ function ExpandableHeaderSearch({...@@ -125,9 +125,8 @@ function ExpandableHeaderSearch({
125 render={125 render={
126 <Button126 <Button
127 type="button"127 type="button"
128 size="icon-xs"128 size="icon-micro"
129 variant="ghost"129 variant="ghost-muted"
130 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
131 onClick={() => onOpenChange(true)}130 onClick={() => onOpenChange(true)}
132 aria-label="Search keybindings"131 aria-label="Search keybindings"
133 >132 >
...@@ -144,10 +143,10 @@ function ExpandableHeaderSearch({...@@ -144,10 +143,10 @@ function ExpandableHeaderSearch({
144 return (143 return (
145 <div className="relative">144 <div className="relative">
146 <SearchIcon className="pointer-events-none absolute top-1/2 left-2 size-3 -translate-y-1/2 text-muted-foreground" />145 <SearchIcon className="pointer-events-none absolute top-1/2 left-2 size-3 -translate-y-1/2 text-muted-foreground" />
147 <input146 <Input
148 ref={inputRef}147 ref={inputRef}
149 autoFocus148 autoFocus
150 type="text"149 type="search"
151 value={query}150 value={query}
152 onChange={(event) => onChange(event.currentTarget.value)}151 onChange={(event) => onChange(event.currentTarget.value)}
153 onBlur={() => {152 onBlur={() => {
...@@ -162,7 +161,8 @@ function ExpandableHeaderSearch({...@@ -162,7 +161,8 @@ function ExpandableHeaderSearch({
162 }}161 }}
163 placeholder="Search keybindings"162 placeholder="Search keybindings"
164 aria-label="Search keybindings"163 aria-label="Search keybindings"
165 className="h-6 w-44 rounded-md border border-input bg-background pl-7 pr-2 text-[11px] text-foreground outline-none placeholder:text-muted-foreground/72 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/24"164 className="w-44 [&_[data-slot=input]]:pl-7"
165 size="compact"
166 />166 />
167 </div>167 </div>
168 );168 );
...@@ -309,10 +309,7 @@ function WhenVariableSelect({...@@ -309,10 +309,7 @@ function WhenVariableSelect({
309309
310 return (310 return (
311 <Select value={value} onValueChange={(nextValue) => nextValue && onChange(nextValue)}>311 <Select value={value} onValueChange={(nextValue) => nextValue && onChange(nextValue)}>
312 <SelectTrigger312 <SelectTrigger size="compact" className="min-w-0 flex-1 font-mono">
313 size="xs"
314 className="h-7 min-h-7 min-w-0 flex-1 rounded-md font-mono text-xs sm:h-7"
315 >
316 <SelectValue placeholder="Condition" className="leading-7" />313 <SelectValue placeholder="Condition" className="leading-7" />
317 {unknownIdentifiers && unknownIdentifiers.length > 0 ? (314 {unknownIdentifiers && unknownIdentifiers.length > 0 ? (
318 <UnknownWhenVariableWarning identifiers={unknownIdentifiers} focusable={false} />315 <UnknownWhenVariableWarning identifiers={unknownIdentifiers} focusable={false} />
...@@ -365,8 +362,8 @@ function WhenExpressionNodeEditor({...@@ -365,8 +362,8 @@ function WhenExpressionNodeEditor({
365 onPressedChange={(pressed) => onChange(setConditionNegated(node, pressed))}362 onPressedChange={(pressed) => onChange(setConditionNegated(node, pressed))}
366 aria-label={`Negate ${condition.identifier}`}363 aria-label={`Negate ${condition.identifier}`}
367 variant="outline"364 variant="outline"
368 size="xs"365 size="compact"
369 className="h-7 min-w-10 px-2 text-[11px] sm:h-7"366 className="min-w-10"
370 >367 >
371 Not368 Not
372 </Toggle>369 </Toggle>
...@@ -381,7 +378,7 @@ function WhenExpressionNodeEditor({...@@ -381,7 +378,7 @@ function WhenExpressionNodeEditor({
381 type="button"378 type="button"
382 variant="ghost"379 variant="ghost"
383 size="icon-sm"380 size="icon-sm"
384 className="size-7 sm:size-7"381 className="size-7"
385 aria-label="Remove condition"382 aria-label="Remove condition"
386 onClick={onRemove}383 onClick={onRemove}
387 >384 >
...@@ -406,8 +403,8 @@ function WhenExpressionNodeEditor({...@@ -406,8 +403,8 @@ function WhenExpressionNodeEditor({
406 onPressedChange={(pressed) => onChange(pressed ? node : node.node)}403 onPressedChange={(pressed) => onChange(pressed ? node : node.node)}
407 aria-label="Negate group"404 aria-label="Negate group"
408 variant="outline"405 variant="outline"
409 size="xs"406 size="compact"
410 className="h-7 min-w-10 px-2 text-[11px] sm:h-7"407 className="min-w-10"
411 >408 >
412 Not409 Not
413 </Toggle>410 </Toggle>
...@@ -416,7 +413,7 @@ function WhenExpressionNodeEditor({...@@ -416,7 +413,7 @@ function WhenExpressionNodeEditor({
416 type="button"413 type="button"
417 variant="ghost"414 variant="ghost"
418 size="icon-sm"415 size="icon-sm"
419 className="ml-auto size-7 sm:size-7"416 className="ml-auto size-7"
420 aria-label="Remove negated group"417 aria-label="Remove negated group"
421 onClick={onRemove}418 onClick={onRemove}
422 >419 >
...@@ -509,7 +506,7 @@ function WhenExpressionNodeEditor({...@@ -509,7 +506,7 @@ function WhenExpressionNodeEditor({
509 >506 >
510 <div className="flex flex-wrap items-center gap-2">507 <div className="flex flex-wrap items-center gap-2">
511 <Select value={operator} onValueChange={(value) => setOperator(value as BooleanOperator)}>508 <Select value={operator} onValueChange={(value) => setOperator(value as BooleanOperator)}>
512 <SelectTrigger size="xs" className="h-7 min-h-7 w-24 rounded-md text-xs sm:h-7">509 <SelectTrigger size="compact" className="w-24">
513 <SelectValue />510 <SelectValue />
514 </SelectTrigger>511 </SelectTrigger>
515 <SelectContent512 <SelectContent
...@@ -526,17 +523,11 @@ function WhenExpressionNodeEditor({...@@ -526,17 +523,11 @@ function WhenExpressionNodeEditor({
526 </SelectItem>523 </SelectItem>
527 </SelectContent>524 </SelectContent>
528 </Select>525 </Select>
529 <Button526 <Button type="button" variant="outline" size="compact" onClick={addCondition}>
530 type="button"
531 variant="outline"
532 size="xs"
533 className="h-7 sm:h-7"
534 onClick={addCondition}
535 >
536 <PlusIcon className="size-3.5" />527 <PlusIcon className="size-3.5" />
537 Condition528 Condition
538 </Button>529 </Button>
539 <Button type="button" variant="outline" size="xs" className="h-7 sm:h-7" onClick={addGroup}>530 <Button type="button" variant="outline" size="compact" onClick={addGroup}>
540 <PlusIcon className="size-3.5" />531 <PlusIcon className="size-3.5" />
541 Group532 Group
542 </Button>533 </Button>
...@@ -545,7 +536,7 @@ function WhenExpressionNodeEditor({...@@ -545,7 +536,7 @@ function WhenExpressionNodeEditor({
545 type="button"536 type="button"
546 variant="ghost"537 variant="ghost"
547 size="icon-sm"538 size="icon-sm"
548 className="ml-auto size-7 sm:size-7"539 className="ml-auto size-7"
549 aria-label="Remove group"540 aria-label="Remove group"
550 onClick={onRemove}541 onClick={onRemove}
551 >542 >
...@@ -640,23 +631,11 @@ function WhenExpressionBuilder({...@@ -640,23 +631,11 @@ function WhenExpressionBuilder({
640 <div className="text-sm font-medium text-foreground">When</div>631 <div className="text-sm font-medium text-foreground">When</div>
641 </div>632 </div>
642 <div className="flex shrink-0 items-center gap-2">633 <div className="flex shrink-0 items-center gap-2">
643 <Button634 <Button type="button" variant="outline" size="compact" onClick={addRootCondition}>
644 type="button"
645 variant="outline"
646 size="xs"
647 className="h-7 sm:h-7"
648 onClick={addRootCondition}
649 >
650 <PlusIcon className="size-3.5" />635 <PlusIcon className="size-3.5" />
651 Condition636 Condition
652 </Button>637 </Button>
653 <Button638 <Button type="button" variant="outline" size="compact" onClick={addRootGroup}>
654 type="button"
655 variant="outline"
656 size="xs"
657 className="h-7 sm:h-7"
658 onClick={addRootGroup}
659 >
660 <PlusIcon className="size-3.5" />639 <PlusIcon className="size-3.5" />
661 Group640 Group
662 </Button>641 </Button>
...@@ -702,17 +681,11 @@ function WhenExpressionBuilder({...@@ -702,17 +681,11 @@ function WhenExpressionBuilder({
702 ) : (681 ) : (
703 <div className="rounded-md border border-dashed border-border/80 bg-muted/15 p-3">682 <div className="rounded-md border border-dashed border-border/80 bg-muted/15 p-3">
704 <div className="flex flex-wrap gap-2">683 <div className="flex flex-wrap gap-2">
705 <Button type="button" size="xs" className="h-7 sm:h-7" onClick={addRootCondition}>684 <Button type="button" size="compact" onClick={addRootCondition}>
706 <PlusIcon className="size-3.5" />685 <PlusIcon className="size-3.5" />
707 Condition686 Condition
708 </Button>687 </Button>
709 <Button688 <Button type="button" variant="outline" size="compact" onClick={addRootGroup}>
710 type="button"
711 variant="outline"
712 size="xs"
713 className="h-7 sm:h-7"
714 onClick={addRootGroup}
715 >
716 <PlusIcon className="size-3.5" />689 <PlusIcon className="size-3.5" />
717 Group690 Group
718 </Button>691 </Button>
...@@ -864,8 +837,7 @@ function KeybindingTableRow({...@@ -864,8 +837,7 @@ function KeybindingTableRow({
864 )}837 )}
865 {isDirty ? (838 {isDirty ? (
866 <Button839 <Button
867 size="xs"840 size="compact"
868 className="h-7 sm:h-7"
869 disabled={isSaving || keyDraft.trim().length === 0 || !isWhenDraftValid}841 disabled={isSaving || keyDraft.trim().length === 0 || !isWhenDraftValid}
870 onClick={save}842 onClick={save}
871 >843 >
...@@ -992,10 +964,7 @@ function NewKeybindingTableRow({...@@ -992,10 +964,7 @@ function NewKeybindingTableRow({
992 value={commandDraft}964 value={commandDraft}
993 onValueChange={(value) => setCommandDraft(value as KeybindingCommand)}965 onValueChange={(value) => setCommandDraft(value as KeybindingCommand)}
994 >966 >
995 <SelectTrigger967 <SelectTrigger size="compact" className="w-full max-w-60">
996 size="xs"
997 className="h-7 min-h-7 w-full max-w-60 rounded-md text-xs sm:h-7"
998 >
999 <SelectValue placeholder="Command" />968 <SelectValue placeholder="Command" />
1000 </SelectTrigger>969 </SelectTrigger>
1001 <SelectContent970 <SelectContent
...@@ -1017,18 +986,15 @@ function NewKeybindingTableRow({...@@ -1017,18 +986,15 @@ function NewKeybindingTableRow({
1017 aria-label={`Keybinding for ${commandLabelText}`}986 aria-label={`Keybinding for ${commandLabelText}`}
1018 value={isRecording ? "" : keyDraft}987 value={isRecording ? "" : keyDraft}
1019 placeholder={isRecording ? "Press shortcut" : "Unassigned"}988 placeholder={isRecording ? "Press shortcut" : "Unassigned"}
1020 className={cn(989 size="compact"
1021 "h-7 w-44 rounded-md font-mono text-[12px] sm:h-7",990 className={cn("w-44 font-mono", isRecording && "border-primary/70 bg-primary/5")}
1022 isRecording && "border-primary/70 bg-primary/5",
1023 )}
1024 onFocus={() => setDraft({ isRecording: true })}991 onFocus={() => setDraft({ isRecording: true })}
1025 onBlur={() => setDraft({ isRecording: false })}992 onBlur={() => setDraft({ isRecording: false })}
1026 onChange={(event) => setDraft({ keyDraft: event.currentTarget.value })}993 onChange={(event) => setDraft({ keyDraft: event.currentTarget.value })}
1027 onKeyDown={captureKeybinding}994 onKeyDown={captureKeybinding}
1028 />995 />
1029 <Button996 <Button
1030 size="xs"997 size="compact"
1031 className="h-7 sm:h-7"
1032 disabled={isSaving || !commandDraft || keyDraft.trim().length === 0 || !isWhenDraftValid}998 disabled={isSaving || !commandDraft || keyDraft.trim().length === 0 || !isWhenDraftValid}
1033 onClick={save}999 onClick={save}
1034 >1000 >
...@@ -1066,7 +1032,7 @@ function NewKeybindingTableRow({...@@ -1066,7 +1032,7 @@ function NewKeybindingTableRow({
1066 type="button"1032 type="button"
1067 variant="ghost"1033 variant="ghost"
1068 size="icon-sm"1034 size="icon-sm"
1069 className="size-7 text-muted-foreground hover:text-foreground sm:size-7"1035 className="size-7 text-muted-foreground hover:text-foreground"
1070 disabled={isSaving}1036 disabled={isSaving}
1071 aria-label="Cancel new keybinding"1037 aria-label="Cancel new keybinding"
1072 onClick={onCancel}1038 onClick={onCancel}
...@@ -1247,9 +1213,8 @@ export function KeybindingsSettingsPanel() {...@@ -1247,9 +1213,8 @@ export function KeybindingsSettingsPanel() {
1247 render={1213 render={
1248 <Button1214 <Button
1249 type="button"1215 type="button"
1250 size="icon-xs"1216 size="icon-micro"
1251 variant="ghost"1217 variant="ghost-muted"
1252 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
1253 onClick={() => setIsAddingBinding(true)}1218 onClick={() => setIsAddingBinding(true)}
1254 aria-label="Add keybinding"1219 aria-label="Add keybinding"
1255 >1220 >
...@@ -1264,9 +1229,8 @@ export function KeybindingsSettingsPanel() {...@@ -1264,9 +1229,8 @@ export function KeybindingsSettingsPanel() {
1264 render={1229 render={
1265 <Button1230 <Button
1266 type="button"1231 type="button"
1267 size="icon-xs"1232 size="icon-micro"
1268 variant="ghost"1233 variant="ghost-muted"
1269 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
1270 disabled={!keybindingsConfigPath}1234 disabled={!keybindingsConfigPath}
1271 onClick={openKeybindingsFile}1235 onClick={openKeybindingsFile}
1272 aria-label="Open keybindings.json"1236 aria-label="Open keybindings.json"
apps/web/src/components/settings/ProjectSettingsPanel.tsx+1-1
...@@ -177,7 +177,7 @@ export function ProjectSettingsPage({ projectKey }: { projectKey: string }) {...@@ -177,7 +177,7 @@ export function ProjectSettingsPage({ projectKey }: { projectKey: string }) {
177 {!isElectron && (177 {!isElectron && (
178 <header178 <header
179 className={cn(179 className={cn(
180 "workspace-topbar px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",180 "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",
181 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,181 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
182 )}182 )}
183 >183 >
apps/web/src/components/settings/ProviderInstanceCard.tsx+4-5
...@@ -560,9 +560,9 @@ export function ProviderInstanceCard({...@@ -560,9 +560,9 @@ export function ProviderInstanceCard({
560 <TooltipTrigger560 <TooltipTrigger
561 render={561 render={
562 <Button562 <Button
563 size="icon-xs"563 size="icon-micro"
564 variant="ghost"564 variant="ghost"
565 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-destructive"565 className="text-muted-foreground hover:text-destructive"
566 onClick={onDelete}566 onClick={onDelete}
567 aria-label={`Delete provider instance ${instanceId}`}567 aria-label={`Delete provider instance ${instanceId}`}
568 >568 >
...@@ -708,9 +708,8 @@ export function ProviderInstanceCard({...@@ -708,9 +708,8 @@ export function ProviderInstanceCard({
708 </div>708 </div>
709 <div className="flex w-full shrink-0 items-center gap-2 sm:w-auto sm:justify-end">709 <div className="flex w-full shrink-0 items-center gap-2 sm:w-auto sm:justify-end">
710 <Button710 <Button
711 size="sm"711 size="compact"
712 variant="ghost"712 variant="ghost-muted"
713 className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
714 onClick={() => onExpandedChange(!isExpanded)}713 onClick={() => onExpandedChange(!isExpanded)}
715 aria-label={`Toggle ${displayName} details`}714 aria-label={`Toggle ${displayName} details`}
716 >715 >
apps/web/src/components/settings/ProviderModelsSection.tsx+13-20
...@@ -245,9 +245,9 @@ export function ProviderModelsSection({...@@ -245,9 +245,9 @@ export function ProviderModelsSection({
245 <TooltipTrigger245 <TooltipTrigger
246 render={246 render={
247 <Button247 <Button
248 size="icon-xs"248 size="icon-micro"
249 variant="ghost"249 variant="ghost"
250 className="size-5 rounded-sm p-0 text-muted-foreground/60 hover:text-muted-foreground"250 className="text-muted-foreground/60 hover:text-muted-foreground"
251 aria-label={`Details for ${model.name}`}251 aria-label={`Details for ${model.name}`}
252 />252 />
253 }253 }
...@@ -282,12 +282,9 @@ export function ProviderModelsSection({...@@ -282,12 +282,9 @@ export function ProviderModelsSection({
282 <TooltipTrigger282 <TooltipTrigger
283 render={283 render={
284 <Button284 <Button
285 size="icon-xs"285 size="icon-micro"
286 variant="ghost"286 variant="ghost-muted"
287 className={cn(287 className={cn(isFavorite && "text-yellow-500 hover:text-yellow-600")}
288 "size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground",
289 isFavorite && "text-yellow-500 hover:text-yellow-600",
290 )}
291 onClick={() => handleToggleFavorite(model.slug)}288 onClick={() => handleToggleFavorite(model.slug)}
292 aria-label={`${isFavorite ? "Remove" : "Add"} ${model.name} ${289 aria-label={`${isFavorite ? "Remove" : "Add"} ${model.name} ${
293 isFavorite ? "from" : "to"290 isFavorite ? "from" : "to"
...@@ -305,9 +302,8 @@ export function ProviderModelsSection({...@@ -305,9 +302,8 @@ export function ProviderModelsSection({
305 <TooltipTrigger302 <TooltipTrigger
306 render={303 render={
307 <Button304 <Button
308 size="icon-xs"305 size="icon-micro"
309 variant="ghost"306 variant="ghost-muted"
310 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
311 disabled={!canMoveUp}307 disabled={!canMoveUp}
312 onClick={() => handleMove(model.slug, -1)}308 onClick={() => handleMove(model.slug, -1)}
313 aria-label={`Move ${model.name} up`}309 aria-label={`Move ${model.name} up`}
...@@ -322,9 +318,8 @@ export function ProviderModelsSection({...@@ -322,9 +318,8 @@ export function ProviderModelsSection({
322 <TooltipTrigger318 <TooltipTrigger
323 render={319 render={
324 <Button320 <Button
325 size="icon-xs"321 size="icon-micro"
326 variant="ghost"322 variant="ghost-muted"
327 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
328 disabled={!canMoveDown}323 disabled={!canMoveDown}
329 onClick={() => handleMove(model.slug, 1)}324 onClick={() => handleMove(model.slug, 1)}
330 aria-label={`Move ${model.name} down`}325 aria-label={`Move ${model.name} down`}
...@@ -340,9 +335,8 @@ export function ProviderModelsSection({...@@ -340,9 +335,8 @@ export function ProviderModelsSection({
340 <TooltipTrigger335 <TooltipTrigger
341 render={336 render={
342 <Button337 <Button
343 size="icon-xs"338 size="icon-micro"
344 variant="ghost"339 variant="ghost-muted"
345 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
346 onClick={() => handleToggleHidden(model.slug)}340 onClick={() => handleToggleHidden(model.slug)}
347 aria-label={`${isHidden ? "Show" : "Hide"} ${model.name}`}341 aria-label={`${isHidden ? "Show" : "Hide"} ${model.name}`}
348 />342 />
...@@ -364,9 +358,8 @@ export function ProviderModelsSection({...@@ -364,9 +358,8 @@ export function ProviderModelsSection({
364 <TooltipTrigger358 <TooltipTrigger
365 render={359 render={
366 <Button360 <Button
367 size="icon-xs"361 size="icon-micro"
368 variant="ghost"362 variant="ghost-muted"
369 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
370 aria-label={`Remove ${model.slug}`}363 aria-label={`Remove ${model.slug}`}
371 onClick={() => handleRemove(model.slug)}364 onClick={() => handleRemove(model.slug)}
372 />365 />
apps/web/src/components/settings/ProviderSettingsPanel.tsx+4-6
...@@ -670,9 +670,8 @@ export function EnvironmentProviderSettings({...@@ -670,9 +670,8 @@ export function EnvironmentProviderSettings({
670 <TooltipTrigger670 <TooltipTrigger
671 render={671 render={
672 <Button672 <Button
673 size="icon-xs"673 size="icon-micro"
674 variant="ghost"674 variant="ghost-muted"
675 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
676 onClick={() => setIsAddInstanceDialogOpen(true)}675 onClick={() => setIsAddInstanceDialogOpen(true)}
677 aria-label="Add provider instance"676 aria-label="Add provider instance"
678 >677 >
...@@ -686,9 +685,8 @@ export function EnvironmentProviderSettings({...@@ -686,9 +685,8 @@ export function EnvironmentProviderSettings({
686 <TooltipTrigger685 <TooltipTrigger
687 render={686 render={
688 <Button687 <Button
689 size="icon-xs"688 size="icon-micro"
690 variant="ghost"689 variant="ghost-muted"
691 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
692 disabled={isRefreshingProviders}690 disabled={isRefreshingProviders}
693 onClick={() => void refreshProviders()}691 onClick={() => void refreshProviders()}
694 aria-label="Refresh provider status"692 aria-label="Refresh provider status"
apps/web/src/components/settings/ResourceTelemetryDiagnostics.tsx+6-8
...@@ -486,14 +486,14 @@ function ProcessTreeName({...@@ -486,14 +486,14 @@ function ProcessTreeName({
486 style={{ paddingLeft: `${Math.min(process.depth, 7) * 10}px` }}486 style={{ paddingLeft: `${Math.min(process.depth, 7) * 10}px` }}
487 >487 >
488 {hasChildren ? (488 {hasChildren ? (
489 <button489 <Button
490 type="button"490 size="icon-micro"
491 className="cursor-pointer inline-flex size-5 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"491 variant="ghost-muted"
492 onClick={() => onToggle(process)}492 onClick={() => onToggle(process)}
493 aria-label={collapsed ? `Expand ${name}` : `Collapse ${name}`}493 aria-label={collapsed ? `Expand ${name}` : `Collapse ${name}`}
494 >494 >
495 <ChevronIcon className="size-3.5" />495 <ChevronIcon className="size-3.5" />
496 </button>496 </Button>
497 ) : (497 ) : (
498 <span className="size-5" aria-hidden />498 <span className="size-5" aria-hidden />
499 )}499 )}
...@@ -975,9 +975,8 @@ export function ResourceTelemetryDiagnostics() {...@@ -975,9 +975,8 @@ export function ResourceTelemetryDiagnostics() {
975 <TooltipTrigger975 <TooltipTrigger
976 render={976 render={
977 <Button977 <Button
978 size="icon-xs"978 size="icon-micro"
979 variant="ghost"979 variant="ghost"
980 className="size-5 rounded-sm p-0"
981 disabled={telemetry.isPending}980 disabled={telemetry.isPending}
982 onClick={telemetry.refresh}981 onClick={telemetry.refresh}
983 aria-label="Refresh resource telemetry"982 aria-label="Refresh resource telemetry"
...@@ -1227,9 +1226,8 @@ export function ResourceTelemetryDiagnostics() {...@@ -1227,9 +1226,8 @@ export function ResourceTelemetryDiagnostics() {
1227 <div className="flex items-center gap-2">1226 <div className="flex items-center gap-2">
1228 <HistoryWindowSelector selectedWindowMs={windowMs} onSelect={setWindowMs} />1227 <HistoryWindowSelector selectedWindowMs={windowMs} onSelect={setWindowMs} />
1229 <Button1228 <Button
1230 size="icon-xs"1229 size="icon-micro"
1231 variant="ghost"1230 variant="ghost"
1232 className="size-5 rounded-sm p-0"
1233 disabled={history.isPending}1231 disabled={history.isPending}
1234 onClick={history.refresh}1232 onClick={history.refresh}
1235 aria-label="Refresh resource history"1233 aria-label="Refresh resource history"
apps/web/src/components/settings/SettingsSidebarNav.tsx+2-2
...@@ -220,9 +220,9 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -220,9 +220,9 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
220 {isSearching ? (220 {isSearching ? (
221 <Button221 <Button
222 type="button"222 type="button"
223 size="icon-xs"223 size="icon-micro"
224 variant="ghost"224 variant="ghost"
225 className="size-5 shrink-0 rounded-sm text-sidebar-muted-foreground hover:bg-sidebar-control-surface hover:text-sidebar-foreground"225 className="shrink-0 text-sidebar-muted-foreground hover:bg-sidebar-control-surface hover:text-sidebar-foreground"
226 aria-label="Clear settings search"226 aria-label="Clear settings search"
227 onClick={() => {227 onClick={() => {
228 clearSearch();228 clearSearch();
apps/web/src/components/settings/SourceControlSettings.tsx+14-31
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { ChevronDownIcon, GitPullRequestIcon, InfoIcon, RefreshCwIcon } from "lucide-react";1import { ChevronDownIcon, GitPullRequestIcon, RefreshCwIcon } from "lucide-react";
2import * as Duration from "effect/Duration";2import * as Duration from "effect/Duration";
3import * as Option from "effect/Option";3import * as Option from "effect/Option";
4import { useState, type ReactNode } from "react";4import { useState, type ReactNode } from "react";
...@@ -43,6 +43,7 @@ import {...@@ -43,6 +43,7 @@ import {
43} from "../ui/number-field";43} from "../ui/number-field";
44import { Switch } from "../ui/switch";44import { Switch } from "../ui/switch";
45import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";45import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
46
46import {47import {
47 AzureDevOpsIcon,48 AzureDevOpsIcon,
48 BitbucketIcon,49 BitbucketIcon,
...@@ -54,7 +55,12 @@ import {...@@ -54,7 +55,12 @@ import {
54} from "../Icons";55} from "../Icons";
55import { RedactedSensitiveText } from "./RedactedSensitiveText";56import { RedactedSensitiveText } from "./RedactedSensitiveText";
56import { SourceControlWritingSettingsSection } from "./SourceControlWritingSettings";57import { SourceControlWritingSettingsSection } from "./SourceControlWritingSettings";
57import { SettingResetButton, SettingsPageContainer, SettingsSection } from "./settingsLayout";58import {
59 PolicyTooltip,
60 SettingResetButton,
61 SettingsPageContainer,
62 SettingsSection,
63} from "./settingsLayout";
58import { searchableSetting } from "./settingsSearch";64import { searchableSetting } from "./settingsSearch";
5965
60const EMPTY_DISCOVERY_RESULT: SourceControlDiscoveryResult = {66const EMPTY_DISCOVERY_RESULT: SourceControlDiscoveryResult = {
...@@ -116,27 +122,6 @@ function backgroundActivityOverrideSettings(...@@ -116,27 +122,6 @@ function backgroundActivityOverrideSettings(
116 };122 };
117}123}
118124
119function BackgroundPolicyTooltip({ children }: { readonly children: string }) {
120 return (
121 <Tooltip>
122 <TooltipTrigger
123 render={
124 <button
125 type="button"
126 className="inline-flex size-5 items-center justify-center rounded-sm text-muted-foreground hover:text-foreground"
127 aria-label="Background policy details"
128 >
129 <InfoIcon className="size-3.5" />
130 </button>
131 }
132 />
133 <TooltipPopup side="top" className="max-w-72">
134 {children}
135 </TooltipPopup>
136 </Tooltip>
137 );
138}
139
140function optionLabel(value: Option.Option<string>): string | null {125function optionLabel(value: Option.Option<string>): string | null {
141 return Option.getOrNull(value);126 return Option.getOrNull(value);
142}127}
...@@ -316,9 +301,8 @@ function DiscoveryItemRow({...@@ -316,9 +301,8 @@ function DiscoveryItemRow({
316 <div className="flex w-full shrink-0 items-center gap-2 sm:w-auto sm:justify-end">301 <div className="flex w-full shrink-0 items-center gap-2 sm:w-auto sm:justify-end">
317 {hasDetails ? (302 {hasDetails ? (
318 <Button303 <Button
319 size="sm"304 size="compact"
320 variant="ghost"305 variant="ghost-muted"
321 className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
322 onClick={() => setIsExpanded((open) => !open)}306 onClick={() => setIsExpanded((open) => !open)}
323 aria-expanded={isExpanded}307 aria-expanded={isExpanded}
324 aria-label={`Toggle ${item.label} details`}308 aria-label={`Toggle ${item.label} details`}
...@@ -367,11 +351,11 @@ function GitFetchIntervalSettings() {...@@ -367,11 +351,11 @@ function GitFetchIntervalSettings() {
367 <div className="min-w-0 space-y-1">351 <div className="min-w-0 space-y-1">
368 <div className="flex min-w-0 items-center gap-1">352 <div className="flex min-w-0 items-center gap-1">
369 <span className="text-xs font-medium text-foreground">Fetch interval</span>353 <span className="text-xs font-medium text-foreground">Fetch interval</span>
370 <BackgroundPolicyTooltip>354 <PolicyTooltip>
371 This interval is configured for Git only. The shared Background activity policy still355 This interval is configured for Git only. The shared Background activity policy still
372 decides whether Git refreshes may run when the timer fires. Custom intervals appear as356 decides whether Git refreshes may run when the timer fires. Custom intervals appear as
373 Advanced in General settings.357 Advanced in General settings.
374 </BackgroundPolicyTooltip>358 </PolicyTooltip>
375 <span359 <span
376 className={cn(360 className={cn(
377 "inline-flex size-5 shrink-0 items-center justify-center transition-opacity",361 "inline-flex size-5 shrink-0 items-center justify-center transition-opacity",
...@@ -537,9 +521,8 @@ export function SourceControlSettingsPanel() {...@@ -537,9 +521,8 @@ export function SourceControlSettingsPanel() {
537 <TooltipTrigger521 <TooltipTrigger
538 render={522 render={
539 <Button523 <Button
540 size="icon-xs"524 size="icon-micro"
541 variant="ghost"525 variant="ghost-muted"
542 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
543 onClick={handleScan}526 onClick={handleScan}
544 disabled={discovery.isPending}527 disabled={discovery.isPending}
545 aria-label="Rescan server environment"528 aria-label="Rescan server environment"
apps/web/src/components/settings/ThemeImportDialog.tsx+4-4
...@@ -79,13 +79,13 @@ function highlightJson(value: string): string {...@@ -79,13 +79,13 @@ function highlightJson(value: string): string {
79 const index = match.index ?? 0;79 const index = match.index ?? 0;
80 highlighted += escapeJsonHtml(value.slice(cursor, index));80 highlighted += escapeJsonHtml(value.slice(cursor, index));
8181
82 let tokenClass = "theme-json-number";82 let tokenClass = "text-[var(--app-theme-secondary-foreground,var(--color-amber-600))]";
83 if (token.startsWith('"')) {83 if (token.startsWith('"')) {
84 tokenClass = /^\s*:/.test(value.slice(index + token.length))84 tokenClass = /^\s*:/.test(value.slice(index + token.length))
85 ? "theme-json-key"85 ? "text-[var(--app-theme-accent,var(--color-blue-600))]"
86 : "theme-json-string";86 : "text-[var(--app-theme-message-action,var(--color-emerald-600))]";
87 } else if (token === "true" || token === "false" || token === "null") {87 } else if (token === "true" || token === "false" || token === "null") {
88 tokenClass = "theme-json-constant";88 tokenClass = "text-[var(--app-theme-accent-surface-foreground,var(--color-violet-600))]";
89 }89 }
90 highlighted += `<span class="${tokenClass}">${escapeJsonHtml(token)}</span>`;90 highlighted += `<span class="${tokenClass}">${escapeJsonHtml(token)}</span>`;
91 cursor = index + token.length;91 cursor = index + token.length;
apps/web/src/components/settings/ThemeSearchSection.tsx+17-13
...@@ -30,7 +30,7 @@ import {...@@ -30,7 +30,7 @@ import {
30 AlertDialogTitle,30 AlertDialogTitle,
31} from "../ui/alert-dialog";31} from "../ui/alert-dialog";
32import { Button } from "../ui/button";32import { Button } from "../ui/button";
33import { Input } from "../ui/input";33import { InputGroup, InputGroupAddon, InputGroupInput } from "../ui/input-group";
34import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";34import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
35import { Spinner } from "../ui/spinner";35import { Spinner } from "../ui/spinner";
3636
...@@ -202,20 +202,24 @@ export function ThemeSearchSection({...@@ -202,20 +202,24 @@ export function ThemeSearchSection({
202 Find open-source themes from Open VSX.202 Find open-source themes from Open VSX.
203 </p>203 </p>
204 </div>204 </div>
205 <form className="relative flex gap-2" onSubmit={handleSearch}>205 <form className="flex gap-2" onSubmit={handleSearch}>
206 <SearchIcon className="pointer-events-none absolute left-3 top-1/2 z-10 size-4 -translate-y-1/2 text-muted-foreground" />206 <InputGroup>
207 <Input207 <InputGroupAddon>
208 aria-label="Search Open VSX themes"208 <SearchIcon />
209 className="rounded-xl [&_input]:pl-9"209 </InputGroupAddon>
210 autoFocus210 <InputGroupInput
211 onChange={(event) => setQuery(event.currentTarget.value)}211 aria-label="Search Open VSX themes"
212 placeholder="try dracula, nord, catppuccin..."212 autoFocus
213 size="lg"213 onChange={(event) => setQuery(event.currentTarget.value)}
214 type="search"214 placeholder="try dracula, nord, catppuccin..."
215 value={query}215 size="lg"
216 />216 type="search"
217 value={query}
218 />
219 </InputGroup>
217 <Button220 <Button
218 disabled={!query.trim() || isSearching || installingId !== null}221 disabled={!query.trim() || isSearching || installingId !== null}
222 size="lg"
219 type="submit"223 type="submit"
220 variant="outline"224 variant="outline"
221 >225 >
apps/web/src/components/settings/settingsLayout.tsx+8-10
...@@ -90,13 +90,9 @@ export function PolicyTooltip({ children }: { readonly children: string }) {...@@ -90,13 +90,9 @@ export function PolicyTooltip({ children }: { readonly children: string }) {
90 <TooltipTrigger90 <TooltipTrigger
91 delay={200}91 delay={200}
92 render={92 render={
93 <button93 <Button size="icon-micro" variant="ghost-muted" aria-label="Background policy details">
94 type="button"
95 className="inline-flex size-5 items-center justify-center rounded-sm text-muted-foreground hover:text-foreground"
96 aria-label="Background policy details"
97 >
98 <InfoIcon className="size-3.5" />94 <InfoIcon className="size-3.5" />
99 </button>95 </Button>
100 }96 }
101 />97 />
102 <TooltipPopup side="top" className="max-w-72">98 <TooltipPopup side="top" className="max-w-72">
...@@ -216,11 +212,10 @@ export function SettingResetButton({...@@ -216,11 +212,10 @@ export function SettingResetButton({
216 <TooltipTrigger212 <TooltipTrigger
217 render={213 render={
218 <Button214 <Button
219 size="icon-xs"215 size="icon-micro"
220 variant="ghost"216 variant="ghost-muted"
221 aria-label={`Reset ${label} to default`}217 aria-label={`Reset ${label} to default`}
222 disabled={disabled}218 disabled={disabled}
223 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
224 onClick={(event) => {219 onClick={(event) => {
225 event.stopPropagation();220 event.stopPropagation();
226 onClick();221 onClick();
...@@ -251,7 +246,10 @@ export function SettingsPageContainer({...@@ -251,7 +246,10 @@ export function SettingsPageContainer({
251246
252 return (247 return (
253 <SettingsSearchTargetProvider targetId={targetId} onTargetHandled={clearTargetHash}>248 <SettingsSearchTargetProvider targetId={targetId} onTargetHandled={clearTargetHash}>
254 <div className="settings-page-scroll-fade scrollbar-gutter-both flex-1 overflow-y-auto px-4 pt-10 pb-7 sm:px-8 sm:pt-12 sm:pb-10">249 <div
250 className="topbar-scroll-fade scrollbar-gutter-both flex-1 overflow-y-auto px-4 pt-10 pb-7 sm:px-8 sm:pt-12 sm:pb-10"
251 data-settings-page-scroll
252 >
255 <div className={cn("mx-auto flex w-full max-w-4xl flex-col gap-12", className)}>253 <div className={cn("mx-auto flex w-full max-w-4xl flex-col gap-12", className)}>
256 {children}254 {children}
257 </div>255 </div>
apps/web/src/components/sidebar/SidebarChrome.tsx+1-1
...@@ -83,7 +83,7 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) {...@@ -83,7 +83,7 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) {
83 <Link83 <Link
84 aria-label="Go to threads"84 aria-label="Go to threads"
85 className={cn(85 className={cn(
86 "sidebar-brand relative z-10 ml-[var(--workspace-titlebar-content-left)] h-7 w-fit min-w-0 shrink-0 items-center gap-1 overflow-hidden rounded-md outline-hidden ring-ring focus-visible:ring-2",86 "relative z-10 ml-[var(--workspace-titlebar-content-left)] hidden h-7 w-fit min-w-0 shrink-0 items-center gap-1 overflow-hidden rounded-md outline-hidden ring-ring focus-visible:ring-2 md:flex",
87 onBackdrop ? "text-white" : "text-foreground",87 onBackdrop ? "text-white" : "text-foreground",
88 )}88 )}
89 to="/"89 to="/"
apps/web/src/components/sidebar/SidebarProviderUpdatePill.tsx+7-5
...@@ -10,6 +10,7 @@ import {...@@ -10,6 +10,7 @@ import {
10 type ProviderUpdateSidebarPillView,10 type ProviderUpdateSidebarPillView,
11} from "../ProviderUpdateLaunchNotification.logic";11} from "../ProviderUpdateLaunchNotification.logic";
12import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";12import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
13import { Button } from "../ui/button";
1314
14const PROVIDER_UPDATE_PILL_STYLES = {15const PROVIDER_UPDATE_PILL_STYLES = {
15 loading:16 loading:
...@@ -151,7 +152,7 @@ export function SidebarProviderUpdatePill() {...@@ -151,7 +152,7 @@ export function SidebarProviderUpdatePill() {
151 <div152 <div
152 key={displayedView.key}153 key={displayedView.key}
153 aria-hidden="true"154 aria-hidden="true"
154 className={`provider-update-pill-progress pointer-events-none absolute inset-y-0 left-0 w-full origin-left border-r border-current/15 shadow-[inset_0_1px_0_rgb(255_255_255_/_0.08)] ${155 className={`pointer-events-none absolute inset-y-0 left-0 w-full origin-left animate-[provider-update-pill-countdown_var(--provider-update-pill-dismiss-ms)_linear_forwards] border-r border-current/15 shadow-[inset_0_1px_0_rgb(255_255_255_/_0.08)] ${
155 PROVIDER_UPDATE_PILL_PROGRESS_STYLES[displayedView.tone]156 PROVIDER_UPDATE_PILL_PROGRESS_STYLES[displayedView.tone]
156 }`}157 }`}
157 style={158 style={
...@@ -190,14 +191,15 @@ export function SidebarProviderUpdatePill() {...@@ -190,14 +191,15 @@ export function SidebarProviderUpdatePill() {
190 <Tooltip>191 <Tooltip>
191 <TooltipTrigger192 <TooltipTrigger
192 render={193 render={
193 <button194 <Button
194 type="button"195 size="icon-micro"
196 variant="ghost"
195 aria-label="Dismiss provider update notice"197 aria-label="Dismiss provider update notice"
196 className="relative z-[1] mr-1 inline-flex size-5 items-center justify-center rounded-md opacity-70 transition-opacity hover:opacity-100"198 className="relative z-[1] mr-1 [--control-icon-color:currentColor] rounded-md text-inherit opacity-70 hover:bg-transparent hover:opacity-100"
197 onClick={() => startExit(displayedView.key, null, displayedView.key)}199 onClick={() => startExit(displayedView.key, null, displayedView.key)}
198 >200 >
199 <XIcon className="size-3.5" />201 <XIcon className="size-3.5" />
200 </button>202 </Button>
201 }203 }
202 />204 />
203 <TooltipPopup side="top">Dismiss until provider status changes</TooltipPopup>205 <TooltipPopup side="top">Dismiss until provider status changes</TooltipPopup>
apps/web/src/components/threadSidebarWidth.test.ts+6-13
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off - Regression coverage compares shipped CSS with the sidebar width contract.1// @effect-diagnostics nodeBuiltinImport:off - Regression coverage compares the sidebar component with its width contract.
2import * as NodeFS from "node:fs";2import * as NodeFS from "node:fs";
33
4import { describe, expect, it } from "vite-plus/test";4import { describe, expect, it } from "vite-plus/test";
...@@ -36,20 +36,13 @@ describe("thread sidebar width", () => {...@@ -36,20 +36,13 @@ describe("thread sidebar width", () => {
36 });36 });
3737
38 it("shows the desktop wordmark across the sidebar's full legal width range", () => {38 it("shows the desktop wordmark across the sidebar's full legal width range", () => {
39 const sidebarStyles = NodeFS.readFileSync(new URL("../index.css", import.meta.url), "utf8");39 const sidebarSource = NodeFS.readFileSync(
40 const desktopHeaderStyles = sidebarStyles.slice(40 new URL("./sidebar/SidebarChrome.tsx", import.meta.url),
41 sidebarStyles.indexOf("@media (min-width: 48rem)"),41 "utf8",
42 sidebarStyles.indexOf("/* Stage-channel sidebar art"),
43 );42 );
44 const stageLabelThreshold = desktopHeaderStyles.match(
45 /@container sidebar-header \(min-width: ([\d.]+)rem\) \{\s*\.sidebar-brand-stage \{\s*display: inline-flex;/,
46 )?.[1];
4743
48 expect(sidebarStyles).toMatch(/\.sidebar-brand \{\s*display: none;/);44 expect(sidebarSource).toContain("hidden h-7 w-fit min-w-0 shrink-0 items-center gap-1");
49 expect(desktopHeaderStyles).toMatch(45 expect(sidebarSource).toContain("md:flex");
50 /@media \(min-width: 48rem\) \{\s*\.sidebar-brand \{\s*display: flex;/,
51 );
52 expect(THREAD_SIDEBAR_MIN_WIDTH).toBe(13 * 16);46 expect(THREAD_SIDEBAR_MIN_WIDTH).toBe(13 * 16);
53 expect(Number(stageLabelThreshold) * 16).toBeGreaterThan(THREAD_SIDEBAR_MIN_WIDTH);
54 });47 });
55});48});
apps/web/src/components/ui/button.test.tsx+15
...@@ -28,4 +28,19 @@ describe("button geometry tokens", () => {...@@ -28,4 +28,19 @@ describe("button geometry tokens", () => {
28 expect(html).toContain("size-7");28 expect(html).toContain("size-7");
29 expect(html).toContain("sm:size-6");29 expect(html).toContain("sm:size-6");
30 });30 });
31
32 it("owns shared compact and micro control geometry", () => {
33 const compact = renderToStaticMarkup(<Button size="compact">Condition</Button>);
34 const micro = renderToStaticMarkup(
35 <Button size="icon-micro" variant="ghost-muted" aria-label="Add action">
36 <span>+</span>
37 </Button>,
38 );
39
40 expect(compact).toContain("h-7");
41 expect(compact).toContain("rounded-md");
42 expect(micro).toContain("size-5");
43 expect(micro).toContain("rounded-sm");
44 expect(micro).toContain("text-muted-foreground");
45 });
31});46});
apps/web/src/components/ui/button.tsx+8
...@@ -16,9 +16,13 @@ const buttonVariants = cva(...@@ -16,9 +16,13 @@ const buttonVariants = cva(
16 },16 },
17 variants: {17 variants: {
18 size: {18 size: {
19 compact:
20 "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",
19 default: "h-9 px-[calc(--spacing(3)-1px)] sm:h-8",21 default: "h-9 px-[calc(--spacing(3)-1px)] sm:h-8",
20 icon: "size-9 sm:size-8",22 icon: "size-9 sm:size-8",
21 "icon-lg": "size-10 sm:size-9",23 "icon-lg": "size-10 sm:size-9",
24 "icon-micro":
25 "size-5 rounded-sm p-0 before:rounded-[calc(var(--radius-sm)-1px)] [&_svg:not([class*='size-'])]:size-3",
22 "icon-sm": "size-8 sm:size-7",26 "icon-sm": "size-8 sm:size-7",
23 "icon-xl":27 "icon-xl":
24 "size-11 sm:size-10 [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",28 "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(...@@ -38,6 +42,10 @@ const buttonVariants = cva(
38 "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",42 "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",
39 ghost:43 ghost:
40 "[--control-icon-color:var(--muted-foreground)] border-transparent text-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent",44 "[--control-icon-color:var(--muted-foreground)] border-transparent text-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent",
45 "ghost-muted":
46 "[--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",
47 glass:
48 "surface-glass [--control-icon-color:var(--muted-foreground)] border-border/60 text-foreground shadow-sm [:hover,[data-pressed]]:border-border",
41 link: "border-transparent underline-offset-4 [:hover,[data-pressed]]:underline",49 link: "border-transparent underline-offset-4 [:hover,[data-pressed]]:underline",
42 outline:50 outline:
43 "[--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",51 "[--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",
apps/web/src/components/ui/combobox.tsx+1-1
...@@ -170,7 +170,7 @@ function ComboboxPopup({...@@ -170,7 +170,7 @@ function ComboboxPopup({
170 >170 >
171 <span171 <span
172 className={cn(172 className={cn(
173 "dropdown-glass relative flex max-h-full min-w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) rounded-lg transition-[scale,opacity]",173 "dropdown-glass relative flex max-h-full min-w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) rounded-lg shadow-[0_16px_40px_-18px_rgb(0_0_0/55%)] transition-[scale,opacity] dark:shadow-[0_18px_44px_-18px_rgb(0_0_0/80%)]",
174 className,174 className,
175 )}175 )}
176 >176 >
apps/web/src/components/ui/input-group.tsx+2-2
...@@ -8,7 +8,7 @@ import { Input, type InputProps } from "~/components/ui/input";...@@ -8,7 +8,7 @@ import { Input, type InputProps } from "~/components/ui/input";
8import { Textarea, type TextareaProps } from "~/components/ui/textarea";8import { Textarea, type TextareaProps } from "~/components/ui/textarea";
99
10const inputGroupVariants = cva(10const inputGroupVariants = cva(
11 "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)]",11 "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)]",
12 {12 {
13 defaultVariants: {13 defaultVariants: {
14 variant: "default",14 variant: "default",
...@@ -16,7 +16,7 @@ const inputGroupVariants = cva(...@@ -16,7 +16,7 @@ const inputGroupVariants = cva(
16 variants: {16 variants: {
17 variant: {17 variant: {
18 default:18 default:
19 "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%)]",19 "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%)]",
20 ghost:20 ghost:
21 "border-transparent bg-transparent shadow-none hover:bg-muted/40 has-[input:focus-visible,textarea:focus-visible]:bg-background",21 "border-transparent bg-transparent shadow-none hover:bg-muted/40 has-[input:focus-visible,textarea:focus-visible]:bg-background",
22 },22 },
apps/web/src/components/ui/input.tsx+5-1
...@@ -6,7 +6,7 @@ import type * as React from "react";...@@ -6,7 +6,7 @@ import type * as React from "react";
6import { cn } from "~/lib/utils";6import { cn } from "~/lib/utils";
77
8type InputProps = Omit<InputPrimitive.Props & React.RefAttributes<HTMLInputElement>, "size"> & {8type InputProps = Omit<InputPrimitive.Props & React.RefAttributes<HTMLInputElement>, "size"> & {
9 size?: "sm" | "default" | "lg" | number;9 size?: "sm" | "compact" | "default" | "lg" | number;
10 unstyled?: boolean;10 unstyled?: boolean;
11 nativeInput?: boolean;11 nativeInput?: boolean;
12};12};
...@@ -20,6 +20,7 @@ function Input({...@@ -20,6 +20,7 @@ function Input({
20}: InputProps) {20}: InputProps) {
21 const inputClassName = cn(21 const inputClassName = cn(
22 "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]",22 "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]",
23 size === "compact" && "h-7 px-[calc(--spacing(2.5)-1px)] text-xs leading-7 sm:h-7 sm:leading-7",
23 size === "sm" && "h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5",24 size === "sm" && "h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5",
24 size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5",25 size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5",
25 props.type === "search" &&26 props.type === "search" &&
...@@ -59,6 +60,9 @@ function Input({...@@ -59,6 +60,9 @@ function Input({
59 cn(60 cn(
60 !unstyled &&61 !unstyled &&
61 "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%)]",62 "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%)]",
63 !unstyled &&
64 size === "compact" &&
65 "rounded-md before:rounded-[calc(var(--radius-md)-1px)]",
62 className,66 className,
63 ) || undefined67 ) || undefined
64 }68 }
apps/web/src/components/ui/menu.tsx+9-1
...@@ -36,6 +36,13 @@ function MenuPopup({...@@ -36,6 +36,13 @@ function MenuPopup({
36 side?: MenuPrimitive.Positioner.Props["side"];36 side?: MenuPrimitive.Positioner.Props["side"];
37 anchor?: MenuPrimitive.Positioner.Props["anchor"];37 anchor?: MenuPrimitive.Positioner.Props["anchor"];
38}) {38}) {
39 const hasExplicitWidthClass =
40 typeof className === "string" &&
41 className.split(/\s+/).some((classToken) => {
42 const utility = classToken.split(":").at(-1) ?? classToken;
43 return /^(?:min-|max-)?w-/.test(utility);
44 });
45
39 return (46 return (
40 <MenuPrimitive.Portal>47 <MenuPrimitive.Portal>
41 <MenuPrimitive.Positioner48 <MenuPrimitive.Positioner
...@@ -49,7 +56,8 @@ function MenuPopup({...@@ -49,7 +56,8 @@ function MenuPopup({
49 >56 >
50 <MenuPrimitive.Popup57 <MenuPrimitive.Popup
51 className={cn(58 className={cn(
52 "dropdown-glass relative flex not-[class*='w-']:min-w-32 origin-(--transform-origin) rounded-lg outline-none focus:outline-none",59 "dropdown-glass relative flex origin-(--transform-origin) rounded-lg shadow-[0_16px_40px_-18px_rgb(0_0_0/55%)] outline-none focus:outline-none dark:shadow-[0_18px_44px_-18px_rgb(0_0_0/80%)]",
60 !hasExplicitWidthClass && "min-w-32",
53 className,61 className,
54 )}62 )}
55 data-slot="menu-popup"63 data-slot="menu-popup"
apps/web/src/components/ui/popover.tsx+2
...@@ -52,6 +52,8 @@ function PopoverPopup({...@@ -52,6 +52,8 @@ function PopoverPopup({
52 "dropdown-glass relative flex h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) rounded-lg text-popover-foreground outline-none transition-[width,height,scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] has-data-[slot=calendar]:rounded-xl has-data-[slot=calendar]:before:rounded-[calc(var(--radius-xl)-1px)] data-starting-style:scale-98 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",52 "dropdown-glass relative flex h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) rounded-lg text-popover-foreground outline-none transition-[width,height,scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] has-data-[slot=calendar]:rounded-xl has-data-[slot=calendar]:before:rounded-[calc(var(--radius-xl)-1px)] data-starting-style:scale-98 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
53 tooltipStyle &&53 tooltipStyle &&
54 "w-fit text-balance rounded-md text-xs shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]",54 "w-fit text-balance rounded-md text-xs shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]",
55 !tooltipStyle &&
56 "shadow-[0_16px_40px_-18px_rgb(0_0_0/55%)] dark:shadow-[0_18px_44px_-18px_rgb(0_0_0/80%)]",
55 className,57 className,
56 )}58 )}
57 data-slot="popover-popup"59 data-slot="popover-popup"
apps/web/src/components/ui/scroll-area.tsx+18-1
...@@ -4,6 +4,23 @@ import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";...@@ -4,6 +4,23 @@ import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
44
5import { cn } from "~/lib/utils";5import { cn } from "~/lib/utils";
66
7function getVirtualizedScrollFadeClassName({ top, bottom }: { top: boolean; bottom: boolean }) {
8 if (!top && !bottom) return undefined;
9
10 return cn(
11 "virtualized-scroll-fade [--fade-size:1.5rem]",
12 top &&
13 bottom &&
14 "[--virtualized-scroll-fade-mask:linear-gradient(to_bottom,transparent,black_var(--fade-size),black_calc(100%-var(--fade-size)),transparent)]",
15 top &&
16 !bottom &&
17 "[--virtualized-scroll-fade-mask:linear-gradient(to_bottom,transparent,black_var(--fade-size))]",
18 !top &&
19 bottom &&
20 "[--virtualized-scroll-fade-mask:linear-gradient(to_bottom,black_calc(100%-var(--fade-size)),transparent)]",
21 );
22}
23
7function ScrollArea({24function ScrollArea({
8 className,25 className,
9 children,26 children,
...@@ -71,4 +88,4 @@ function ScrollBar({...@@ -71,4 +88,4 @@ function ScrollBar({
71 );88 );
72}89}
7390
74export { ScrollArea, ScrollBar };91export { getVirtualizedScrollFadeClassName, ScrollArea, ScrollBar };
apps/web/src/components/ui/select.tsx+3-1
...@@ -26,6 +26,8 @@ const selectTriggerVariants = cva(...@@ -26,6 +26,8 @@ const selectTriggerVariants = cva(
26 "border-transparent text-secondary-label focus-visible:ring-2 focus-visible:ring-ring data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent [:hover,[data-pressed]]:text-foreground",26 "border-transparent text-secondary-label focus-visible:ring-2 focus-visible:ring-ring data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent [:hover,[data-pressed]]:text-foreground",
27 },27 },
28 size: {28 size: {
29 compact:
30 "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",
29 default: "min-h-9 px-[calc(--spacing(3)-1px)] sm:min-h-8",31 default: "min-h-9 px-[calc(--spacing(3)-1px)] sm:min-h-8",
30 lg: "min-h-10 px-[calc(--spacing(3)-1px)] sm:min-h-9",32 lg: "min-h-10 px-[calc(--spacing(3)-1px)] sm:min-h-9",
31 sm: "min-h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:min-h-7",33 sm: "min-h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:min-h-7",
...@@ -150,7 +152,7 @@ function SelectPopup({...@@ -150,7 +152,7 @@ function SelectPopup({
150 </SelectPrimitive.ScrollUpArrow>152 </SelectPrimitive.ScrollUpArrow>
151 <div153 <div
152 className={cn(154 className={cn(
153 "dropdown-glass relative h-full rounded-lg",155 "dropdown-glass relative h-full rounded-lg shadow-[0_16px_40px_-18px_rgb(0_0_0/55%)] dark:shadow-[0_18px_44px_-18px_rgb(0_0_0/80%)]",
154 matchTriggerWidth && "min-w-(--anchor-width)",156 matchTriggerWidth && "min-w-(--anchor-width)",
155 popupClassName,157 popupClassName,
156 )}158 )}
apps/web/src/components/ui/sidebar.tsx+2
...@@ -166,6 +166,8 @@ function SidebarProvider({...@@ -166,6 +166,8 @@ function SidebarProvider({
166 {166 {
167 "--sidebar-width": SIDEBAR_WIDTH,167 "--sidebar-width": SIDEBAR_WIDTH,
168 "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,168 "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
169 "--workspace-titlebar-content-left":
170 "calc(var(--workspace-controls-left) + var(--workspace-titlebar-control-size) + var(--workspace-titlebar-control-gap))",
169 ...style,171 ...style,
170 } as React.CSSProperties172 } as React.CSSProperties
171 }173 }
apps/web/src/components/ui/skeleton.tsx+1-1
...@@ -4,7 +4,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {...@@ -4,7 +4,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4 return (4 return (
5 <div5 <div
6 className={cn(6 className={cn(
7 "relative overflow-hidden rounded-sm bg-muted [--skeleton-highlight:--alpha(var(--color-white)/64%)] after:absolute after:inset-0 after:animate-skeleton after:bg-[linear-gradient(120deg,transparent_40%,var(--skeleton-highlight),transparent_60%)] dark:[--skeleton-highlight:--alpha(var(--color-white)/4%)]",7 "relative overflow-hidden rounded-sm bg-muted [--skeleton-highlight:--alpha(var(--color-white)/64%)] after:absolute after:inset-0 after:animate-skeleton after:bg-[linear-gradient(120deg,transparent_40%,var(--skeleton-highlight),transparent_60%)] motion-reduce:after:content-none dark:[--skeleton-highlight:--alpha(var(--color-white)/4%)]",
8 className,8 className,
9 )}9 )}
10 data-slot="skeleton"10 data-slot="skeleton"
apps/web/src/components/ui/toast.tsx+13-14
...@@ -26,7 +26,7 @@ import {...@@ -26,7 +26,7 @@ import {
26} from "lucide-react";26} from "lucide-react";
2727
28import { cn } from "~/lib/utils";28import { cn } from "~/lib/utils";
29import { buttonVariants } from "~/components/ui/button";29import { Button, buttonVariants } from "~/components/ui/button";
30import { useComposerDraftStore } from "~/composerDraftStore";30import { useComposerDraftStore } from "~/composerDraftStore";
31import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";31import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
32import { resolveThreadRouteTarget } from "~/threadRoutes";32import { resolveThreadRouteTarget } from "~/threadRoutes";
...@@ -124,11 +124,12 @@ function CopyErrorButton({ text }: { text: string }) {...@@ -124,11 +124,12 @@ function CopyErrorButton({ text }: { text: string }) {
124 <Tooltip>124 <Tooltip>
125 <TooltipTrigger125 <TooltipTrigger
126 render={126 render={
127 <button127 <Button
128 size="icon-micro"
129 variant="ghost-muted"
128 aria-label={label}130 aria-label={label}
129 className="inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-md p-0 text-muted-foreground/80 transition-colors hover:text-muted-foreground"131 className="[--control-icon-color:currentColor] rounded-md text-muted-foreground/80 hover:bg-transparent hover:text-muted-foreground"
130 onClick={() => copyToClipboard(text)}132 onClick={() => copyToClipboard(text)}
131 type="button"
132 />133 />
133 }134 }
134 >135 >
...@@ -381,24 +382,22 @@ function ToastBodyContent({...@@ -381,24 +382,22 @@ function ToastBodyContent({
381 >382 >
382 {copyErrorText !== null ? <CopyErrorButton text={copyErrorText} /> : null}383 {copyErrorText !== null ? <CopyErrorButton text={copyErrorText} /> : null}
383 {additionalActions.map(({ id, props: { className, ...props } }) => (384 {additionalActions.map(({ id, props: { className, ...props } }) => (
384 <button385 <Button
385 {...props}386 {...props}
386 className={cn(387 className={className}
387 buttonVariants({ size: "xs", variant: secondaryActionVariant }),
388 className,
389 )}
390 key={id}388 key={id}
389 size="xs"
391 type="button"390 type="button"
391 variant={secondaryActionVariant}
392 />392 />
393 ))}393 ))}
394 {secondaryActionProps ? (394 {secondaryActionProps ? (
395 <button395 <Button
396 {...secondaryActionRest}396 {...secondaryActionRest}
397 className={cn(397 className={secondaryActionClassName}
398 buttonVariants({ size: "xs", variant: secondaryActionVariant }),398 size="xs"
399 secondaryActionClassName,
400 )}
401 type="button"399 type="button"
400 variant={secondaryActionVariant}
402 />401 />
403 ) : null}402 ) : null}
404 {actionProps ? (403 {actionProps ? (
apps/web/src/components/ui/toggle.tsx+2
...@@ -14,6 +14,8 @@ const toggleVariants = cva(...@@ -14,6 +14,8 @@ const toggleVariants = cva(
14 },14 },
15 variants: {15 variants: {
16 size: {16 size: {
17 compact:
18 "h-7 min-w-7 rounded-md px-[calc(--spacing(1)-1px)] text-xs before:rounded-[calc(var(--radius-md)-1px)] [&_svg:not([class*='size-'])]:size-3.5",
17 default: "h-9 min-w-9 px-[calc(--spacing(2)-1px)] sm:h-8 sm:min-w-8",19 default: "h-9 min-w-9 px-[calc(--spacing(2)-1px)] sm:h-8 sm:min-w-8",
18 lg: "h-10 min-w-10 px-[calc(--spacing(2.5)-1px)] sm:h-9 sm:min-w-9",20 lg: "h-10 min-w-10 px-[calc(--spacing(2.5)-1px)] sm:h-9 sm:min-w-9",
19 sm: "h-8 min-w-8 px-[calc(--spacing(1.5)-1px)] sm:h-7 sm:min-w-7",21 sm: "h-8 min-w-8 px-[calc(--spacing(1.5)-1px)] sm:h-7 sm:min-w-7",
apps/web/src/components/usage/UsagePage.tsx+6-5
...@@ -20,6 +20,7 @@ import {...@@ -20,6 +20,7 @@ import {
20 makeWindow,20 makeWindow,
21} from "@t3tools/shared/usageFormat";21} from "@t3tools/shared/usageFormat";
22import { ScrollArea } from "../ui/scroll-area";22import { ScrollArea } from "../ui/scroll-area";
23import { Button } from "../ui/button";
23import { SidebarInset } from "../ui/sidebar";24import { SidebarInset } from "../ui/sidebar";
24import { WorkspaceBreadcrumb, WorkspaceBreadcrumbItem } from "../WorkspaceBreadcrumb";25import { WorkspaceBreadcrumb, WorkspaceBreadcrumbItem } from "../WorkspaceBreadcrumb";
25import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../../workspaceTitlebar";26import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../../workspaceTitlebar";
...@@ -106,7 +107,7 @@ export function UsagePage() {...@@ -106,7 +107,7 @@ export function UsagePage() {
106 {!isElectron && (107 {!isElectron && (
107 <header108 <header
108 className={cn(109 className={cn(
109 "workspace-topbar px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",110 "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",
110 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,111 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
111 )}112 )}
112 >113 >
...@@ -156,14 +157,14 @@ export function UsagePage() {...@@ -156,14 +157,14 @@ export function UsagePage() {
156 </button>157 </button>
157 ))}158 ))}
158 </div>159 </div>
159 <button160 <Button
160 type="button"161 size="icon"
162 variant="outline"
161 onClick={refreshWindow}163 onClick={refreshWindow}
162 aria-label="Refresh usage"164 aria-label="Refresh usage"
163 className="cursor-pointer rounded-md border border-border p-2 text-muted-foreground hover:text-foreground"
164 >165 >
165 <RefreshCwIcon className="size-3.5" />166 <RefreshCwIcon className="size-3.5" />
166 </button>167 </Button>
167 </div>168 </div>
168 </div>169 </div>
169170
apps/web/src/index.css+352-749
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1@import "tailwindcss";1@import "tailwindcss";
22
3@custom-variant dark (&:is(.dark, .dark *));3@custom-variant dark (&:is(.dark, .dark *));
4@custom-variant light (&:not(.dark, .dark *));
4/* Window Controls Overlay: active when Electron exposes native titlebar control geometry. */5/* Window Controls Overlay: active when Electron exposes native titlebar control geometry. */
5@custom-variant wco (&:is(.wco, .wco *));6@custom-variant wco (&:is(.wco, .wco *));
67
...@@ -102,20 +103,13 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -102,20 +103,13 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
102 --workspace-native-controls-inset: 0px;103 --workspace-native-controls-inset: 0px;
103 --workspace-titlebar-control-size: 1.75rem;104 --workspace-titlebar-control-size: 1.75rem;
104 --workspace-titlebar-control-gap: 0.75rem;105 --workspace-titlebar-control-gap: 0.75rem;
105}
106
107.dark {
108 --app-scrollbar-thumb: rgb(255 255 255 / 8%);
109 --app-scrollbar-thumb-hover: rgb(255 255 255 / 12%);
110 --glass-blur: 16px;
111 --glass-saturation: 1.08;
112}
113106
114[data-slot="sidebar-wrapper"] {107 @variant dark {
115 --workspace-titlebar-content-left: calc(108 --app-scrollbar-thumb: rgb(255 255 255 / 8%);
116 var(--workspace-controls-left) + var(--workspace-titlebar-control-size) +109 --app-scrollbar-thumb-hover: rgb(255 255 255 / 12%);
117 var(--workspace-titlebar-control-gap)110 --glass-blur: 16px;
118 );111 --glass-saturation: 1.08;
112 }
119}113}
120114
121.wco {115.wco {
...@@ -264,6 +258,179 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -264,6 +258,179 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
264 }258 }
265}259}
266260
261@utility surface-glass {
262 background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent);
263 -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
264 backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
265
266 @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
267 background: var(--background) !important;
268 }
269}
270
271@utility alert-glass {
272 --alert-glass-tint: transparent;
273 background:
274 linear-gradient(
275 color-mix(in srgb, var(--alert-glass-tint) 4%, transparent),
276 color-mix(in srgb, var(--alert-glass-tint) 4%, transparent)
277 ),
278 color-mix(in srgb, var(--background) var(--glass-opacity), transparent) !important;
279 -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
280 backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
281
282 &[data-variant="error"] {
283 --alert-glass-tint: var(--destructive);
284 }
285
286 &[data-variant="info"] {
287 --alert-glass-tint: var(--info);
288 }
289
290 &[data-variant="success"] {
291 --alert-glass-tint: var(--success);
292 }
293
294 &[data-variant="warning"] {
295 --alert-glass-tint: var(--warning);
296 }
297
298 @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
299 background: var(--background) !important;
300 }
301}
302
303@utility dialog-glass {
304 background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent);
305 -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
306 backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
307 border-color: color-mix(in srgb, var(--foreground) 10%, transparent);
308 box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%);
309
310 @variant dark {
311 border-color: color-mix(in srgb, var(--color-white) 8%, transparent);
312 box-shadow:
313 inset 0 1px rgb(255 255 255 / 4%),
314 0 24px 72px -20px rgb(0 0 0 / 90%);
315 }
316
317 @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
318 background: var(--popover) !important;
319 }
320}
321
322@utility dialog-backdrop {
323 background: color-mix(in srgb, var(--background) 60%, transparent);
324 -webkit-backdrop-filter: blur(4px);
325 backdrop-filter: blur(4px);
326
327 @variant dark {
328 background: color-mix(in srgb, var(--background) 64%, transparent);
329 }
330}
331
332@utility dropdown-glass {
333 background: color-mix(
334 in srgb,
335 var(--popover) 18%,
336 color-mix(in srgb, var(--popover) var(--glass-opacity), transparent)
337 );
338 -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
339 backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
340 border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
341
342 @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
343 background: var(--popover) !important;
344 }
345}
346
347@utility topbar-scroll-fade {
348 --topbar-scroll-fade-height: 2.5rem;
349 -webkit-mask-image:
350 linear-gradient(
351 to bottom,
352 transparent 0%,
353 rgb(0 0 0 / 10%) 10%,
354 rgb(0 0 0 / 30%) 24%,
355 rgb(0 0 0 / 58%) 42%,
356 rgb(0 0 0 / 82%) 62%,
357 rgb(0 0 0 / 96%) 82%,
358 black 100%
359 ),
360 linear-gradient(black, black), linear-gradient(black, black);
361 -webkit-mask-position: top, bottom, right;
362 -webkit-mask-repeat: no-repeat;
363 -webkit-mask-size:
364 100% var(--topbar-scroll-fade-height),
365 100% calc(100% - var(--topbar-scroll-fade-height)),
366 var(--app-scrollbar-width) 100%;
367 mask-image:
368 linear-gradient(
369 to bottom,
370 transparent 0%,
371 rgb(0 0 0 / 10%) 10%,
372 rgb(0 0 0 / 30%) 24%,
373 rgb(0 0 0 / 58%) 42%,
374 rgb(0 0 0 / 82%) 62%,
375 rgb(0 0 0 / 96%) 82%,
376 black 100%
377 ),
378 linear-gradient(black, black), linear-gradient(black, black);
379 mask-position: top, bottom, right;
380 mask-repeat: no-repeat;
381 mask-size:
382 100% var(--topbar-scroll-fade-height),
383 100% calc(100% - var(--topbar-scroll-fade-height)),
384 var(--app-scrollbar-width) 100%;
385
386 @variant sm {
387 --topbar-scroll-fade-height: 3rem;
388 }
389}
390
391/* Virtualizers own their native scroll element, so they cannot use ScrollArea's
392 viewport fade. Keep the scrollbar lane opaque while sharing the same fade
393 contract across those lists. */
394@utility virtualized-scroll-fade {
395 -webkit-mask-image: var(--virtualized-scroll-fade-mask), linear-gradient(black, black);
396 mask-image: var(--virtualized-scroll-fade-mask), linear-gradient(black, black);
397 -webkit-mask-position: left, right;
398 mask-position: left, right;
399 -webkit-mask-repeat: no-repeat;
400 mask-repeat: no-repeat;
401 -webkit-mask-size:
402 calc(100% - var(--app-scrollbar-width)) 100%,
403 var(--app-scrollbar-width) 100%;
404 mask-size:
405 calc(100% - var(--app-scrollbar-width)) 100%,
406 var(--app-scrollbar-width) 100%;
407}
408
409/* Stage-channel art needs a mask and pseudo-element gradient, so keep the
410 behavior composable without tying it to the global components layer. */
411@utility sidebar-stage-backdrop {
412 --stage-fade: var(--sidebar-stage-fade, var(--app-chrome-background));
413 mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%);
414 -webkit-mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%);
415
416 &::after {
417 content: "";
418 position: absolute;
419 inset: 0;
420 background: linear-gradient(
421 to bottom,
422 transparent 0%,
423 transparent 28%,
424 color-mix(in srgb, var(--stage-fade) 10%, transparent) 40%,
425 color-mix(in srgb, var(--stage-fade) 30%, transparent) 52%,
426 color-mix(in srgb, var(--stage-fade) 58%, transparent) 64%,
427 color-mix(in srgb, var(--stage-fade) 82%, transparent) 75%,
428 color-mix(in srgb, var(--stage-fade) 96%, transparent) 85%,
429 var(--stage-fade) 93%
430 );
431 }
432}
433
267@layer base {434@layer base {
268 :root {435 :root {
269 /* Keep the original T3 Code artwork palettes as the defaults. Built-in436 /* 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...@@ -292,12 +459,12 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
292 --stage-night-glow-highlight: oklch(0.553749 0.176543 271.958);459 --stage-night-glow-highlight: oklch(0.553749 0.176543 271.958);
293 --stage-night-glow-secondary: oklch(0.345571 0.117466 273.568);460 --stage-night-glow-secondary: oklch(0.345571 0.117466 273.568);
294 --stage-night-sparkle: oklch(0.880867 0.057747 269.011);461 --stage-night-sparkle: oklch(0.880867 0.057747 269.011);
295 }
296462
297 .dark {463 @variant dark {
298 --stage-art-top: oklch(0.581473 0.149124 256.9);464 --stage-art-top: oklch(0.581473 0.149124 256.9);
299 --stage-art-mid: oklch(0.456509 0.159377 261.945);465 --stage-art-mid: oklch(0.456509 0.159377 261.945);
300 --stage-art-bottom: oklch(0.291327 0.136578 267.649);466 --stage-art-bottom: oklch(0.291327 0.136578 267.649);
467 }
301 }468 }
302469
303 * {470 * {
...@@ -311,63 +478,16 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -311,63 +478,16 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
311 ):focus-visible {478 ):focus-visible {
312 @apply outline-none ring-0;479 @apply outline-none ring-0;
313 }480 }
314 html {481 html,
482 body {
315 background-color: var(--app-chrome-background);483 background-color: var(--app-chrome-background);
316 }484 }
317 body {485 body {
318 @apply text-foreground relative;486 @apply text-foreground relative;
319 background-color: var(--app-chrome-background);
320 }487 }
321}488}
322489
323@layer components {490@layer components {
324 .sidebar-brand {
325 display: none;
326 }
327
328 .sidebar-brand-stage {
329 display: none;
330 }
331
332 @media (min-width: 48rem) {
333 .sidebar-brand {
334 display: flex;
335 }
336
337 @container sidebar-header (min-width: 15.75rem) {
338 .sidebar-brand-stage {
339 display: inline-flex;
340 }
341 }
342 }
343
344 /* Stage-channel sidebar art; ::after ramps to the sidebar bg color and the
345 mask lets the surface grain show through at the boundary. Panels whose
346 background differs from the app chrome (e.g. sidebar v2) override
347 --sidebar-stage-fade so the art fades into their own surface color. */
348 .sidebar-stage-backdrop {
349 --stage-fade: var(--sidebar-stage-fade, var(--app-chrome-background));
350 mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%);
351 -webkit-mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%);
352 }
353
354 .sidebar-stage-backdrop::after {
355 content: "";
356 position: absolute;
357 inset: 0;
358 background: linear-gradient(
359 to bottom,
360 transparent 0%,
361 transparent 28%,
362 color-mix(in srgb, var(--stage-fade) 10%, transparent) 40%,
363 color-mix(in srgb, var(--stage-fade) 30%, transparent) 52%,
364 color-mix(in srgb, var(--stage-fade) 58%, transparent) 64%,
365 color-mix(in srgb, var(--stage-fade) 82%, transparent) 75%,
366 color-mix(in srgb, var(--stage-fade) 96%, transparent) 85%,
367 var(--stage-fade) 93%
368 );
369 }
370
371 /* Each maintainer palette gives the same line art its own material: rose491 /* Each maintainer palette gives the same line art its own material: rose
372 vellum, forest drafting paper, marine cyanotype, copper, and violet ink.492 vellum, forest drafting paper, marine cyanotype, copper, and violet ink.
373 These colors stay deliberately deep at the top edge so the white stage493 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...@@ -380,16 +500,16 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
380 --stage-art-secondary: oklch(0.763402 0.163836 352.525);500 --stage-art-secondary: oklch(0.763402 0.163836 352.525);
381 --stage-art-tertiary: oklch(0.70819 0.180285 311.949);501 --stage-art-tertiary: oklch(0.70819 0.180285 311.949);
382 --stage-art-line: oklch(0.952158 0.034194 336.179);502 --stage-art-line: oklch(0.952158 0.034194 336.179);
383 }
384503
385 html.dark[data-theme-id="t3-chat"] {504 @variant dark {
386 --stage-art-top: oklch(0.540689 0.143665 347.587);505 --stage-art-top: oklch(0.540689 0.143665 347.587);
387 --stage-art-mid: oklch(0.396586 0.126592 347.6);506 --stage-art-mid: oklch(0.396586 0.126592 347.6);
388 --stage-art-bottom: oklch(0.249959 0.079694 340.523);507 --stage-art-bottom: oklch(0.249959 0.079694 340.523);
389 --stage-art-highlight: oklch(0.921297 0.051708 343.229);508 --stage-art-highlight: oklch(0.921297 0.051708 343.229);
390 --stage-art-secondary: oklch(0.667398 0.165674 352.549);509 --stage-art-secondary: oklch(0.667398 0.165674 352.549);
391 --stage-art-tertiary: oklch(0.609315 0.163722 306.315);510 --stage-art-tertiary: oklch(0.609315 0.163722 306.315);
392 --stage-art-line: oklch(0.945349 0.036045 341.433);511 --stage-art-line: oklch(0.945349 0.036045 341.433);
512 }
393 }513 }
394514
395 html[data-theme-id="grove"] {515 html[data-theme-id="grove"] {
...@@ -407,23 +527,23 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -407,23 +527,23 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
407 --stage-night-secondary: oklch(0.665652 0.109731 156.599);527 --stage-night-secondary: oklch(0.665652 0.109731 156.599);
408 --stage-night-tertiary: oklch(0.698651 0.103024 89.828);528 --stage-night-tertiary: oklch(0.698651 0.103024 89.828);
409 --stage-night-line: oklch(0.945336 0.041923 157.222);529 --stage-night-line: oklch(0.945336 0.041923 157.222);
410 }
411530
412 html.dark[data-theme-id="grove"] {531 @variant dark {
413 --stage-art-top: oklch(0.58719 0.09869 157.426);532 --stage-art-top: oklch(0.58719 0.09869 157.426);
414 --stage-art-mid: oklch(0.454979 0.079031 159.756);533 --stage-art-mid: oklch(0.454979 0.079031 159.756);
415 --stage-art-bottom: oklch(0.297856 0.050355 161.167);534 --stage-art-bottom: oklch(0.297856 0.050355 161.167);
416 --stage-art-highlight: oklch(0.952407 0.053872 158.44);535 --stage-art-highlight: oklch(0.952407 0.053872 158.44);
417 --stage-art-secondary: oklch(0.732591 0.120606 155.853);536 --stage-art-secondary: oklch(0.732591 0.120606 155.853);
418 --stage-art-tertiary: oklch(0.716282 0.116547 80.563);537 --stage-art-tertiary: oklch(0.716282 0.116547 80.563);
419 --stage-art-line: oklch(0.961577 0.035285 157.03);538 --stage-art-line: oklch(0.961577 0.035285 157.03);
420 --stage-night-top: oklch(0.398632 0.065534 158.601);539 --stage-night-top: oklch(0.398632 0.065534 158.601);
421 --stage-night-mid: oklch(0.290561 0.049694 160.456);540 --stage-night-mid: oklch(0.290561 0.049694 160.456);
422 --stage-night-bottom: oklch(0.210147 0.03173 169.818);541 --stage-night-bottom: oklch(0.210147 0.03173 169.818);
423 --stage-night-highlight: oklch(0.866303 0.057526 156.796);542 --stage-night-highlight: oklch(0.866303 0.057526 156.796);
424 --stage-night-secondary: oklch(0.586553 0.093722 157.365);543 --stage-night-secondary: oklch(0.586553 0.093722 157.365);
425 --stage-night-tertiary: oklch(0.6364 0.101769 82.985);544 --stage-night-tertiary: oklch(0.6364 0.101769 82.985);
426 --stage-night-line: oklch(0.913292 0.035718 156.976);545 --stage-night-line: oklch(0.913292 0.035718 156.976);
546 }
427 }547 }
428548
429 html[data-theme-id="ocean"] {549 html[data-theme-id="ocean"] {
...@@ -434,16 +554,16 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -434,16 +554,16 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
434 --stage-art-secondary: oklch(0.788391 0.090856 215.684);554 --stage-art-secondary: oklch(0.788391 0.090856 215.684);
435 --stage-art-tertiary: oklch(0.76441 0.099607 187.893);555 --stage-art-tertiary: oklch(0.76441 0.099607 187.893);
436 --stage-art-line: oklch(0.976025 0.019647 212.543);556 --stage-art-line: oklch(0.976025 0.019647 212.543);
437 }
438557
439 html.dark[data-theme-id="ocean"] {558 @variant dark {
440 --stage-art-top: oklch(0.59663 0.089167 233.427);559 --stage-art-top: oklch(0.59663 0.089167 233.427);
441 --stage-art-mid: oklch(0.461094 0.084904 243.478);560 --stage-art-mid: oklch(0.461094 0.084904 243.478);
442 --stage-art-bottom: oklch(0.294818 0.05947 250.526);561 --stage-art-bottom: oklch(0.294818 0.05947 250.526);
443 --stage-art-highlight: oklch(0.952907 0.032224 221.27);562 --stage-art-highlight: oklch(0.952907 0.032224 221.27);
444 --stage-art-secondary: oklch(0.732079 0.09296 224.414);563 --stage-art-secondary: oklch(0.732079 0.09296 224.414);
445 --stage-art-tertiary: oklch(0.720885 0.095495 190.903);564 --stage-art-tertiary: oklch(0.720885 0.095495 190.903);
446 --stage-art-line: oklch(0.961039 0.027355 219.756);565 --stage-art-line: oklch(0.961039 0.027355 219.756);
566 }
447 }567 }
448568
449 html[data-theme-id="ember"] {569 html[data-theme-id="ember"] {
...@@ -461,23 +581,23 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -461,23 +581,23 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
461 --stage-night-secondary: oklch(0.641705 0.126508 44.376);581 --stage-night-secondary: oklch(0.641705 0.126508 44.376);
462 --stage-night-tertiary: oklch(0.538694 0.129931 25.865);582 --stage-night-tertiary: oklch(0.538694 0.129931 25.865);
463 --stage-night-line: oklch(0.926348 0.046029 58.73);583 --stage-night-line: oklch(0.926348 0.046029 58.73);
464 }
465584
466 html.dark[data-theme-id="ember"] {585 @variant dark {
467 --stage-art-top: oklch(0.597533 0.120694 43.455);586 --stage-art-top: oklch(0.597533 0.120694 43.455);
468 --stage-art-mid: oklch(0.437763 0.101287 34.86);587 --stage-art-mid: oklch(0.437763 0.101287 34.86);
469 --stage-art-bottom: oklch(0.264269 0.055858 26.548);588 --stage-art-bottom: oklch(0.264269 0.055858 26.548);
470 --stage-art-highlight: oklch(0.929214 0.042638 55.801);589 --stage-art-highlight: oklch(0.929214 0.042638 55.801);
471 --stage-art-secondary: oklch(0.705592 0.137369 43.176);590 --stage-art-secondary: oklch(0.705592 0.137369 43.176);
472 --stage-art-tertiary: oklch(0.629583 0.158322 24.088);591 --stage-art-tertiary: oklch(0.629583 0.158322 24.088);
473 --stage-art-line: oklch(0.945058 0.033906 58.824);592 --stage-art-line: oklch(0.945058 0.033906 58.824);
474 --stage-night-top: oklch(0.392352 0.081287 36.444);593 --stage-night-top: oklch(0.392352 0.081287 36.444);
475 --stage-night-mid: oklch(0.271305 0.056352 31.135);594 --stage-night-mid: oklch(0.271305 0.056352 31.135);
476 --stage-night-bottom: oklch(0.182126 0.028154 27.774);595 --stage-night-bottom: oklch(0.182126 0.028154 27.774);
477 --stage-night-highlight: oklch(0.851007 0.061294 53.805);596 --stage-night-highlight: oklch(0.851007 0.061294 53.805);
478 --stage-night-secondary: oklch(0.560789 0.10645 42.953);597 --stage-night-secondary: oklch(0.560789 0.10645 42.953);
479 --stage-night-tertiary: oklch(0.476228 0.106656 24.165);598 --stage-night-tertiary: oklch(0.476228 0.106656 24.165);
480 --stage-night-line: oklch(0.884931 0.046607 56.556);599 --stage-night-line: oklch(0.884931 0.046607 56.556);
600 }
481 }601 }
482602
483 html[data-theme-id="iris"] {603 html[data-theme-id="iris"] {
...@@ -488,16 +608,16 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -488,16 +608,16 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
488 --stage-art-secondary: oklch(0.745085 0.125892 298.647);608 --stage-art-secondary: oklch(0.745085 0.125892 298.647);
489 --stage-art-tertiary: oklch(0.73066 0.167815 340.964);609 --stage-art-tertiary: oklch(0.73066 0.167815 340.964);
490 --stage-art-line: oklch(0.960278 0.024064 306.969);610 --stage-art-line: oklch(0.960278 0.024064 306.969);
491 }
492611
493 html.dark[data-theme-id="iris"] {612 @variant dark {
494 --stage-art-top: oklch(0.57297 0.145973 295.185);613 --stage-art-top: oklch(0.57297 0.145973 295.185);
495 --stage-art-mid: oklch(0.419499 0.13752 292.131);614 --stage-art-mid: oklch(0.419499 0.13752 292.131);
496 --stage-art-bottom: oklch(0.274235 0.095798 286.608);615 --stage-art-bottom: oklch(0.274235 0.095798 286.608);
497 --stage-art-highlight: oklch(0.916698 0.047206 300.224);616 --stage-art-highlight: oklch(0.916698 0.047206 300.224);
498 --stage-art-secondary: oklch(0.670994 0.13095 296.689);617 --stage-art-secondary: oklch(0.670994 0.13095 296.689);
499 --stage-art-tertiary: oklch(0.679357 0.165376 340.439);618 --stage-art-tertiary: oklch(0.679357 0.165376 340.439);
500 --stage-art-line: oklch(0.940582 0.032921 299.076);619 --stage-art-line: oklch(0.940582 0.032921 299.076);
620 }
501 }621 }
502622
503 :is(html[data-theme-id="t3-chat"], html[data-theme-id="ocean"], html[data-theme-id="iris"]) {623 :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...@@ -538,65 +658,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
538 --stage-night-sparkle: var(--stage-night-line);658 --stage-night-sparkle: var(--stage-night-line);
539 }659 }
540660
541 .workspace-topbar {
542 display: flex;
543 height: var(--workspace-topbar-height);
544 min-height: var(--workspace-topbar-height);
545 flex-shrink: 0;
546 align-items: center;
547 }
548
549 /* Fade rows themselves as they pass beneath the top chrome. A mask remains
550 visible even when the header and timeline share the same background. */
551 .chat-timeline-scroll-fade,
552 .settings-page-scroll-fade,
553 .pull-requests-scroll-fade {
554 --topbar-scroll-fade-height: 2.5rem;
555 -webkit-mask-image:
556 linear-gradient(
557 to bottom,
558 transparent 0%,
559 rgb(0 0 0 / 10%) 10%,
560 rgb(0 0 0 / 30%) 24%,
561 rgb(0 0 0 / 58%) 42%,
562 rgb(0 0 0 / 82%) 62%,
563 rgb(0 0 0 / 96%) 82%,
564 black 100%
565 ),
566 linear-gradient(black, black), linear-gradient(black, black);
567 -webkit-mask-position: top, bottom, right;
568 -webkit-mask-repeat: no-repeat;
569 -webkit-mask-size:
570 100% var(--topbar-scroll-fade-height),
571 100% calc(100% - var(--topbar-scroll-fade-height)),
572 var(--app-scrollbar-width) 100%;
573 mask-image:
574 linear-gradient(
575 to bottom,
576 transparent 0%,
577 rgb(0 0 0 / 10%) 10%,
578 rgb(0 0 0 / 30%) 24%,
579 rgb(0 0 0 / 58%) 42%,
580 rgb(0 0 0 / 82%) 62%,
581 rgb(0 0 0 / 96%) 82%,
582 black 100%
583 ),
584 linear-gradient(black, black), linear-gradient(black, black);
585 mask-position: top, bottom, right;
586 mask-repeat: no-repeat;
587 mask-size:
588 100% var(--topbar-scroll-fade-height),
589 100% calc(100% - var(--topbar-scroll-fade-height)),
590 var(--app-scrollbar-width) 100%;
591 }
592
593 /* The pull request list sits directly under its topbar, so the tall band the chat and
594 settings pages fade under would read as empty padding here. A shorter band keeps the
595 fade while letting the controls start near the chrome. */
596 .pull-requests-scroll-fade {
597 --topbar-scroll-fade-height: 1.5rem;
598 }
599
600 @keyframes settings-search-target-pulse {661 @keyframes settings-search-target-pulse {
601 0%,662 0%,
602 100% {663 100% {
...@@ -607,56 +668,29 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -607,56 +668,29 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
607 }668 }
608 }669 }
609670
610 .settings-page-scroll-fade div.settings-search-target-pulse,671 [data-settings-page-scroll] div.settings-search-target-pulse,
611 .settings-page-scroll-fade section.settings-search-target-pulse > div:first-child {672 [data-settings-page-scroll] section.settings-search-target-pulse > div:first-child {
612 animation: settings-search-target-pulse 650ms ease-in-out 2;673 animation: settings-search-target-pulse 650ms ease-in-out 2;
613 border-radius: 0.75rem;674 border-radius: 0.75rem;
614 }675 }
615676
616 /* The pulse is the destination indicator; without it (reduced motion), the677 /* The pulse is the destination indicator; without it (reduced motion), the
617 focus outline takes over, so exactly one indicator shows at a time. */678 focus outline takes over, so exactly one indicator shows at a time. */
618 .settings-page-scroll-fade .settings-search-target-pulse:focus {679 [data-settings-page-scroll] .settings-search-target-pulse:focus {
619 outline: none;680 outline: none;
620 }681 }
621682
622 .workspace-titlebar-controls {
623 position: absolute;
624 top: var(--workspace-controls-top);
625 right: var(--workspace-controls-right);
626 display: flex;
627 height: var(--workspace-topbar-height);
628 align-items: center;
629 -webkit-app-region: no-drag;
630 }
631
632 .surface-subheader {
633 @apply flex h-10 min-h-10 shrink-0 items-center border-b border-border/60 bg-background;
634 }
635
636 [data-preview-panel-mode="inline"] [data-right-panel-surface-content] [data-surface-subheader] {
637 height: calc(var(--spacing) * 7);
638 min-height: calc(var(--spacing) * 7);
639 margin-bottom: calc(var(--spacing) * 3);
640 border-bottom-color: transparent;
641 }
642
643 .chat-composer-horizontal-inset {
644 padding-inline-start: calc(env(safe-area-inset-left) + 0.75rem);
645 padding-inline-end: calc(env(safe-area-inset-right) + 0.75rem);
646 }
647
648 .chat-composer-glass {
649 background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent);
650 -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
651 backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
652 }
653
654 .chat-composer-glass-shell {683 .chat-composer-glass-shell {
655 --chat-composer-glass-surface: var(--card);684 --chat-composer-glass-surface: var(--card);
656 --chat-composer-outline: rgb(0 0 0 / 8%);685 --chat-composer-outline: rgb(0 0 0 / 8%);
657686
658 position: relative;
659 isolation: isolate;687 isolation: isolate;
688
689 @variant dark {
690 --chat-composer-glass-surface: color-mix(in srgb, var(--background) 96%, var(--color-white));
691 --chat-composer-outline: color-mix(in srgb, var(--color-white) 5%, transparent);
692 --chat-composer-highlight: rgb(255 255 255 / 3%);
693 }
660 }694 }
661695
662 .chat-composer-glass-shell::before {696 .chat-composer-glass-shell::before {
...@@ -716,8 +750,15 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -716,8 +750,15 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
716 }750 }
717751
718 .chat-composer-glass-host {752 .chat-composer-glass-host {
719 position: relative;
720 box-shadow: 0 12px 28px -18px rgb(0 0 0 / 40%);753 box-shadow: 0 12px 28px -18px rgb(0 0 0 / 40%);
754
755 @variant dark {
756 box-shadow: none;
757
758 &::after {
759 box-shadow: inset 0 1px var(--chat-composer-highlight);
760 }
761 }
721 }762 }
722763
723 .chat-composer-glass-host::after {764 .chat-composer-glass-host::after {
...@@ -747,6 +788,21 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -747,6 +788,21 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
747 .chat-composer-context-strip {788 .chat-composer-context-strip {
748 position: relative;789 position: relative;
749 isolation: isolate;790 isolation: isolate;
791
792 @variant dark {
793 &::before {
794 border-color: rgb(255 255 255 / 7%);
795 background:
796 linear-gradient(
797 to bottom,
798 transparent 0 1rem,
799 rgb(0 0 0 / 18%) 1rem,
800 transparent calc(1rem + 10px)
801 ),
802 rgb(255 255 255 / 2%);
803 box-shadow: 0 14px 32px -18px rgb(0 0 0 / 75%);
804 }
805 }
750 }806 }
751807
752 .chat-composer-context-strip::before {808 .chat-composer-context-strip::before {
...@@ -762,33 +818,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -762,33 +818,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
762 content: "";818 content: "";
763 }819 }
764820
765 .dark .chat-composer-glass-shell {
766 --chat-composer-glass-surface: color-mix(in srgb, var(--background) 96%, var(--color-white));
767 --chat-composer-outline: color-mix(in srgb, var(--color-white) 5%, transparent);
768 --chat-composer-highlight: rgb(255 255 255 / 3%);
769 }
770
771 .dark .chat-composer-glass-host {
772 box-shadow: none;
773 }
774
775 .dark .chat-composer-glass-host::after {
776 box-shadow: inset 0 1px var(--chat-composer-highlight);
777 }
778
779 .dark .chat-composer-context-strip::before {
780 border-color: rgb(255 255 255 / 7%);
781 background:
782 linear-gradient(
783 to bottom,
784 transparent 0 1rem,
785 rgb(0 0 0 / 18%) 1rem,
786 transparent calc(1rem + 10px)
787 ),
788 rgb(255 255 255 / 2%);
789 box-shadow: 0 14px 32px -18px rgb(0 0 0 / 75%);
790 }
791
792 @supports not (clip-path: shape(from 0 0, line to 1px 1px)) {821 @supports not (clip-path: shape(from 0 0, line to 1px 1px)) {
793 .chat-composer-glass-shell-with-context::before {822 .chat-composer-glass-shell-with-context::before {
794 inset-block-end: var(--chat-composer-context-extension);823 inset-block-end: var(--chat-composer-context-extension);
...@@ -806,106 +835,23 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -806,106 +835,23 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
806 backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));835 backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
807 }836 }
808837
809 .dark .chat-composer-context-strip::before {838 .chat-composer-context-strip {
810 background:839 @variant dark {
811 linear-gradient(840 &::before {
812 to bottom,841 background:
813 transparent 0 1rem,842 linear-gradient(
814 rgb(0 0 0 / 18%) 1rem,843 to bottom,
815 transparent calc(1rem + 10px)844 transparent 0 1rem,
816 ),845 rgb(0 0 0 / 18%) 1rem,
817 linear-gradient(rgb(255 255 255 / 2%), rgb(255 255 255 / 2%)),846 transparent calc(1rem + 10px)
818 color-mix(in srgb, var(--chat-composer-glass-surface) var(--glass-opacity), transparent);847 ),
848 linear-gradient(rgb(255 255 255 / 2%), rgb(255 255 255 / 2%)),
849 color-mix(in srgb, var(--chat-composer-glass-surface) var(--glass-opacity), transparent);
850 }
851 }
819 }852 }
820 }853 }
821854
822 .alert-glass {
823 --alert-glass-tint: transparent;
824
825 background:
826 linear-gradient(
827 color-mix(in srgb, var(--alert-glass-tint) 4%, transparent),
828 color-mix(in srgb, var(--alert-glass-tint) 4%, transparent)
829 ),
830 color-mix(in srgb, var(--background) var(--glass-opacity), transparent) !important;
831 -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
832 backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
833 }
834
835 .alert-glass[data-variant="error"] {
836 --alert-glass-tint: var(--destructive);
837 }
838
839 .alert-glass[data-variant="info"] {
840 --alert-glass-tint: var(--info);
841 }
842
843 .alert-glass[data-variant="success"] {
844 --alert-glass-tint: var(--success);
845 }
846
847 .alert-glass[data-variant="warning"] {
848 --alert-glass-tint: var(--warning);
849 }
850
851 .dialog-glass {
852 background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent);
853 -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
854 backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
855 }
856
857 .dialog-backdrop {
858 background: color-mix(in srgb, var(--background) 60%, transparent);
859 -webkit-backdrop-filter: blur(4px);
860 backdrop-filter: blur(4px);
861 }
862
863 .dropdown-glass {
864 /*
865 * Elevated glass needs a denser tint than broad ambient surfaces. Nesting
866 * the user-controlled mix inside an 18% popover tint preserves the full
867 * opacity setting range (40% -> 51%, 80% -> 84%, 100% -> 100%) while
868 * keeping high-contrast page content from blooming through menus.
869 */
870 background: color-mix(
871 in srgb,
872 var(--popover) 18%,
873 color-mix(in srgb, var(--popover) var(--glass-opacity), transparent)
874 );
875 -webkit-backdrop-filter: blur(var(--glass-blur));
876 backdrop-filter: blur(var(--glass-blur));
877 border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
878 box-shadow: 0 16px 40px -18px rgb(0 0 0 / 55%);
879 }
880
881 .dialog-glass {
882 border-color: color-mix(in srgb, var(--foreground) 10%, transparent);
883 box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%);
884 }
885
886 .dark .dropdown-glass {
887 box-shadow: 0 18px 44px -18px rgb(0 0 0 / 80%);
888 }
889
890 .dark .model-picker-surface.model-picker-surface {
891 background: color-mix(
892 in srgb,
893 var(--popover) 18%,
894 color-mix(in srgb, var(--popover) var(--glass-opacity), transparent)
895 );
896 }
897
898 .dark .dialog-glass {
899 border-color: color-mix(in srgb, var(--color-white) 8%, transparent);
900 box-shadow:
901 inset 0 1px rgb(255 255 255 / 4%),
902 0 24px 72px -20px rgb(0 0 0 / 90%);
903 }
904
905 .dark .dialog-backdrop {
906 background: color-mix(in srgb, var(--background) 64%, transparent);
907 }
908
909 .settings-slider {855 .settings-slider {
910 --settings-slider-progress: 0%;856 --settings-slider-progress: 0%;
911 --settings-slider-fill-offset: 0.5rem;857 --settings-slider-fill-offset: 0.5rem;
...@@ -1021,32 +967,10 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -1021,32 +967,10 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
1021 }967 }
1022 }968 }
1023969
1024 @media (min-width: 40rem) {
1025 .chat-timeline-scroll-fade,
1026 .settings-page-scroll-fade {
1027 --topbar-scroll-fade-height: 3rem;
1028 }
1029
1030 .chat-composer-horizontal-inset {
1031 padding-inline-start: calc(env(safe-area-inset-left) + 1.25rem);
1032 padding-inline-end: calc(env(safe-area-inset-right) + 1.25rem);
1033 }
1034 }
1035
1036 @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {970 @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
1037 .chat-composer-glass,
1038 .alert-glass {
1039 background: var(--background) !important;
1040 }
1041
1042 .chat-composer-glass-shell::before {971 .chat-composer-glass-shell::before {
1043 background: var(--chat-composer-glass-surface);972 background: var(--chat-composer-glass-surface);
1044 }973 }
1045
1046 .dialog-glass,
1047 .dropdown-glass {
1048 background: var(--popover) !important;
1049 }
1050 }974 }
1051}975}
1052976
...@@ -1152,15 +1076,12 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -1152,15 +1076,12 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
1152 --terminal-foreground: var(--foreground);1076 --terminal-foreground: var(--foreground);
1153 --terminal-cursor: rgb(38 56 78);1077 --terminal-cursor: rgb(38 56 78);
1154 --terminal-selection-background: rgb(37 63 99 / 20%);1078 --terminal-selection-background: rgb(37 63 99 / 20%);
1155 --terminal-scrollbar: rgb(0 0 0 / 15%);
1156 --terminal-scrollbar-hover: rgb(0 0 0 / 25%);
11571079
1158 @variant dark {1080 @variant dark {
1159 color-scheme: dark;1081 color-scheme: dark;
1160 /* Keep the workspace in the same neutral-black family as sidebar v2.1082 /* Keep the workspace in the same neutral-black family as sidebar v2.
1161 Surfaces lift from this base instead of starting from a milky gray. */1083 Surfaces lift from this base instead of starting from a milky gray. */
1162 --background: var(--color-neutral-950);1084 --background: var(--color-neutral-950);
1163 --app-chrome-background: var(--background);
1164 --surface-raised: var(--secondary);1085 --surface-raised: var(--secondary);
1165 --foreground: var(--color-neutral-100);1086 --foreground: var(--color-neutral-100);
1166 --card: color-mix(in srgb, var(--background) 97%, var(--color-white));1087 --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...@@ -1168,54 +1089,31 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
1168 --popover: color-mix(in srgb, var(--background) 94%, var(--color-white));1089 --popover: color-mix(in srgb, var(--background) 94%, var(--color-white));
1169 --popover-foreground: var(--color-neutral-100);1090 --popover-foreground: var(--color-neutral-100);
1170 --primary: oklch(0.571 0.21 264);1091 --primary: oklch(0.571 0.21 264);
1171 --primary-foreground: var(--color-white);
1172 --secondary: --alpha(var(--color-white) / 4%);1092 --secondary: --alpha(var(--color-white) / 4%);
1173 --secondary-foreground: var(--color-neutral-100);1093 --secondary-foreground: var(--color-neutral-100);
1174 --muted: --alpha(var(--color-white) / 4%);1094 --muted: --alpha(var(--color-white) / 4%);
1175 --muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-white));1095 --muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-white));
1176 --placeholder: var(--muted-foreground);
1177 --secondary-label: var(--muted-foreground);
1178 --icon-muted: var(--muted-foreground);
1179 --message-surface: var(--accent);
1180 --message-foreground: var(--foreground);
1181 --message-action: var(--primary);
1182 --message-action-foreground: var(--primary-foreground);
1183 --message-action-hover: color-mix(in srgb, var(--primary) 90%, var(--background));
1184 --accent: --alpha(var(--color-white) / 4%);1096 --accent: --alpha(var(--color-white) / 4%);
1185 --accent-foreground: var(--color-neutral-100);1097 --accent-foreground: var(--color-neutral-100);
1186 --error: color-mix(in srgb, var(--color-red-500) 90%, var(--color-white));1098 --error: color-mix(in srgb, var(--color-red-500) 90%, var(--color-white));
1187 --error-foreground: var(--color-red-400);1099 --error-foreground: var(--color-red-400);
1188 --error-surface: color-mix(in srgb, var(--error) 16%, transparent);1100 --error-surface: color-mix(in srgb, var(--error) 16%, transparent);
1189 --destructive: var(--error);
1190 --border: --alpha(var(--color-white) / 6%);1101 --border: --alpha(var(--color-white) / 6%);
1191 --input: --alpha(var(--color-white) / 8%);1102 --input: --alpha(var(--color-white) / 8%);
1192 --ring: var(--primary);
1193 --destructive-foreground: var(--error-foreground);
1194 --info: var(--color-blue-500);
1195 --info-foreground: var(--color-blue-400);1103 --info-foreground: var(--color-blue-400);
1196 --success: var(--color-emerald-500);
1197 --success-foreground: var(--color-emerald-400);1104 --success-foreground: var(--color-emerald-400);
1198 --warning: var(--color-amber-500);
1199 --warning-foreground: var(--color-amber-400);1105 --warning-foreground: var(--color-amber-400);
1200 --warning-surface: color-mix(in srgb, var(--warning) 16%, transparent);1106 --warning-surface: color-mix(in srgb, var(--warning) 16%, transparent);
1201 --update: var(--primary);
1202 --update-foreground: var(--color-blue-400);1107 --update-foreground: var(--color-blue-400);
1203 --update-surface: color-mix(in srgb, var(--update) 18%, transparent);1108 --update-surface: color-mix(in srgb, var(--update) 18%, transparent);
1204 --sidebar: var(--card);1109 --sidebar: var(--card);
1205 --sidebar-foreground: var(--foreground);
1206 --sidebar-muted-foreground: var(--muted-foreground);
1207 --sidebar-control-surface: var(--muted);1110 --sidebar-control-surface: var(--muted);
1208 --sidebar-row-hover: var(--accent);1111 --sidebar-row-hover: var(--accent);
1209 --sidebar-row-active: var(--accent);1112 --sidebar-row-active: var(--accent);
1210 --sidebar-row-selected: var(--muted);1113 --sidebar-row-selected: var(--muted);
1211 --sidebar-border: var(--border);
1212 --sidebar-stage-fade: var(--card);1114 --sidebar-stage-fade: var(--card);
1213 --terminal-background: var(--background);
1214 --terminal-foreground: var(--foreground);
1215 --terminal-cursor: rgb(180 203 255);1115 --terminal-cursor: rgb(180 203 255);
1216 --terminal-selection-background: rgb(180 203 255 / 25%);1116 --terminal-selection-background: rgb(180 203 255 / 25%);
1217 --terminal-scrollbar: rgb(255 255 255 / 10%);
1218 --terminal-scrollbar-hover: rgb(255 255 255 / 18%);
1219 }1117 }
1220}1118}
12211119
...@@ -1242,32 +1140,28 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -1242,32 +1140,28 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
1242 --sidebar-row-selected: var(--color-white);1140 --sidebar-row-selected: var(--color-white);
1243 --sidebar-border: var(--color-zinc-200);1141 --sidebar-border: var(--color-zinc-200);
1244 --sidebar-stage-fade: var(--sidebar);1142 --sidebar-stage-fade: var(--sidebar);
1245 background-color: var(--sidebar);1143
1246}1144 @variant dark {
12471145 --background: #000;
1248.dark [data-app-sidebar] {1146 --foreground: #f1f3f7;
1249 --background: #000;1147 --card: #000;
1250 --foreground: #f1f3f7;1148 --card-foreground: var(--foreground);
1251 --card: #000;1149 --accent: #191a1d;
1252 --card-foreground: var(--foreground);1150 --accent-foreground: #f7f9ff;
1253 --accent: #191a1d;1151 --muted: #0a0a0a;
1254 --accent-foreground: #f7f9ff;1152 --muted-foreground: #a3a3a3;
1255 --muted: #0a0a0a;1153 --border: rgb(255 255 255 / 8%);
1256 --muted-foreground: #a3a3a3;1154 --input: rgb(255 255 255 / 18%);
1257 --border: rgb(255 255 255 / 8%);1155 --sidebar: var(--card);
1258 --input: rgb(255 255 255 / 18%);1156 --sidebar-foreground: var(--foreground);
1259 --sidebar: var(--card);1157 --sidebar-muted-foreground: var(--muted-foreground);
1260 --sidebar-foreground: var(--foreground);1158 --sidebar-control-surface: var(--muted);
1261 --sidebar-muted-foreground: var(--muted-foreground);1159 --sidebar-row-hover: color-mix(in srgb, var(--foreground) 8%, transparent);
1262 --sidebar-control-surface: var(--muted);1160 --sidebar-row-active: color-mix(in srgb, var(--foreground) 11%, transparent);
1263 --sidebar-row-hover: color-mix(in srgb, var(--foreground) 8%, transparent);1161 --sidebar-row-selected: color-mix(in srgb, var(--foreground) 7%, transparent);
1264 --sidebar-row-active: color-mix(in srgb, var(--foreground) 11%, transparent);1162 --sidebar-border: var(--border);
1265 --sidebar-row-selected: color-mix(in srgb, var(--foreground) 7%, transparent);1163 --sidebar-stage-fade: var(--card);
1266 --sidebar-border: var(--border);1164 }
1267 /* The stage-channel header art must ramp to THIS panel's surface, not the
1268 global chrome background, or the fade shows a seam (same rule as the
1269 light palette above). */
1270 --sidebar-stage-fade: var(--card);
1271}1165}
12721166
1273/* Theme files are expressed in app color roles and mapped to the existing1167/* 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...@@ -1275,8 +1169,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
1275 compatibility overrides so both navigation implementations receive the1169 compatibility overrides so both navigation implementations receive the
1276 same palette. Success, info, provider, and channel identity colors remain1170 same palette. Success, info, provider, and channel identity colors remain
1277 independent; error, warning, and update roles are themeable below. */1171 independent; error, warning, and update roles are themeable below. */
1278html[data-theme-id],1172html[data-theme-id] {
1279html.dark[data-theme-id] {
1280 --background: var(--app-theme-canvas);1173 --background: var(--app-theme-canvas);
1281 --app-chrome-background: var(--app-theme-chrome);1174 --app-chrome-background: var(--app-theme-chrome);
1282 --toolbar-background: var(--app-theme-toolbar);1175 --toolbar-background: var(--app-theme-toolbar);
...@@ -1340,8 +1233,6 @@ html.dark[data-theme-id] {...@@ -1340,8 +1233,6 @@ html.dark[data-theme-id] {
1340 --terminal-foreground: var(--app-theme-terminal-foreground);1233 --terminal-foreground: var(--app-theme-terminal-foreground);
1341 --terminal-cursor: var(--app-theme-terminal-cursor);1234 --terminal-cursor: var(--app-theme-terminal-cursor);
1342 --terminal-selection-background: var(--app-theme-terminal-selection-background);1235 --terminal-selection-background: var(--app-theme-terminal-selection-background);
1343 --terminal-scrollbar: var(--app-theme-terminal-scrollbar);
1344 --terminal-scrollbar-hover: var(--app-theme-terminal-scrollbar-hover);
1345}1236}
13461237
1347/* T3 Chat's composer is a translucent lift over --chat-background. Route its1238/* T3 Chat's composer is a translucent lift over --chat-background. Route its
...@@ -1349,35 +1240,21 @@ html.dark[data-theme-id] {...@@ -1349,35 +1240,21 @@ html.dark[data-theme-id] {
1349 another tint from the canvas, which made the dark composer too red. */1240 another tint from the canvas, which made the dark composer too red. */
1350html[data-theme-id] .chat-composer-glass-shell {1241html[data-theme-id] .chat-composer-glass-shell {
1351 --chat-composer-glass-surface: var(--app-theme-surface-raised);1242 --chat-composer-glass-surface: var(--app-theme-surface-raised);
1352}
1353
1354html[data-theme-id]:not(.dark) .chat-composer-glass-shell {
1355 --chat-composer-outline: var(--app-theme-toolbar-border);1243 --chat-composer-outline: var(--app-theme-toolbar-border);
1356}
13571244
1358html.dark[data-theme-id] .chat-composer-glass-shell {1245 @variant dark {
1359 --chat-composer-outline: color-mix(in srgb, var(--app-theme-input) 30%, var(--background));1246 --chat-composer-outline: color-mix(in srgb, var(--app-theme-input) 30%, var(--background));
1360 --chat-composer-highlight: color-mix(in srgb, var(--app-theme-input) 12%, transparent);1247 --chat-composer-highlight: color-mix(in srgb, var(--app-theme-input) 12%, transparent);
1248 }
1361}1249}
13621250
1363html.dark[data-theme-id="t3-chat"] .chat-composer-glass-shell {1251html[data-theme-id="t3-chat"] .chat-composer-glass-shell {
1364 /* T3 Chat's visible composer edge is a dark plum, not the stock translucent1252 /* T3 Chat's visible composer edge is a dark plum, not the stock translucent
1365 white outline. Its highlight is derived from --chat-input-gradient. */1253 white outline. Its highlight is derived from --chat-input-gradient. */
1366 --chat-composer-outline: #241e28;1254 @variant dark {
1367 --chat-composer-highlight: color-mix(in srgb, #432d48 12%, transparent);1255 --chat-composer-outline: #241e28;
1368}1256 --chat-composer-highlight: color-mix(in srgb, #432d48 12%, transparent);
13691257 }
1370html[data-theme-id]:not(.dark) {
1371 color-scheme: light;
1372}
1373
1374html.dark[data-theme-id] {
1375 color-scheme: dark;
1376}
1377
1378html[data-theme-id] body {
1379 background-color: var(--app-chrome-background);
1380 color: var(--foreground);
1381}1258}
13821259
1383/* Theme-token dependency probes are restored synchronously, before paint. Keep1260/* Theme-token dependency probes are restored synchronously, before paint. Keep
...@@ -1477,8 +1354,8 @@ html[data-theme-id] [data-chat-header] [data-toolbar-control] {...@@ -1477,8 +1354,8 @@ html[data-theme-id] [data-chat-header] [data-toolbar-control] {
1477 toggle's when the trigger renders the toggle, so match both. */1354 toggle's when the trigger renders the toggle, so match both. */
1478html[data-theme-id] [data-panel-layout-controls] [data-slot="toggle"],1355html[data-theme-id] [data-panel-layout-controls] [data-slot="toggle"],
1479html[data-theme-id] [data-panel-layout-controls] [data-slot="tooltip-trigger"],1356html[data-theme-id] [data-panel-layout-controls] [data-slot="tooltip-trigger"],
1480html[data-theme-id] .workspace-titlebar-controls [data-slot="toggle"],1357html[data-theme-id] [data-workspace-titlebar-controls] [data-slot="toggle"],
1481html[data-theme-id] .workspace-titlebar-controls [data-slot="tooltip-trigger"] {1358html[data-theme-id] [data-workspace-titlebar-controls] [data-slot="tooltip-trigger"] {
1482 --control-icon-color: var(--toolbar-foreground);1359 --control-icon-color: var(--toolbar-foreground);
1483 color: var(--toolbar-foreground);1360 color: var(--toolbar-foreground);
1484}1361}
...@@ -1522,19 +1399,23 @@ html[data-theme-id] .chat-markdown .chat-markdown-chrome-action {...@@ -1522,19 +1399,23 @@ html[data-theme-id] .chat-markdown .chat-markdown-chrome-action {
1522/* T3 Chat renders inline code and compact chat artifacts with its translucent1399/* T3 Chat renders inline code and compact chat artifacts with its translucent
1523 secondary surface flattened over the light chat canvas. The raw muted and1400 secondary surface flattened over the light chat canvas. The raw muted and
1524 secondary tokens are substantially darker than those visible pixels. */1401 secondary tokens are substantially darker than those visible pixels. */
1525html[data-theme-id="t3-chat"]:not(.dark) .chat-markdown :not(pre) > code,1402html[data-theme-id="t3-chat"] {
1526html[data-theme-id="t3-chat"]:not(.dark) [data-changed-files-state],1403 @variant light {
1527html[data-theme-id="t3-chat"]:not(.dark) [data-changed-files-header] {1404 & .chat-markdown :not(pre) > code,
1528 background-color: var(--message-surface);1405 & [data-changed-files-state],
1529}1406 & [data-changed-files-header] {
1407 background-color: var(--message-surface);
1408 }
15301409
1531html[data-theme-id="t3-chat"]:not(.dark) .chat-markdown :not(pre) > code,1410 & .chat-markdown :not(pre) > code,
1532html[data-theme-id="t3-chat"]:not(.dark) [data-changed-files-state] {1411 & [data-changed-files-state] {
1533 border-color: transparent;1412 border-color: transparent;
1534}1413 }
15351414
1536html[data-theme-id="t3-chat"]:not(.dark) .chat-markdown :not(pre) > code {1415 & .chat-markdown :not(pre) > code {
1537 color: var(--message-foreground);1416 color: var(--message-foreground);
1417 }
1418 }
1538}1419}
15391420
1540html[data-theme-id] .chat-markdown .chat-markdown-chrome-action:hover,1421html[data-theme-id] .chat-markdown .chat-markdown-chrome-action:hover,
...@@ -1562,40 +1443,19 @@ html[data-theme-id] [data-app-sidebar] {...@@ -1562,40 +1443,19 @@ html[data-theme-id] [data-app-sidebar] {
1562 --sidebar-row-selected: var(--app-theme-sidebar-row-selected);1443 --sidebar-row-selected: var(--app-theme-sidebar-row-selected);
1563 --sidebar-border: var(--app-theme-sidebar-border);1444 --sidebar-border: var(--app-theme-sidebar-border);
1564 --sidebar-stage-fade: var(--app-theme-sidebar);1445 --sidebar-stage-fade: var(--app-theme-sidebar);
1565 background-color: var(--sidebar);
1566}
1567
1568/* Keep the navigation edge as quiet as the standard palettes. Theme files may
1569 still use sidebarBorder for controls and internal separators, but the outer
1570 divider should not become more prominent just because a palette is vivid. */
1571html[data-theme-id] [data-app-sidebar] {
1572 border-color: color-mix(in srgb, var(--sidebar-foreground) 10%, transparent);1446 border-color: color-mix(in srgb, var(--sidebar-foreground) 10%, transparent);
1573}
15741447
1575html.dark[data-theme-id] [data-app-sidebar] {1448 @variant dark {
1576 border-color: color-mix(in srgb, var(--sidebar-foreground) 8%, transparent);1449 border-color: color-mix(in srgb, var(--sidebar-foreground) 8%, transparent);
1450 }
1577}1451}
15781452
1579/* T3 Chat's panel divider is deliberately pink, and its resize affordance1453/* T3 Chat's panel divider is deliberately pink, and its resize affordance
1580 keeps that color while hovered. Do not neutralize this branded edge. */1454 keeps that color while hovered. Do not neutralize this branded edge. */
1581html.dark[data-theme-id="t3-chat"] [data-app-sidebar] {1455html[data-theme-id="t3-chat"] [data-app-sidebar] {
1582 border-color: var(--sidebar-border);1456 @variant dark {
1583}1457 border-color: var(--sidebar-border);
15841458 }
1585.theme-json-key {
1586 color: var(--app-theme-accent, var(--color-blue-600));
1587}
1588
1589.theme-json-string {
1590 color: var(--app-theme-message-action, var(--color-emerald-600));
1591}
1592
1593.theme-json-number {
1594 color: var(--app-theme-secondary-foreground, var(--color-amber-600));
1595}
1596
1597.theme-json-constant {
1598 color: var(--app-theme-accent-surface-foreground, var(--color-violet-600));
1599}1459}
16001460
1601body {1461body {
...@@ -1715,125 +1575,7 @@ code {...@@ -1715,125 +1575,7 @@ code {
1715 background: var(--app-scrollbar-thumb-hover);1575 background: var(--app-scrollbar-thumb-hover);
1716}1576}
17171577
1718/* Settings -> Appearance can point the composer at its own face (for example a
1719 mono font); default follows the sans stack. Applied on the surface wrapper so
1720 the editor and its placeholder inherit together. */
1721.composer-editor-surface {
1722 font-family: var(--font-composer, var(--font-sans));
1723 font-size: var(--font-size-prompt, 0.875rem);
1724}
1725
1726/* Touch browsers zoom the page when a focused field is under 16px, so keep
1727 the floor there regardless of the preference. Gated on a coarse pointer:
1728 the zoom quirk does not exist on desktop, where a narrow window must not
1729 silently override a smaller chosen prompt size. */
1730@media (max-width: 39.999rem) and (pointer: coarse) {
1731 .composer-editor-surface {
1732 font-size: max(var(--font-size-prompt, 1rem), 16px);
1733 }
1734}
1735
1736.t3-ghostty-canvas {
1737 cursor: text;
1738}
1739
1740.t3-ghostty-scrollbar {
1741 position: absolute;
1742 z-index: 1;
1743 top: 4px;
1744 right: 1px;
1745 bottom: 4px;
1746 width: var(--app-scrollbar-width);
1747 cursor: default;
1748 touch-action: none;
1749}
1750
1751.t3-ghostty-scrollbar-thumb {
1752 position: absolute;
1753 top: 0;
1754 right: 1px;
1755 left: 1px;
1756 border-radius: 3px;
1757 background: var(--app-scrollbar-thumb);
1758 transition: background-color 120ms ease-out;
1759}
1760
1761.t3-ghostty-scrollbar:hover .t3-ghostty-scrollbar-thumb,
1762.t3-ghostty-scrollbar:focus-visible .t3-ghostty-scrollbar-thumb {
1763 background: var(--app-scrollbar-thumb-hover);
1764}
1765
1766.model-picker-list::-webkit-scrollbar-track {
1767 margin-block: 0.5rem;
1768}
1769
1770.model-picker-list-scroll-fade-top,
1771.model-picker-list-scroll-fade-bottom {
1772 -webkit-mask-image: var(--model-picker-list-scroll-mask), linear-gradient(black, black);
1773 -webkit-mask-position: left, right;
1774 -webkit-mask-repeat: no-repeat;
1775 -webkit-mask-size:
1776 calc(100% - var(--app-scrollbar-width)) 100%,
1777 var(--app-scrollbar-width) 100%;
1778 mask-image: var(--model-picker-list-scroll-mask), linear-gradient(black, black);
1779 mask-position: left, right;
1780 mask-repeat: no-repeat;
1781 mask-size:
1782 calc(100% - var(--app-scrollbar-width)) 100%,
1783 var(--app-scrollbar-width) 100%;
1784}
1785
1786.model-picker-list-scroll-fade-top {
1787 --model-picker-list-scroll-mask: linear-gradient(to bottom, transparent, black var(--fade-size));
1788}
1789
1790.model-picker-list-scroll-fade-bottom {
1791 --model-picker-list-scroll-mask: linear-gradient(
1792 to bottom,
1793 black calc(100% - var(--fade-size)),
1794 transparent
1795 );
1796}
1797
1798.model-picker-list-scroll-fade-top.model-picker-list-scroll-fade-bottom {
1799 --model-picker-list-scroll-mask: linear-gradient(
1800 to bottom,
1801 transparent,
1802 black var(--fade-size),
1803 black calc(100% - var(--fade-size)),
1804 transparent
1805 );
1806}
1807
1808.turn-chip-strip {
1809 scrollbar-width: none;
1810 -ms-overflow-style: none;
1811 overscroll-behavior-x: contain;
1812}
1813
1814.turn-chip-strip::-webkit-scrollbar {
1815 display: none;
1816}
1817
1818/* Reasoning select -- clickable label surface */
1819label:has(> select#reasoning-effort) {
1820 position: relative;
1821}
1822label:has(> select#reasoning-effort) select {
1823 position: absolute;
1824 inset: 0;
1825 opacity: 0;
1826 cursor: pointer;
1827 width: 100%;
1828 height: 100%;
1829}
1830
1831/* Chat markdown rendering */1578/* Chat markdown rendering */
1832.chat-markdown {
1833 min-width: 0;
1834 overflow-wrap: anywhere;
1835 word-break: break-word;
1836}
18371579
1838.chat-markdown > :first-child {1580.chat-markdown > :first-child {
1839 margin-top: 0;1581 margin-top: 0;
...@@ -1945,18 +1687,6 @@ label:has(> select#reasoning-effort) select {...@@ -1945,18 +1687,6 @@ label:has(> select#reasoning-effort) select {
1945 background-size: 4px 2px;1687 background-size: 4px 2px;
1946}1688}
19471689
1948.chat-markdown .chat-markdown-link-favicon {
1949 @apply inline-flex;
1950 width: 14px;
1951 height: 14px;
1952 margin-inline: 0.25em 0.2em;
1953 vertical-align: -0.125em;
1954}
1955
1956.chat-markdown .chat-markdown-link-leading {
1957 white-space: nowrap;
1958}
1959
1960.chat-markdown blockquote {1690.chat-markdown blockquote {
1961 border-left: 2px solid var(--border);1691 border-left: 2px solid var(--border);
1962 padding-left: 0.8rem;1692 padding-left: 0.8rem;
...@@ -2001,11 +1731,7 @@ label:has(> select#reasoning-effort) select {...@@ -2001,11 +1731,7 @@ label:has(> select#reasoning-effort) select {
2001 font-size: 0.75rem;1731 font-size: 0.75rem;
2002}1732}
20031733
2004.chat-markdown a.chat-markdown-file-link {1734.chat-markdown a.chat-markdown-file-link,
2005 color: var(--foreground);
2006 text-decoration: none;
2007}
2008
2009.chat-markdown a.chat-markdown-file-link:hover {1735.chat-markdown a.chat-markdown-file-link:hover {
2010 color: var(--foreground);1736 color: var(--foreground);
2011 text-decoration: none;1737 text-decoration: none;
...@@ -2023,75 +1749,26 @@ label:has(> select#reasoning-effort) select {...@@ -2023,75 +1749,26 @@ label:has(> select#reasoning-effort) select {
2023 border-radius: 0.75rem;1749 border-radius: 0.75rem;
2024 background: var(--muted);1750 background: var(--muted);
2025 padding: 0.8rem 0.9rem;1751 padding: 0.8rem 0.9rem;
1752 scrollbar-width: thin;
1753 scrollbar-color: color-mix(in srgb, var(--border) 78%, transparent) transparent;
2026}1754}
20271755
2028.chat-markdown pre code {1756.chat-markdown pre code {
2029 border: none;1757 border: none;
2030 background: transparent;1758 background: transparent;
2031 padding: 0;1759 padding: 0;
2032 font-size: 0.75rem;
2033}
2034
2035.chat-markdown pre {
2036 scrollbar-width: thin;
2037 scrollbar-color: color-mix(in srgb, var(--border) 78%, transparent) transparent;
2038}1760}
20391761
2040.chat-markdown pre::-webkit-scrollbar {1762.chat-markdown pre::-webkit-scrollbar {
2041 height: 7px;1763 height: 7px;
2042}1764}
20431765
2044.chat-markdown pre::-webkit-scrollbar-track {
2045 background: transparent;
2046}
2047
2048.chat-markdown pre::-webkit-scrollbar-thumb {1766.chat-markdown pre::-webkit-scrollbar-thumb {
2049 border-radius: 999px;1767 border-radius: 999px;
2050 background: color-mix(in srgb, var(--border) 78%, transparent);1768 background: color-mix(in srgb, var(--border) 78%, transparent);
2051}1769}
20521770
2053.markdown-file-link-tooltip-scroll {1771.chat-markdown .chat-markdown-codeblock-header,
2054 scrollbar-width: thin;
2055 scrollbar-color: color-mix(in srgb, var(--border) 78%, transparent) transparent;
2056}
2057
2058.markdown-file-link-tooltip-scroll::-webkit-scrollbar {
2059 height: 6px;
2060}
2061
2062.markdown-file-link-tooltip-scroll::-webkit-scrollbar-track {
2063 background: transparent;
2064}
2065
2066.markdown-file-link-tooltip-scroll::-webkit-scrollbar-thumb {
2067 border-radius: 999px;
2068 background: color-mix(in srgb, var(--border) 78%, transparent);
2069}
2070
2071.chat-markdown .chat-markdown-codeblock {
2072 margin: 0.65rem 0;
2073 overflow: hidden;
2074 border-radius: var(--radius);
2075}
2076
2077.chat-markdown .chat-markdown-codeblock-header {
2078 display: flex;
2079 align-items: center;
2080 justify-content: space-between;
2081 gap: 0.5rem;
2082 padding: 0.375rem 0.375rem 0 0.75rem;
2083 color: color-mix(in srgb, var(--foreground) 72%, transparent);
2084}
2085
2086.chat-markdown .chat-markdown-codeblock-title {
2087 display: inline-flex;
2088 min-width: 0;
2089 align-items: center;
2090 gap: 0.4rem;
2091 font-family: var(--font-mono, ui-monospace, SFMono-Regular, monospace);
2092 font-size: 0.6875rem;
2093}
2094
2095.chat-markdown .chat-markdown-chrome-action {1772.chat-markdown .chat-markdown-chrome-action {
2096 color: color-mix(in srgb, var(--foreground) 72%, transparent);1773 color: color-mix(in srgb, var(--foreground) 72%, transparent);
2097}1774}
...@@ -2167,13 +1844,6 @@ label:has(> select#reasoning-effort) select {...@@ -2167,13 +1844,6 @@ label:has(> select#reasoning-effort) select {
2167 overflow-wrap: anywhere;1844 overflow-wrap: anywhere;
2168}1845}
21691846
2170.chat-markdown .chat-markdown-table-footer {
2171 display: flex;
2172 align-items: center;
2173 justify-content: space-between;
2174 margin-top: 0.125rem;
2175}
2176
2177/* Prompt-stash save acknowledgement: the new count fades up from just below1847/* Prompt-stash save acknowledgement: the new count fades up from just below
2178 its resting position, once, then stops. One-shot and event-driven (React1848 its resting position, once, then stops. One-shot and event-driven (React
2179 remounts the element by key on each stash) — no continuous animation. */1849 remounts the element by key on each stash) — no continuous animation. */
...@@ -2188,19 +1858,6 @@ label:has(> select#reasoning-effort) select {...@@ -2188,19 +1858,6 @@ label:has(> select#reasoning-effort) select {
2188 }1858 }
2189}1859}
21901860
2191.prompt-stash-count-enter {
2192 animation: prompt-stash-count-enter 180ms ease-out both;
2193}
2194
2195@media (prefers-reduced-motion: reduce) {
2196 .prompt-stash-count-enter {
2197 animation: none;
2198 }
2199 [data-slot="skeleton"]::after {
2200 content: none;
2201 }
2202}
2203
2204@keyframes provider-update-pill-countdown {1861@keyframes provider-update-pill-countdown {
2205 from {1862 from {
2206 transform: scaleX(1);1863 transform: scaleX(1);
...@@ -2210,23 +1867,6 @@ label:has(> select#reasoning-effort) select {...@@ -2210,23 +1867,6 @@ label:has(> select#reasoning-effort) select {
2210 }1867 }
2211}1868}
22121869
2213.provider-update-pill-progress {
2214 animation: provider-update-pill-countdown var(--provider-update-pill-dismiss-ms) linear forwards;
2215}
2216
2217/* Diffs theme bridge (match diff surfaces to app palette) */
2218.diff-panel-viewport {
2219 background: var(--background);
2220}
2221
2222/* Diffs live directly on the panel canvas. Normal chat code blocks may use a
2223 raised code surface, but carrying that fill into the diff creates a card-like
2224 rectangle that does not belong in the panel. */
2225.diff-render-surface {
2226 --code-background: var(--background);
2227}
2228
2229.diff-render-file,
2230.diff-render-surface diffs-container {1870.diff-render-surface diffs-container {
2231 border: 0;1871 border: 0;
2232 border-radius: 0;1872 border-radius: 0;
...@@ -2307,40 +1947,3 @@ label:has(> select#reasoning-effort) select {...@@ -2307,40 +1947,3 @@ label:has(> select#reasoning-effort) select {
2307.ultrathink-chroma {1947.ultrathink-chroma {
2308 animation: ultrathink-chroma-shift 10s linear infinite;1948 animation: ultrathink-chroma-shift 10s linear infinite;
2309}1949}
2310
2311.ultrathink-pill {
2312 background:
2313 linear-gradient(var(--card), var(--card)) padding-box,
2314 var(--ultrathink-spectrum) border-box;
2315 background-size:
2316 100% 100%,
2317 220% 220%;
2318 background-position:
2319 0 0,
2320 0% 50%;
2321 animation: ultrathink-rainbow 10s linear infinite;
2322 box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--card) 82%, transparent);
2323}
2324
2325.ultrathink-word {
2326 display: inline-block;
2327 color: transparent;
2328 background-image: var(--ultrathink-spectrum);
2329 background-size: 220% 220%;
2330 background-position: 0% 50%;
2331 background-clip: text;
2332 -webkit-background-clip: text;
2333 animation: ultrathink-rainbow 10s linear infinite;
2334}
2335
2336/* Composer chips are non-editable decorators, so the browser skips them when
2337 painting text selection; this overlay stands in for the native highlight. */
2338.composer-inline-chip[data-composer-chip-selected]::after {
2339 content: "";
2340 position: absolute;
2341 inset: 0;
2342 border-radius: 6px;
2343 background-color: Highlight;
2344 opacity: 0.3;
2345 pointer-events: none;
2346}
apps/web/src/routes/-chatIndexTitlebar.test.ts+5-2
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1// @effect-diagnostics nodeBuiltinImport:off - Regression coverage compares the onboarding header with the shared titlebar contract.1// @effect-diagnostics nodeBuiltinImport:off
2// Regression coverage compares the onboarding header with the shared titlebar contract.
2import * as NodeFS from "node:fs";3import * as NodeFS from "node:fs";
34
4import { describe, expect, it } from "vite-plus/test";5import { describe, expect, it } from "vite-plus/test";
...@@ -14,7 +15,9 @@ describe("hosted static onboarding header", () => {...@@ -14,7 +15,9 @@ describe("hosted static onboarding header", () => {
1415
15 const onboardingHeader = routeSource.slice(onboardingStart, onboardingEnd);16 const onboardingHeader = routeSource.slice(onboardingStart, onboardingEnd);
1617
17 expect(onboardingHeader).toContain("workspace-topbar");18 expect(onboardingHeader).toContain("h-[var(--workspace-topbar-height)]");
19 expect(onboardingHeader).toContain("min-h-[var(--workspace-topbar-height)]");
20 expect(onboardingHeader).toContain("COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS");
18 expect(onboardingHeader).not.toMatch(/(?:^|\s)(?:[\w-]+:)*py-/);21 expect(onboardingHeader).not.toMatch(/(?:^|\s)(?:[\w-]+:)*py-/);
19 });22 });
20});23});
apps/web/src/routes/_chat.index.tsx+1-1
...@@ -145,7 +145,7 @@ function HostedStaticOnboardingState() {...@@ -145,7 +145,7 @@ function HostedStaticOnboardingState() {
145 <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">145 <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">
146 <header146 <header
147 className={cn(147 className={cn(
148 "workspace-topbar border-b border-border px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",148 "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center border-b border-border px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",
149 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,149 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
150 )}150 )}
151 >151 >
apps/web/src/routes/_chat.pull-requests.tsx+4-3
...@@ -1308,7 +1308,8 @@ function PullRequestsRouteView() {...@@ -1308,7 +1308,8 @@ function PullRequestsRouteView() {
1308 // anchor the thread view's controls and the sidebar trigger use, so1308 // anchor the thread view's controls and the sidebar trigger use, so
1309 // every titlebar cluster in the app sits one shared inset from its1309 // every titlebar cluster in the app sits one shared inset from its
1310 // edge.1310 // edge.
1311 className="workspace-titlebar-controls z-50 mr-px gap-1 [-webkit-app-region:no-drag]"1311 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]"
1312 data-workspace-titlebar-controls
1312 >1313 >
1313 {panelToggleControls}1314 {panelToggleControls}
1314 </div>1315 </div>
...@@ -1828,7 +1829,7 @@ function PullRequestsColumn({...@@ -1828,7 +1829,7 @@ function PullRequestsColumn({
1828 <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background">1829 <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background">
1829 <header1830 <header
1830 className={cn(1831 className={cn(
1831 "workspace-topbar drag-region gap-1.5 px-3 sm:px-5",1832 "drag-region flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center gap-1.5 px-3 sm:px-5",
1832 // A closed right panel leaves this column full-width, so its header runs1833 // A closed right panel leaves this column full-width, so its header runs
1833 // underneath the native window controls on Windows; reserve the inset the1834 // underneath the native window controls on Windows; reserve the inset the
1834 // way Settings and the chat view do. While the panel is open the column1835 // way Settings and the chat view do. While the panel is open the column
...@@ -1903,7 +1904,7 @@ function PullRequestsColumn({...@@ -1903,7 +1904,7 @@ function PullRequestsColumn({
19031904
1904 <div1905 <div
1905 ref={scrollRef}1906 ref={scrollRef}
1906 className="pull-requests-scroll-fade scrollbar-gutter-both min-h-0 flex-1 overflow-y-auto"1907 className="topbar-scroll-fade scrollbar-gutter-both min-h-0 flex-1 overflow-y-auto [--topbar-scroll-fade-height:1.5rem] sm:[--topbar-scroll-fade-height:1.5rem]"
1907 >1908 >
1908 {/* The top padding is the fade band's own height (1.5rem here), the same pairing the1909 {/* The top padding is the fade band's own height (1.5rem here), the same pairing the
1909 settings page makes: at rest the controls sit fully below the mask, and only1910 settings page makes: at rest the controls sit fully below the mask, and only
apps/web/src/routes/settings.tsx+1-1
...@@ -75,7 +75,7 @@ function SettingsContentLayout() {...@@ -75,7 +75,7 @@ function SettingsContentLayout() {
75 {!isElectron && (75 {!isElectron && (
76 <header76 <header
77 className={cn(77 className={cn(
78 "workspace-topbar px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",78 "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",
79 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,79 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
80 )}80 )}
81 >81 >
apps/web/src/terminal/ghostty/surface.ts+5-4
...@@ -578,8 +578,7 @@ export class GhosttyTerminalSurface {...@@ -578,8 +578,7 @@ export class GhosttyTerminalSurface {
578 options: GhosttyTerminalSurfaceOptions,578 options: GhosttyTerminalSurfaceOptions,
579 ): Promise<GhosttyTerminalSurface> {579 ): Promise<GhosttyTerminalSurface> {
580 const canvas = document.createElement("canvas");580 const canvas = document.createElement("canvas");
581 canvas.className = "t3-ghostty-canvas";581 canvas.className = "block size-full cursor-text";
582 canvas.style.cssText = "display:block;width:100%;height:100%;";
583 canvas.setAttribute("aria-hidden", "true");582 canvas.setAttribute("aria-hidden", "true");
584583
585 const input = document.createElement("textarea");584 const input = document.createElement("textarea");
...@@ -592,14 +591,16 @@ export class GhosttyTerminalSurface {...@@ -592,14 +591,16 @@ export class GhosttyTerminalSurface {
592 "position:absolute;left:4px;top:4px;width:1px;height:1px;opacity:0;padding:0;border:0;resize:none;pointer-events:none;";591 "position:absolute;left:4px;top:4px;width:1px;height:1px;opacity:0;padding:0;border:0;resize:none;pointer-events:none;";
593592
594 const scrollbar = document.createElement("div");593 const scrollbar = document.createElement("div");
595 scrollbar.className = "t3-ghostty-scrollbar";594 scrollbar.className =
595 "group absolute top-1 right-px bottom-1 z-1 w-[var(--app-scrollbar-width)] cursor-default touch-none";
596 scrollbar.setAttribute("role", "scrollbar");596 scrollbar.setAttribute("role", "scrollbar");
597 scrollbar.setAttribute("aria-label", "Terminal scrollback");597 scrollbar.setAttribute("aria-label", "Terminal scrollback");
598 scrollbar.setAttribute("aria-orientation", "vertical");598 scrollbar.setAttribute("aria-orientation", "vertical");
599 scrollbar.tabIndex = 0;599 scrollbar.tabIndex = 0;
600 scrollbar.hidden = true;600 scrollbar.hidden = true;
601 const scrollbarThumb = document.createElement("div");601 const scrollbarThumb = document.createElement("div");
602 scrollbarThumb.className = "t3-ghostty-scrollbar-thumb";602 scrollbarThumb.className =
603 "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)]";
603 scrollbar.append(scrollbarThumb);604 scrollbar.append(scrollbarThumb);
604 mount.replaceChildren(canvas, input, scrollbar);605 mount.replaceChildren(canvas, input, scrollbar);
605606