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";
2828import { cn } from "~/lib/utils";
2929import { orchestrationEnvironment } from "~/state/orchestration";
3030import { ScrollArea } from "~/components/ui/scroll-area";
31import { Button } from "~/components/ui/button";
3132
3233/**
3334 * In-flight states all present as Working (one steady state, per the
......@@ -282,14 +283,15 @@ function WorkflowScriptView({
282283 <span className="truncate font-mono text-[.65rem] text-muted-foreground">
283284 {scriptPath.split("/").at(-1)}
284285 </span>
285 <button
286 type="button"
286 <Button
287 size="icon-micro"
288 variant="ghost-muted"
287289 onClick={onClose}
288290 aria-label="Close script"
289 className="ml-auto text-muted-foreground hover:text-foreground"
291 className="ml-auto"
290292 >
291293 <X aria-hidden className="size-3" />
292 </button>
294 </Button>
293295 </div>
294296 <div className="max-h-72 overflow-auto p-2">
295297 {result._tag === "Success" ? (
......@@ -417,14 +419,14 @@ function ExpandedWorkflowSection({
417419 <span className="ml-auto font-mono normal-case text-muted-foreground/80">
418420 {settled}/{members.length} settled
419421 </span>
420 <button
421 type="button"
422 <Button
423 size="icon-micro"
424 variant="ghost-muted"
422425 onClick={onCollapse}
423426 aria-label="Collapse workflow"
424 className="text-muted-foreground hover:text-foreground"
425427 >
426428 <ChevronDown aria-hidden className="size-3" />
427 </button>
429 </Button>
428430 </div>
429431 <PhaseRail group={group} />
430432 {scriptOpen && canShowScript ? (
apps/web/src/components/BranchToolbarBranchSelector.tsx+6-3
......@@ -51,6 +51,7 @@ import {
5151} from "./ThreadStatusIndicators";
5252import { Button } from "./ui/button";
5353import { Switch } from "./ui/switch";
54import { getVirtualizedScrollFadeClassName } from "./ui/scroll-area";
5455import {
5556 Combobox,
5657 ComboboxEmpty,
......@@ -814,9 +815,11 @@ export function BranchToolbarBranchSelector({
814815 maybeFetchNextBranchPage();
815816 }}
816817 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 showTopBranchScrollFade && "mask-t-from-[calc(100%-var(--fade-size))]",
819 showBottomBranchScrollFade && "mask-b-from-[calc(100%-var(--fade-size))]",
818 "scrollbar-gutter-stable overflow-x-hidden overscroll-y-contain ps-1 pe-0 pt-2 pb-1",
819 getVirtualizedScrollFadeClassName({
820 top: showTopBranchScrollFade,
821 bottom: showBottomBranchScrollFade,
822 }),
820823 )}
821824 style={{ maxHeight: "14rem" }}
822825 />
apps/web/src/components/ChatMarkdown.tsx+13-10
......@@ -449,7 +449,7 @@ function MarkdownTable({ children, ...props }: React.ComponentProps<"table">) {
449449 {children}
450450 </table>
451451 </ScrollArea>
452 <div className="chat-markdown-table-footer select-none">
452 <div className="mt-0.5 flex items-center justify-between select-none">
453453 <Tooltip>
454454 <TooltipTrigger
455455 render={
......@@ -642,12 +642,12 @@ function MarkdownCodeBlock({
642642
643643 return (
644644 <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"
646646 data-language={language}
647647 data-wrap={wrapped ? "true" : "false"}
648648 >
649 <div className="chat-markdown-codeblock-header select-none">
650 <span className="chat-markdown-codeblock-title">
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="inline-flex min-w-0 items-center gap-[0.4rem] [font-family:var(--font-mono,ui-monospace,SFMono-Regular,monospace)] [font-size:0.6875rem]">
651651 <MarkdownCodeBlockTitleContent
652652 fenceTitle={fenceTitle}
653653 language={language}
......@@ -897,7 +897,10 @@ const failedFaviconHosts = new Set<string>();
897897const MarkdownLinkFavicon = memo(function MarkdownLinkFavicon({ host }: { host: string }) {
898898 const [failedHost, setFailedHost] = useState<string | null>(null);
899899 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 >
901904 {failedHost === host || failedFaviconHosts.has(host) ? (
902905 <GlobeIcon className={MARKDOWN_LINK_FAVICON_CLASS_NAME} />
903906 ) : (
......@@ -1044,7 +1047,7 @@ function MarkdownExternalLinkContent({
10441047 const leadingLength = leadingExternalLinkTextLength(plainText);
10451048 return (
10461049 <>
1047 <span className="chat-markdown-link-leading">
1050 <span className="whitespace-nowrap">
10481051 <MarkdownLinkFavicon host={host} />
10491052 {plainText.slice(0, leadingLength)}
10501053 </span>
......@@ -1060,7 +1063,7 @@ function MarkdownExternalLinkContent({
10601063 const leadingLength = leadingExternalLinkTextLength(firstChild);
10611064 return (
10621065 <>
1063 <span className="chat-markdown-link-leading">
1066 <span className="whitespace-nowrap">
10641067 <MarkdownLinkFavicon host={host} />
10651068 {firstChild.slice(0, leadingLength)}
10661069 </span>
......@@ -1072,7 +1075,7 @@ function MarkdownExternalLinkContent({
10721075
10731076 return (
10741077 <>
1075 <span className="chat-markdown-link-leading">
1078 <span className="whitespace-nowrap">
10761079 <MarkdownLinkFavicon host={host} />
10771080 {firstChild}
10781081 </span>
......@@ -1289,7 +1292,7 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
12891292 side="top"
12901293 className="max-w-[min(40rem,calc(100vw-2rem))] font-mono text-[11px] leading-tight"
12911294 >
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">
12931296 {displayPath}
12941297 </div>
12951298 </TooltipPopup>
......@@ -1699,7 +1702,7 @@ function ChatMarkdown({
16991702 return (
17001703 <div
17011704 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]",
17031706 className,
17041707 )}
17051708 onCopy={handleCopy}
apps/web/src/components/ChatView.tsx+10-8
......@@ -6023,8 +6023,9 @@ function ChatViewContent(props: ChatViewProps) {
60236023 // One inset in both states: the controls move between containers when
60246024 // the right panel opens, and a different right offset made them jump
60256025 // 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]",
60276027 )}
6028 data-workspace-titlebar-controls
60286029 >
60296030 {rightPanelOpen && !shouldUseRightPanelSheet ? (
60306031 <RightPanelMaximizeControl
......@@ -6165,12 +6166,12 @@ function ChatViewContent(props: ChatViewProps) {
61656166 "bg-background transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none",
61666167 isElectron
61676168 ? 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",
61696170 reserveTitleBarControlInset &&
61706171 !inlineRightPanelOwnsTitleBar &&
61716172 "wco:pr-[var(--workspace-native-controls-inset)]",
61726173 )
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)]",
61746175 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
61756176 )}
61766177 >
......@@ -6273,16 +6274,17 @@ function ChatViewContent(props: ChatViewProps) {
62736274 className="pointer-events-none absolute left-1/2 z-30 flex -translate-x-1/2 justify-center py-1.5"
62746275 style={{ bottom: composerOverlayHeight + 4 }}
62756276 >
6276 <button
6277 type="button"
6277 <Button
62786278 aria-label="Scroll to end"
62796279 title="Scroll to end"
62806280 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"
62826284 >
62836285 <ChevronDownIcon className="size-3.5" />
62846286 Scroll to end
6285 </button>
6287 </Button>
62866288 </div>
62876289 )}
62886290 </div>
......@@ -6299,7 +6301,7 @@ function ChatViewContent(props: ChatViewProps) {
62996301 >
63006302 <div
63016303 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)]"
63036305 >
63046306 <div className="pointer-events-auto relative z-10">
63056307 {isDraftHeroState ? (
apps/web/src/components/ComposerPromptEditor.tsx+6-6
......@@ -71,6 +71,7 @@ import {
7171import { cn, isMacPlatform } from "~/lib/utils";
7272import { basenameOfPath } from "~/pierre-icons";
7373import {
74 COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME,
7475 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
7576 COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME,
7677 COMPOSER_INLINE_SKILL_CHIP_LABEL_CLASS_NAME,
......@@ -188,7 +189,7 @@ class ComposerMentionNode extends DecoratorNode<React.ReactElement> {
188189
189190 override createDOM(): HTMLElement {
190191 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;
192193 return dom;
193194 }
194195
......@@ -326,7 +327,7 @@ class ComposerSkillNode extends DecoratorNode<React.ReactElement> {
326327
327328 override createDOM(): HTMLElement {
328329 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;
330331 return dom;
331332 }
332333
......@@ -397,7 +398,7 @@ class ComposerTerminalContextNode extends DecoratorNode<React.ReactElement> {
397398
398399 override createDOM(): HTMLElement {
399400 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;
401402 return dom;
402403 }
403404
......@@ -1747,13 +1748,12 @@ function ComposerPromptEditorInner({
17471748
17481749 return (
17491750 <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)]">
17511752 <PlainTextPlugin
17521753 contentEditable={
17531754 <ContentEditable
17541755 className={cn(
1755 // The size comes from .composer-editor-surface so Settings -> Appearance
1756 // can drive it; keep everything else here.
1756 // The wrapper owns the appearance preference; keep everything else here.
17571757 "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",
17581758 className,
17591759 )}
apps/web/src/components/DiffPanel.tsx+5-4
......@@ -825,7 +825,7 @@ export default function DiffPanel({
825825 </div>
826826 ) : (
827827 <>
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">
829829 {isSelectedPatchTruncated && (
830830 <p className="shrink-0 border-b border-border/70 bg-muted/40 px-3 py-1.5 text-[11px] text-muted-foreground">
831831 This diff was truncated because it exceeded the preview limit. The changes shown are
......@@ -907,10 +907,11 @@ export default function DiffPanel({
907907 <Tooltip>
908908 <TooltipTrigger
909909 render={
910 <button
911 type="button"
910 <Button
911 size="icon-micro"
912 variant="ghost"
912913 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",
914915 getDiffCollapseIconClassName(fileDiff),
915916 )}
916917 aria-label={collapsed ? `Expand ${filePath}` : `Collapse ${filePath}`}
apps/web/src/components/DiffPanelShell.tsx+1-1
......@@ -14,7 +14,7 @@ function getDiffPanelHeaderRowClassName(mode: DiffPanelMode) {
1414 mode === "embedded" ? "px-2" : "px-4",
1515 shouldUseDragRegion
1616 ? "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",
1818 );
1919}
2020
apps/web/src/components/LegacySidebar.tsx+4-3
......@@ -2925,11 +2925,12 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
29252925 <Tooltip>
29262926 <TooltipTrigger
29272927 render={
2928 <button
2929 type="button"
2928 <Button
2929 size="icon-xs"
2930 variant="ghost-muted"
29302931 aria-label="Add project"
29312932 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"
29332934 onClick={openAddProject}
29342935 />
29352936 }
apps/web/src/components/NoActiveThreadState.tsx+3-1
......@@ -11,7 +11,9 @@ export function NoActiveThreadState() {
1111 <header
1212 className={cn(
1313 "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",
1517 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
1618 )}
1719 >
apps/web/src/components/RightPanelTabs.tsx+10-3
......@@ -26,6 +26,7 @@ import type { DesktopPreviewOverlay } from "~/previewStateStore";
2626import type { RightPanelSurface } from "~/rightPanelStore";
2727import { cn } from "~/lib/utils";
2828import { readLocalApi } from "~/localApi";
29import { Button } from "~/components/ui/button";
2930import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
3031import { Kbd } from "~/components/ui/kbd";
3132import { Menu, MenuItem, MenuPopup, MenuTrigger } from "~/components/ui/menu";
......@@ -603,7 +604,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
603604 >
604605 <div
605606 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",
607608 // The sheet overlays from the viewport top, so its tab bar keeps
608609 // the titlebar's height: a compact row re-centers the layout
609610 // controls a few pixels higher and the cluster jumps on open.
......@@ -682,8 +683,14 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
682683 {props.surfaces.length > 0 ? (
683684 <Menu>
684685 <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 aria-label="Add panel surface"
686 render={
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 }
687694 >
688695 <Plus className="size-3.5" />
689696 </MenuTrigger>
apps/web/src/components/Sidebar.tsx+7-6
......@@ -3298,9 +3298,9 @@ export default function Sidebar() {
32983298 {isSearchingThreads ? (
32993299 <Button
33003300 type="button"
3301 size="icon-xs"
3301 size="icon-micro"
33023302 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"
33043304 aria-label="Clear thread search"
33053305 onClick={() => {
33063306 clearThreadSearch();
......@@ -3412,18 +3412,19 @@ export default function Sidebar() {
34123412 className="size-4 shrink-0"
34133413 />
34143414 <span className="min-w-0 truncate text-sm">{project.displayName}</span>
3415 <button
3416 type="button"
3415 <Button
3416 size="icon-xs"
3417 variant="ghost-muted"
34173418 aria-label={`Project settings for ${project.displayName}`}
34183419 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"
34203421 onPointerDown={(event) => event.stopPropagation()}
34213422 onClick={(event) => {
34223423 void handleProjectSettings(event, project);
34233424 }}
34243425 >
34253426 <SettingsIcon className="size-3.5" />
3426 </button>
3427 </Button>
34273428 </MenuRadioItem>
34283429 );
34293430 })}
apps/web/src/components/ThreadTerminalDrawer.tsx+3-6
......@@ -31,6 +31,7 @@ import {
3131 useState,
3232} from "react";
3333import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover";
34import { Button } from "~/components/ui/button";
3435import { writeTextToClipboard } from "~/hooks/useCopyToClipboard";
3536import { cn } from "~/lib/utils";
3637import { type TerminalContextSelection } from "~/lib/terminalContext";
......@@ -1273,13 +1274,9 @@ export default function ThreadTerminalDrawer({
12731274 ) : null}
12741275 <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">
12751276 <p>No terminal sessions for this thread yet.</p>
1276 <button
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 >
1277 <Button size="xs" variant="outline" onClick={onNewTerminalAction}>
12811278 {newTerminalActionLabel}
1282 </button>
1279 </Button>
12831280 </div>
12841281 </aside>
12851282 );
apps/web/src/components/chat/ComposerCommandMenu.tsx+1-1
......@@ -141,7 +141,7 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {
141141 >
142142 <div
143143 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%)]"
145145 >
146146 {props.items.length > 0 ? (
147147 <CommandList className="max-h-72">
apps/web/src/components/chat/ComposerPreviewAnnotationCards.test.tsx+14
......@@ -43,4 +43,18 @@ describe("ComposerPreviewAnnotationCards", () => {
4343 expect(markup).not.toContain("localhost:3000");
4444 expect(markup).not.toContain("Preview annotation");
4545 });
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 });
4660});
apps/web/src/components/chat/ComposerPreviewAnnotationCards.tsx+6-4
......@@ -5,6 +5,7 @@ import type { ReactNode } from "react";
55import type { ComposerImageAttachment } from "~/composerDraftStore";
66import { formatElementContextLabel, normalizeElementContextSelection } from "~/lib/elementContext";
77import { cn } from "~/lib/utils";
8import { Button } from "../ui/button";
89
910interface ComposerPreviewAnnotationCardsProps {
1011 annotations: ReadonlyArray<PreviewAnnotationPayload>;
......@@ -128,14 +129,15 @@ export function ComposerPreviewAnnotationCards({
128129 </div>
129130 </div>
130131 </div>
131 <button
132 type="button"
132 <Button
133 size="icon-micro"
134 variant="ghost-muted"
133135 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"
135137 onClick={() => onRemove(annotation.id)}
136138 >
137139 <X className="size-3" />
138 </button>
140 </Button>
139141 </section>
140142 );
141143 })}
apps/web/src/components/chat/ComposerStashBadge.tsx+1-1
......@@ -46,7 +46,7 @@ export const ComposerStashBadge = memo(function ComposerStashBadge(props: {
4646 className={cn(
4747 "rounded-full px-1.5 text-[10px] font-medium tabular-nums",
4848 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"
5050 : "bg-muted text-muted-foreground",
5151 )}
5252 >
apps/web/src/components/chat/ComposerStashMenu.tsx+1-1
......@@ -91,7 +91,7 @@ export const ComposerStashMenu = memo(function ComposerStashMenu(props: {
9191
9292 return (
9393 <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%)]">
9595 <CommandList className="max-h-72">
9696 <CommandGroup>
9797 <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 @@
1import { cn } from "~/lib/utils";
1import { Button } from "../ui/button";
22import { type ContextWindowSnapshot, formatContextWindowTokens } from "~/lib/contextWindow";
33import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
44
......@@ -36,13 +36,10 @@ export function ContextWindowMeter(props: {
3636 delay={150}
3737 closeDelay={0}
3838 render={
39 <button
40 type="button"
41 className={cn(
42 "inline-flex size-7 cursor-pointer items-center justify-center rounded-full border border-transparent text-muted-foreground outline-none transition-colors",
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 )}
39 <Button
40 size="icon-sm"
41 variant="ghost-muted"
42 className="size-7 rounded-full hover:text-muted-foreground data-pressed:text-muted-foreground"
4643 aria-label={
4744 usage.maxTokens !== null && usedPercentage
4845 ? `Context window ${usedPercentage} used`
......@@ -77,7 +74,7 @@ export function ContextWindowMeter(props: {
7774 />
7875 </svg>
7976 </span>
80 </button>
77 </Button>
8178 }
8279 />
8380 <PopoverPopup
apps/web/src/components/chat/MessagesTimeline.test.tsx+2-2
......@@ -237,9 +237,9 @@ describe("MessagesTimeline", () => {
237237 );
238238
239239 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");
241241 expect(fadedMarkup).toContain('class="h-10 sm:h-12"');
242 expect(fadedMarkup).toContain("chat-timeline-scroll-fade");
242 expect(fadedMarkup).toContain("topbar-scroll-fade");
243243 });
244244
245245 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({
591591 onScroll={handleScroll}
592592 className={cn(
593593 "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",
595595 )}
596596 ListHeaderComponent={
597597 loadEarlier !== null ? (
apps/web/src/components/chat/ModelPickerContent.tsx+7-4
......@@ -33,6 +33,7 @@ import {
3333} from "../../keybindings";
3434import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings";
3535import { cn } from "~/lib/utils";
36import { getVirtualizedScrollFadeClassName } from "../ui/scroll-area";
3637import { TooltipProvider } from "../ui/tooltip";
3738import {
3839 isProviderInstancePickerReady,
......@@ -598,7 +599,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
598599 return (
599600 <TooltipProvider delay={0}>
600601 <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%)]"
602603 data-model-picker-content="true"
603604 >
604605 {/* Sidebar */}
......@@ -781,9 +782,11 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
781782 onLayout={updateModelListScrollFades}
782783 onScroll={updateModelListScrollFades}
783784 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 showTopScrollFade && "model-picker-list-scroll-fade-top",
786 showBottomScrollFade && "model-picker-list-scroll-fade-bottom",
785 "scrollbar-gutter-stable h-full overflow-x-hidden overscroll-y-contain py-1.5 [&::-webkit-scrollbar-track]:my-2",
786 getVirtualizedScrollFadeClassName({
787 top: showTopScrollFade,
788 bottom: showBottomScrollFade,
789 }),
787790 )}
788791 />
789792 </ComboboxListVirtualized>
apps/web/src/components/chat/ProviderStatusBanner.tsx+6-4
......@@ -2,6 +2,7 @@ import { type ServerProvider } from "@t3tools/contracts";
22import { memo } from "react";
33import { InfoIcon, XIcon } from "lucide-react";
44import { cn } from "~/lib/utils";
5import { Button } from "../ui/button";
56import { formatProviderDriverKindLabel } from "../../providerModels";
67import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
78
......@@ -66,14 +67,15 @@ export const ProviderStatusBanner = memo(function ProviderStatusBanner({
6667 </TooltipPopup>
6768 </Tooltip>
6869 </div>
69 <button
70 type="button"
70 <Button
7171 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"
7373 onClick={onDismiss}
74 size="icon-xs"
75 variant="ghost"
7476 >
7577 <XIcon aria-hidden className="size-3.5" />
76 </button>
78 </Button>
7779 </div>
7880 </div>
7981 );
apps/web/src/components/composerInlineChip.ts+3
......@@ -8,6 +8,9 @@ export const CHAT_INLINE_CHIP_CLASS_NAME = `${INLINE_CHIP_CLASS_NAME} text-[12px
88
99export 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
1114export const COMPOSER_INLINE_CHIP_ICON_CLASS_NAME = "size-[1.17em] shrink-0 opacity-85";
1215
1316export 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", () => {
3535 />,
3636 );
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 );
3941 expect(testState.codeViewOptions).toMatchObject({
4042 theme: "pierre-dark",
4143 stickyHeaders: true,
apps/web/src/components/diffs/StyledDiffCodeView.tsx+2-2
......@@ -292,8 +292,8 @@ export function StyledDiffCodeView<LAnnotation = undefined>({
292292 // outside the panel clipping boundary; actual controls inside retain their own indicators.
293293 className={
294294 className
295 ? `diff-render-surface outline-none ${className}`
296 : "diff-render-surface outline-none"
295 ? `diff-render-surface [--code-background:var(--background)] outline-none ${className}`
296 : "diff-render-surface [--code-background:var(--background)] outline-none"
297297 }
298298 options={{
299299 ...options,
apps/web/src/components/files/FileBrowserPanel.tsx+5-2
......@@ -78,7 +78,7 @@ function FileSearchField(props: {
7878 value: string;
7979}) {
8080 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">
8282 <InputGroupInput
8383 type="search"
8484 name={props.name}
......@@ -350,7 +350,10 @@ export default function FileBrowserPanel({
350350 className="flex min-h-0 flex-1 flex-col bg-background"
351351 data-file-browser-panel={`${environmentId}:${cwd}`}
352352 >
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 >
354357 <RefreshFilesButton isPending={entriesQuery.isPending} onRefresh={entriesQuery.refresh} />
355358 <FileSearchField
356359 name="project-files-search"
apps/web/src/components/files/FilePreviewPanel.tsx+4-1
......@@ -857,7 +857,10 @@ export default function FilePreviewPanel({
857857 return (
858858 <div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background">
859859 {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 >
861864 <ScrollArea
862865 ref={breadcrumbRef}
863866 hideScrollbars
apps/web/src/components/preview/PreviewChromeRow.tsx+6-2
......@@ -109,7 +109,11 @@ export function PreviewChromeRow({
109109
110110 return (
111111 <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 >
113117 <div className="flex items-center gap-0.5" role="group" aria-label="Navigation">
114118 <Tooltip>
115119 <TooltipTrigger
......@@ -164,7 +168,7 @@ export function PreviewChromeRow({
164168 </Tooltip>
165169 </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">
168172 <Tooltip>
169173 <TooltipTrigger
170174 render={
apps/web/src/components/pullRequest/PullRequestCodeTab.tsx+12-12
......@@ -668,13 +668,12 @@ export function PullRequestCodeTab({
668668 // chevron follows it rather than recomputing the default here.
669669 const collapsed = item.collapsed === true;
670670 return (
671 <button
672 type="button"
671 <Button
672 size="icon-micro"
673 variant="ghost-muted"
673674 aria-expanded={!collapsed}
674675 aria-label={collapsed ? "Expand diff" : "Collapse diff"}
675 className={cn(
676 "mr-1 inline-flex size-5 items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground",
677 )}
676 className="mr-1 rounded hover:bg-transparent"
678677 onClick={(event) => {
679678 event.stopPropagation();
680679 toggleFile(item.id);
......@@ -685,7 +684,7 @@ export function PullRequestCodeTab({
685684 ) : (
686685 <ChevronDownIcon className="size-4" />
687686 )}
688 </button>
687 </Button>
689688 );
690689 },
691690 [toggleFile],
......@@ -899,7 +898,7 @@ export function PullRequestCodeTab({
899898 review.verdicts.length === 0 ? null : (
900899 <div className="pointer-events-none absolute inset-x-0 bottom-0 z-10">
901900 {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">
903902 <Button
904903 type="button"
905904 size="icon-sm"
......@@ -923,10 +922,11 @@ export function PullRequestCodeTab({
923922 ) : (
924923 // Bottom-right, clear of the vertical scrollbar the diff view keeps to its own right
925924 // edge.
926 <button
927 type="button"
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"
925 <Button
926 className="pointer-events-auto absolute right-4 bottom-3 rounded-full shadow-lg"
929927 onClick={() => setReviewOpen(true)}
928 size="compact"
929 variant="glass"
930930 >
931931 <MessageSquareIcon className="size-3.5" />
932932 Review
......@@ -935,7 +935,7 @@ export function PullRequestCodeTab({
935935 {pendingComments.length}
936936 </span>
937937 ) : null}
938 </button>
938 </Button>
939939 )}
940940 </div>
941941 );
......@@ -959,7 +959,7 @@ export function PullRequestCodeTab({
959959 * diff API offers it.
960960 */
961961 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">
963963 <div className="flex min-w-0 flex-1 items-center gap-3">
964964 {/* A host that reports no commits has nothing to scope by, and a dropdown whose only
965965 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({
11371137 <>
11381138 <Menu>
11391139 <MenuTrigger
1140 className="inline-flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
1141 aria-label="More pull request actions"
1140 render={
1141 <Button
1142 aria-label="More pull request actions"
1143 className="size-6"
1144 size="icon-xs"
1145 variant="ghost-muted"
1146 />
1147 }
11421148 >
11431149 <MoreHorizontalIcon className="size-4" />
11441150 </MenuTrigger>
apps/web/src/components/pullRequest/PullRequestListFilters.tsx+8-18
......@@ -24,6 +24,7 @@ import type { ElementType } from "react";
2424import { cn } from "~/lib/utils";
2525import { getSourceControlPresentationForKind } from "~/sourceControlPresentation";
2626import { ProjectFavicon } from "../ProjectFavicon";
27import { InputGroup, InputGroupAddon, InputGroupInput } from "../ui/input-group";
2728
2829import {
2930 Menu,
......@@ -79,29 +80,18 @@ export function PullRequestSearchInput({
7980 onChange: (value: string) => void;
8081}) {
8182 return (
82 <div className="relative min-w-0 flex-1">
83 {busy ? (
84 <LoaderIcon
85 aria-hidden
86 className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 animate-spin text-muted-foreground"
87 />
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"
83 <InputGroup className="min-w-0 flex-1 **:[input]:h-9 sm:**:[input]:h-8">
84 <InputGroupAddon>
85 {busy ? <LoaderIcon aria-hidden className="animate-spin" /> : <SearchIcon aria-hidden />}
86 </InputGroupAddon>
87 <InputGroupInput
88 type="search"
9689 value={value}
9790 onChange={(event) => onChange(event.currentTarget.value)}
9891 placeholder="Search pull requests, or label:bug"
9992 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"
10393 />
104 </div>
94 </InputGroup>
10595 );
10696}
10797
apps/web/src/components/pullRequest/PullRequestReviewerPicker.tsx+3-2
......@@ -19,6 +19,7 @@ import { useAtomCommand } from "~/state/use-atom-command";
1919import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
2020
2121import { Button } from "../ui/button";
22import { Input } from "../ui/input";
2223import { Menu, MenuPopup, MenuTrigger } from "../ui/menu";
2324import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
2425import { toastManager } from "../ui/toast";
......@@ -131,13 +132,13 @@ export function PullRequestReviewerPicker({
131132 />
132133 <MenuPopup align="start" side="bottom" className="w-72 p-0">
133134 <div className="border-b border-border/60 p-2">
134 <input
135 <Input
135136 autoFocus
136137 value={query}
137138 onChange={(event) => setQuery(event.currentTarget.value)}
138139 placeholder="Search people with access"
139140 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"
141142 />
142143 </div>
143144 <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";
1111import { PierreEntryIcon } from "../chat/PierreEntryIcon";
1212import { CommandPaletteContent } from "../CommandPaletteContent";
1313import { ScrollArea } from "../ui/scroll-area";
14import { Toggle } from "../ui/toggle";
1415import { HighlightedSearchLine } from "./HighlightedSearchLine";
1516
1617interface ProjectContentSearchDialogProps {
......@@ -58,19 +59,17 @@ function SearchOptionButton(props: {
5859 readonly children: ReactNode;
5960}) {
6061 return (
61 <button
62 type="button"
62 <Toggle
6363 aria-label={props.label}
64 aria-pressed={props.active}
64 pressed={props.active}
6565 title={props.label}
66 className={cn(
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",
68 props.active && "bg-accent text-foreground shadow-sm",
69 )}
66 className="size-8 rounded-[5px] font-mono text-muted-foreground data-pressed:text-foreground sm:size-7"
67 size="compact"
68 variant="ghost"
7069 onClick={props.onClick}
7170 >
7271 {props.children}
73 </button>
72 </Toggle>
7473 );
7574}
7675
apps/web/src/components/settings/DiagnosticsSettings.tsx+12-14
......@@ -273,14 +273,14 @@ function TraceIdCell({ traceId }: { traceId: string }) {
273273 <Tooltip>
274274 <TooltipTrigger
275275 render={
276 <button
277 type="button"
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"
276 <Button
277 size="icon-micro"
278 variant="ghost-muted"
279279 aria-label={copied ? "Copied trace ID" : "Copy trace ID"}
280280 onClick={() => copyToClipboard(traceId)}
281281 >
282282 <CopyIcon className="size-3" />
283 </button>
283 </Button>
284284 }
285285 />
286286 <TooltipPopup side="top">{copied ? "Copied" : "Copy full trace ID"}</TooltipPopup>
......@@ -322,14 +322,14 @@ function ProcessNameCell({
322322 style={{ paddingLeft: `${Math.min(process.depth, 6) * 10}px` }}
323323 >
324324 {hasChildren ? (
325 <button
326 type="button"
327 className="cursor-pointer inline-flex size-5 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
325 <Button
326 size="icon-micro"
327 variant="ghost-muted"
328328 aria-label={isExpanded ? `Collapse ${name}` : `Expand ${name}`}
329329 onClick={() => onToggle(process.pid)}
330330 >
331331 <ChevronIcon className="size-3.5" />
332 </button>
332 </Button>
333333 ) : (
334334 <span className="size-5 shrink-0" aria-hidden="true" />
335335 )}
......@@ -793,9 +793,8 @@ function DiagnosticsRefreshButton({
793793 <TooltipTrigger
794794 render={
795795 <Button
796 size="icon-xs"
797 variant="ghost"
798 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
796 size="icon-micro"
797 variant="ghost-muted"
799798 disabled={isPending}
800799 onClick={onClick}
801800 aria-label={label}
......@@ -1130,9 +1129,8 @@ export function DiagnosticsSettingsPanel() {
11301129 <TooltipTrigger
11311130 render={
11321131 <Button
1133 size="icon-xs"
1134 variant="ghost"
1135 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
1132 size="icon-micro"
1133 variant="ghost-muted"
11361134 disabled={!observability?.logsDirectoryPath || isOpeningLogsDirectory}
11371135 onClick={openLogsDirectory}
11381136 aria-label="Open logs folder"
apps/web/src/components/settings/KeybindingsSettings.tsx+31-67
......@@ -125,9 +125,8 @@ function ExpandableHeaderSearch({
125125 render={
126126 <Button
127127 type="button"
128 size="icon-xs"
129 variant="ghost"
130 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
128 size="icon-micro"
129 variant="ghost-muted"
131130 onClick={() => onOpenChange(true)}
132131 aria-label="Search keybindings"
133132 >
......@@ -144,10 +143,10 @@ function ExpandableHeaderSearch({
144143 return (
145144 <div className="relative">
146145 <SearchIcon className="pointer-events-none absolute top-1/2 left-2 size-3 -translate-y-1/2 text-muted-foreground" />
147 <input
146 <Input
148147 ref={inputRef}
149148 autoFocus
150 type="text"
149 type="search"
151150 value={query}
152151 onChange={(event) => onChange(event.currentTarget.value)}
153152 onBlur={() => {
......@@ -162,7 +161,8 @@ function ExpandableHeaderSearch({
162161 }}
163162 placeholder="Search keybindings"
164163 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"
166166 />
167167 </div>
168168 );
......@@ -309,10 +309,7 @@ function WhenVariableSelect({
309309
310310 return (
311311 <Select value={value} onValueChange={(nextValue) => nextValue && onChange(nextValue)}>
312 <SelectTrigger
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 >
312 <SelectTrigger size="compact" className="min-w-0 flex-1 font-mono">
316313 <SelectValue placeholder="Condition" className="leading-7" />
317314 {unknownIdentifiers && unknownIdentifiers.length > 0 ? (
318315 <UnknownWhenVariableWarning identifiers={unknownIdentifiers} focusable={false} />
......@@ -365,8 +362,8 @@ function WhenExpressionNodeEditor({
365362 onPressedChange={(pressed) => onChange(setConditionNegated(node, pressed))}
366363 aria-label={`Negate ${condition.identifier}`}
367364 variant="outline"
368 size="xs"
369 className="h-7 min-w-10 px-2 text-[11px] sm:h-7"
365 size="compact"
366 className="min-w-10"
370367 >
371368 Not
372369 </Toggle>
......@@ -381,7 +378,7 @@ function WhenExpressionNodeEditor({
381378 type="button"
382379 variant="ghost"
383380 size="icon-sm"
384 className="size-7 sm:size-7"
381 className="size-7"
385382 aria-label="Remove condition"
386383 onClick={onRemove}
387384 >
......@@ -406,8 +403,8 @@ function WhenExpressionNodeEditor({
406403 onPressedChange={(pressed) => onChange(pressed ? node : node.node)}
407404 aria-label="Negate group"
408405 variant="outline"
409 size="xs"
410 className="h-7 min-w-10 px-2 text-[11px] sm:h-7"
406 size="compact"
407 className="min-w-10"
411408 >
412409 Not
413410 </Toggle>
......@@ -416,7 +413,7 @@ function WhenExpressionNodeEditor({
416413 type="button"
417414 variant="ghost"
418415 size="icon-sm"
419 className="ml-auto size-7 sm:size-7"
416 className="ml-auto size-7"
420417 aria-label="Remove negated group"
421418 onClick={onRemove}
422419 >
......@@ -509,7 +506,7 @@ function WhenExpressionNodeEditor({
509506 >
510507 <div className="flex flex-wrap items-center gap-2">
511508 <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">
513510 <SelectValue />
514511 </SelectTrigger>
515512 <SelectContent
......@@ -526,17 +523,11 @@ function WhenExpressionNodeEditor({
526523 </SelectItem>
527524 </SelectContent>
528525 </Select>
529 <Button
530 type="button"
531 variant="outline"
532 size="xs"
533 className="h-7 sm:h-7"
534 onClick={addCondition}
535 >
526 <Button type="button" variant="outline" size="compact" onClick={addCondition}>
536527 <PlusIcon className="size-3.5" />
537528 Condition
538529 </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}>
540531 <PlusIcon className="size-3.5" />
541532 Group
542533 </Button>
......@@ -545,7 +536,7 @@ function WhenExpressionNodeEditor({
545536 type="button"
546537 variant="ghost"
547538 size="icon-sm"
548 className="ml-auto size-7 sm:size-7"
539 className="ml-auto size-7"
549540 aria-label="Remove group"
550541 onClick={onRemove}
551542 >
......@@ -640,23 +631,11 @@ function WhenExpressionBuilder({
640631 <div className="text-sm font-medium text-foreground">When</div>
641632 </div>
642633 <div className="flex shrink-0 items-center gap-2">
643 <Button
644 type="button"
645 variant="outline"
646 size="xs"
647 className="h-7 sm:h-7"
648 onClick={addRootCondition}
649 >
634 <Button type="button" variant="outline" size="compact" onClick={addRootCondition}>
650635 <PlusIcon className="size-3.5" />
651636 Condition
652637 </Button>
653 <Button
654 type="button"
655 variant="outline"
656 size="xs"
657 className="h-7 sm:h-7"
658 onClick={addRootGroup}
659 >
638 <Button type="button" variant="outline" size="compact" onClick={addRootGroup}>
660639 <PlusIcon className="size-3.5" />
661640 Group
662641 </Button>
......@@ -702,17 +681,11 @@ function WhenExpressionBuilder({
702681 ) : (
703682 <div className="rounded-md border border-dashed border-border/80 bg-muted/15 p-3">
704683 <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}>
706685 <PlusIcon className="size-3.5" />
707686 Condition
708687 </Button>
709 <Button
710 type="button"
711 variant="outline"
712 size="xs"
713 className="h-7 sm:h-7"
714 onClick={addRootGroup}
715 >
688 <Button type="button" variant="outline" size="compact" onClick={addRootGroup}>
716689 <PlusIcon className="size-3.5" />
717690 Group
718691 </Button>
......@@ -864,8 +837,7 @@ function KeybindingTableRow({
864837 )}
865838 {isDirty ? (
866839 <Button
867 size="xs"
868 className="h-7 sm:h-7"
840 size="compact"
869841 disabled={isSaving || keyDraft.trim().length === 0 || !isWhenDraftValid}
870842 onClick={save}
871843 >
......@@ -992,10 +964,7 @@ function NewKeybindingTableRow({
992964 value={commandDraft}
993965 onValueChange={(value) => setCommandDraft(value as KeybindingCommand)}
994966 >
995 <SelectTrigger
996 size="xs"
997 className="h-7 min-h-7 w-full max-w-60 rounded-md text-xs sm:h-7"
998 >
967 <SelectTrigger size="compact" className="w-full max-w-60">
999968 <SelectValue placeholder="Command" />
1000969 </SelectTrigger>
1001970 <SelectContent
......@@ -1017,18 +986,15 @@ function NewKeybindingTableRow({
1017986 aria-label={`Keybinding for ${commandLabelText}`}
1018987 value={isRecording ? "" : keyDraft}
1019988 placeholder={isRecording ? "Press shortcut" : "Unassigned"}
1020 className={cn(
1021 "h-7 w-44 rounded-md font-mono text-[12px] sm:h-7",
1022 isRecording && "border-primary/70 bg-primary/5",
1023 )}
989 size="compact"
990 className={cn("w-44 font-mono", isRecording && "border-primary/70 bg-primary/5")}
1024991 onFocus={() => setDraft({ isRecording: true })}
1025992 onBlur={() => setDraft({ isRecording: false })}
1026993 onChange={(event) => setDraft({ keyDraft: event.currentTarget.value })}
1027994 onKeyDown={captureKeybinding}
1028995 />
1029996 <Button
1030 size="xs"
1031 className="h-7 sm:h-7"
997 size="compact"
1032998 disabled={isSaving || !commandDraft || keyDraft.trim().length === 0 || !isWhenDraftValid}
1033999 onClick={save}
10341000 >
......@@ -1066,7 +1032,7 @@ function NewKeybindingTableRow({
10661032 type="button"
10671033 variant="ghost"
10681034 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"
10701036 disabled={isSaving}
10711037 aria-label="Cancel new keybinding"
10721038 onClick={onCancel}
......@@ -1247,9 +1213,8 @@ export function KeybindingsSettingsPanel() {
12471213 render={
12481214 <Button
12491215 type="button"
1250 size="icon-xs"
1251 variant="ghost"
1252 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
1216 size="icon-micro"
1217 variant="ghost-muted"
12531218 onClick={() => setIsAddingBinding(true)}
12541219 aria-label="Add keybinding"
12551220 >
......@@ -1264,9 +1229,8 @@ export function KeybindingsSettingsPanel() {
12641229 render={
12651230 <Button
12661231 type="button"
1267 size="icon-xs"
1268 variant="ghost"
1269 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
1232 size="icon-micro"
1233 variant="ghost-muted"
12701234 disabled={!keybindingsConfigPath}
12711235 onClick={openKeybindingsFile}
12721236 aria-label="Open keybindings.json"
apps/web/src/components/settings/ProjectSettingsPanel.tsx+1-1
......@@ -177,7 +177,7 @@ export function ProjectSettingsPage({ projectKey }: { projectKey: string }) {
177177 {!isElectron && (
178178 <header
179179 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",
181181 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
182182 )}
183183 >
apps/web/src/components/settings/ProviderInstanceCard.tsx+4-5
......@@ -560,9 +560,9 @@ export function ProviderInstanceCard({
560560 <TooltipTrigger
561561 render={
562562 <Button
563 size="icon-xs"
563 size="icon-micro"
564564 variant="ghost"
565 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-destructive"
565 className="text-muted-foreground hover:text-destructive"
566566 onClick={onDelete}
567567 aria-label={`Delete provider instance ${instanceId}`}
568568 >
......@@ -708,9 +708,8 @@ export function ProviderInstanceCard({
708708 </div>
709709 <div className="flex w-full shrink-0 items-center gap-2 sm:w-auto sm:justify-end">
710710 <Button
711 size="sm"
712 variant="ghost"
713 className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
711 size="compact"
712 variant="ghost-muted"
714713 onClick={() => onExpandedChange(!isExpanded)}
715714 aria-label={`Toggle ${displayName} details`}
716715 >
apps/web/src/components/settings/ProviderModelsSection.tsx+13-20
......@@ -245,9 +245,9 @@ export function ProviderModelsSection({
245245 <TooltipTrigger
246246 render={
247247 <Button
248 size="icon-xs"
248 size="icon-micro"
249249 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"
251251 aria-label={`Details for ${model.name}`}
252252 />
253253 }
......@@ -282,12 +282,9 @@ export function ProviderModelsSection({
282282 <TooltipTrigger
283283 render={
284284 <Button
285 size="icon-xs"
286 variant="ghost"
287 className={cn(
288 "size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground",
289 isFavorite && "text-yellow-500 hover:text-yellow-600",
290 )}
285 size="icon-micro"
286 variant="ghost-muted"
287 className={cn(isFavorite && "text-yellow-500 hover:text-yellow-600")}
291288 onClick={() => handleToggleFavorite(model.slug)}
292289 aria-label={`${isFavorite ? "Remove" : "Add"} ${model.name} ${
293290 isFavorite ? "from" : "to"
......@@ -305,9 +302,8 @@ export function ProviderModelsSection({
305302 <TooltipTrigger
306303 render={
307304 <Button
308 size="icon-xs"
309 variant="ghost"
310 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
305 size="icon-micro"
306 variant="ghost-muted"
311307 disabled={!canMoveUp}
312308 onClick={() => handleMove(model.slug, -1)}
313309 aria-label={`Move ${model.name} up`}
......@@ -322,9 +318,8 @@ export function ProviderModelsSection({
322318 <TooltipTrigger
323319 render={
324320 <Button
325 size="icon-xs"
326 variant="ghost"
327 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
321 size="icon-micro"
322 variant="ghost-muted"
328323 disabled={!canMoveDown}
329324 onClick={() => handleMove(model.slug, 1)}
330325 aria-label={`Move ${model.name} down`}
......@@ -340,9 +335,8 @@ export function ProviderModelsSection({
340335 <TooltipTrigger
341336 render={
342337 <Button
343 size="icon-xs"
344 variant="ghost"
345 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
338 size="icon-micro"
339 variant="ghost-muted"
346340 onClick={() => handleToggleHidden(model.slug)}
347341 aria-label={`${isHidden ? "Show" : "Hide"} ${model.name}`}
348342 />
......@@ -364,9 +358,8 @@ export function ProviderModelsSection({
364358 <TooltipTrigger
365359 render={
366360 <Button
367 size="icon-xs"
368 variant="ghost"
369 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
361 size="icon-micro"
362 variant="ghost-muted"
370363 aria-label={`Remove ${model.slug}`}
371364 onClick={() => handleRemove(model.slug)}
372365 />
apps/web/src/components/settings/ProviderSettingsPanel.tsx+4-6
......@@ -670,9 +670,8 @@ export function EnvironmentProviderSettings({
670670 <TooltipTrigger
671671 render={
672672 <Button
673 size="icon-xs"
674 variant="ghost"
675 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
673 size="icon-micro"
674 variant="ghost-muted"
676675 onClick={() => setIsAddInstanceDialogOpen(true)}
677676 aria-label="Add provider instance"
678677 >
......@@ -686,9 +685,8 @@ export function EnvironmentProviderSettings({
686685 <TooltipTrigger
687686 render={
688687 <Button
689 size="icon-xs"
690 variant="ghost"
691 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
688 size="icon-micro"
689 variant="ghost-muted"
692690 disabled={isRefreshingProviders}
693691 onClick={() => void refreshProviders()}
694692 aria-label="Refresh provider status"
apps/web/src/components/settings/ResourceTelemetryDiagnostics.tsx+6-8
......@@ -486,14 +486,14 @@ function ProcessTreeName({
486486 style={{ paddingLeft: `${Math.min(process.depth, 7) * 10}px` }}
487487 >
488488 {hasChildren ? (
489 <button
490 type="button"
491 className="cursor-pointer inline-flex size-5 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
489 <Button
490 size="icon-micro"
491 variant="ghost-muted"
492492 onClick={() => onToggle(process)}
493493 aria-label={collapsed ? `Expand ${name}` : `Collapse ${name}`}
494494 >
495495 <ChevronIcon className="size-3.5" />
496 </button>
496 </Button>
497497 ) : (
498498 <span className="size-5" aria-hidden />
499499 )}
......@@ -975,9 +975,8 @@ export function ResourceTelemetryDiagnostics() {
975975 <TooltipTrigger
976976 render={
977977 <Button
978 size="icon-xs"
978 size="icon-micro"
979979 variant="ghost"
980 className="size-5 rounded-sm p-0"
981980 disabled={telemetry.isPending}
982981 onClick={telemetry.refresh}
983982 aria-label="Refresh resource telemetry"
......@@ -1227,9 +1226,8 @@ export function ResourceTelemetryDiagnostics() {
12271226 <div className="flex items-center gap-2">
12281227 <HistoryWindowSelector selectedWindowMs={windowMs} onSelect={setWindowMs} />
12291228 <Button
1230 size="icon-xs"
1229 size="icon-micro"
12311230 variant="ghost"
1232 className="size-5 rounded-sm p-0"
12331231 disabled={history.isPending}
12341232 onClick={history.refresh}
12351233 aria-label="Refresh resource history"
apps/web/src/components/settings/SettingsSidebarNav.tsx+2-2
......@@ -220,9 +220,9 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
220220 {isSearching ? (
221221 <Button
222222 type="button"
223 size="icon-xs"
223 size="icon-micro"
224224 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"
226226 aria-label="Clear settings search"
227227 onClick={() => {
228228 clearSearch();
apps/web/src/components/settings/SourceControlSettings.tsx+14-31
......@@ -1,4 +1,4 @@
1import { ChevronDownIcon, GitPullRequestIcon, InfoIcon, RefreshCwIcon } from "lucide-react";
1import { ChevronDownIcon, GitPullRequestIcon, RefreshCwIcon } from "lucide-react";
22import * as Duration from "effect/Duration";
33import * as Option from "effect/Option";
44import { useState, type ReactNode } from "react";
......@@ -43,6 +43,7 @@ import {
4343} from "../ui/number-field";
4444import { Switch } from "../ui/switch";
4545import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
46
4647import {
4748 AzureDevOpsIcon,
4849 BitbucketIcon,
......@@ -54,7 +55,12 @@ import {
5455} from "../Icons";
5556import { RedactedSensitiveText } from "./RedactedSensitiveText";
5657import { SourceControlWritingSettingsSection } from "./SourceControlWritingSettings";
57import { SettingResetButton, SettingsPageContainer, SettingsSection } from "./settingsLayout";
58import {
59 PolicyTooltip,
60 SettingResetButton,
61 SettingsPageContainer,
62 SettingsSection,
63} from "./settingsLayout";
5864import { searchableSetting } from "./settingsSearch";
5965
6066const EMPTY_DISCOVERY_RESULT: SourceControlDiscoveryResult = {
......@@ -116,27 +122,6 @@ function backgroundActivityOverrideSettings(
116122 };
117123}
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
140125function optionLabel(value: Option.Option<string>): string | null {
141126 return Option.getOrNull(value);
142127}
......@@ -316,9 +301,8 @@ function DiscoveryItemRow({
316301 <div className="flex w-full shrink-0 items-center gap-2 sm:w-auto sm:justify-end">
317302 {hasDetails ? (
318303 <Button
319 size="sm"
320 variant="ghost"
321 className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
304 size="compact"
305 variant="ghost-muted"
322306 onClick={() => setIsExpanded((open) => !open)}
323307 aria-expanded={isExpanded}
324308 aria-label={`Toggle ${item.label} details`}
......@@ -367,11 +351,11 @@ function GitFetchIntervalSettings() {
367351 <div className="min-w-0 space-y-1">
368352 <div className="flex min-w-0 items-center gap-1">
369353 <span className="text-xs font-medium text-foreground">Fetch interval</span>
370 <BackgroundPolicyTooltip>
354 <PolicyTooltip>
371355 This interval is configured for Git only. The shared Background activity policy still
372356 decides whether Git refreshes may run when the timer fires. Custom intervals appear as
373357 Advanced in General settings.
374 </BackgroundPolicyTooltip>
358 </PolicyTooltip>
375359 <span
376360 className={cn(
377361 "inline-flex size-5 shrink-0 items-center justify-center transition-opacity",
......@@ -537,9 +521,8 @@ export function SourceControlSettingsPanel() {
537521 <TooltipTrigger
538522 render={
539523 <Button
540 size="icon-xs"
541 variant="ghost"
542 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
524 size="icon-micro"
525 variant="ghost-muted"
543526 onClick={handleScan}
544527 disabled={discovery.isPending}
545528 aria-label="Rescan server environment"
apps/web/src/components/settings/ThemeImportDialog.tsx+4-4
......@@ -79,13 +79,13 @@ function highlightJson(value: string): string {
7979 const index = match.index ?? 0;
8080 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))]";
8383 if (token.startsWith('"')) {
8484 tokenClass = /^\s*:/.test(value.slice(index + token.length))
85 ? "theme-json-key"
86 : "theme-json-string";
85 ? "text-[var(--app-theme-accent,var(--color-blue-600))]"
86 : "text-[var(--app-theme-message-action,var(--color-emerald-600))]";
8787 } 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))]";
8989 }
9090 highlighted += `<span class="${tokenClass}">${escapeJsonHtml(token)}</span>`;
9191 cursor = index + token.length;
apps/web/src/components/settings/ThemeSearchSection.tsx+17-13
......@@ -30,7 +30,7 @@ import {
3030 AlertDialogTitle,
3131} from "../ui/alert-dialog";
3232import { Button } from "../ui/button";
33import { Input } from "../ui/input";
33import { InputGroup, InputGroupAddon, InputGroupInput } from "../ui/input-group";
3434import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
3535import { Spinner } from "../ui/spinner";
3636
......@@ -202,20 +202,24 @@ export function ThemeSearchSection({
202202 Find open-source themes from Open VSX.
203203 </p>
204204 </div>
205 <form className="relative 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" />
207 <Input
208 aria-label="Search Open VSX themes"
209 className="rounded-xl [&_input]:pl-9"
210 autoFocus
211 onChange={(event) => setQuery(event.currentTarget.value)}
212 placeholder="try dracula, nord, catppuccin..."
213 size="lg"
214 type="search"
215 value={query}
216 />
205 <form className="flex gap-2" onSubmit={handleSearch}>
206 <InputGroup>
207 <InputGroupAddon>
208 <SearchIcon />
209 </InputGroupAddon>
210 <InputGroupInput
211 aria-label="Search Open VSX themes"
212 autoFocus
213 onChange={(event) => setQuery(event.currentTarget.value)}
214 placeholder="try dracula, nord, catppuccin..."
215 size="lg"
216 type="search"
217 value={query}
218 />
219 </InputGroup>
217220 <Button
218221 disabled={!query.trim() || isSearching || installingId !== null}
222 size="lg"
219223 type="submit"
220224 variant="outline"
221225 >
apps/web/src/components/settings/settingsLayout.tsx+8-10
......@@ -90,13 +90,9 @@ export function PolicyTooltip({ children }: { readonly children: string }) {
9090 <TooltipTrigger
9191 delay={200}
9292 render={
93 <button
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 >
93 <Button size="icon-micro" variant="ghost-muted" aria-label="Background policy details">
9894 <InfoIcon className="size-3.5" />
99 </button>
95 </Button>
10096 }
10197 />
10298 <TooltipPopup side="top" className="max-w-72">
......@@ -216,11 +212,10 @@ export function SettingResetButton({
216212 <TooltipTrigger
217213 render={
218214 <Button
219 size="icon-xs"
220 variant="ghost"
215 size="icon-micro"
216 variant="ghost-muted"
221217 aria-label={`Reset ${label} to default`}
222218 disabled={disabled}
223 className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"
224219 onClick={(event) => {
225220 event.stopPropagation();
226221 onClick();
......@@ -251,7 +246,10 @@ export function SettingsPageContainer({
251246
252247 return (
253248 <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 >
255253 <div className={cn("mx-auto flex w-full max-w-4xl flex-col gap-12", className)}>
256254 {children}
257255 </div>
apps/web/src/components/sidebar/SidebarChrome.tsx+1-1
......@@ -83,7 +83,7 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) {
8383 <Link
8484 aria-label="Go to threads"
8585 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",
8787 onBackdrop ? "text-white" : "text-foreground",
8888 )}
8989 to="/"
apps/web/src/components/sidebar/SidebarProviderUpdatePill.tsx+7-5
......@@ -10,6 +10,7 @@ import {
1010 type ProviderUpdateSidebarPillView,
1111} from "../ProviderUpdateLaunchNotification.logic";
1212import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
13import { Button } from "../ui/button";
1314
1415const PROVIDER_UPDATE_PILL_STYLES = {
1516 loading:
......@@ -151,7 +152,7 @@ export function SidebarProviderUpdatePill() {
151152 <div
152153 key={displayedView.key}
153154 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)] ${
155156 PROVIDER_UPDATE_PILL_PROGRESS_STYLES[displayedView.tone]
156157 }`}
157158 style={
......@@ -190,14 +191,15 @@ export function SidebarProviderUpdatePill() {
190191 <Tooltip>
191192 <TooltipTrigger
192193 render={
193 <button
194 type="button"
194 <Button
195 size="icon-micro"
196 variant="ghost"
195197 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"
197199 onClick={() => startExit(displayedView.key, null, displayedView.key)}
198200 >
199201 <XIcon className="size-3.5" />
200 </button>
202 </Button>
201203 }
202204 />
203205 <TooltipPopup side="top">Dismiss until provider status changes</TooltipPopup>
apps/web/src/components/threadSidebarWidth.test.ts+6-13
......@@ -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.
22import * as NodeFS from "node:fs";
33
44import { describe, expect, it } from "vite-plus/test";
......@@ -36,20 +36,13 @@ describe("thread sidebar width", () => {
3636 });
3737
3838 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");
40 const desktopHeaderStyles = sidebarStyles.slice(
41 sidebarStyles.indexOf("@media (min-width: 48rem)"),
42 sidebarStyles.indexOf("/* Stage-channel sidebar art"),
39 const sidebarSource = NodeFS.readFileSync(
40 new URL("./sidebar/SidebarChrome.tsx", import.meta.url),
41 "utf8",
4342 );
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;/);
49 expect(desktopHeaderStyles).toMatch(
50 /@media \(min-width: 48rem\) \{\s*\.sidebar-brand \{\s*display: flex;/,
51 );
44 expect(sidebarSource).toContain("hidden h-7 w-fit min-w-0 shrink-0 items-center gap-1");
45 expect(sidebarSource).toContain("md:flex");
5246 expect(THREAD_SIDEBAR_MIN_WIDTH).toBe(13 * 16);
53 expect(Number(stageLabelThreshold) * 16).toBeGreaterThan(THREAD_SIDEBAR_MIN_WIDTH);
5447 });
5548});
apps/web/src/components/ui/button.test.tsx+15
......@@ -28,4 +28,19 @@ describe("button geometry tokens", () => {
2828 expect(html).toContain("size-7");
2929 expect(html).toContain("sm:size-6");
3030 });
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 });
3146});
apps/web/src/components/ui/button.tsx+8
......@@ -16,9 +16,13 @@ const buttonVariants = cva(
1616 },
1717 variants: {
1818 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",
1921 default: "h-9 px-[calc(--spacing(3)-1px)] sm:h-8",
2022 icon: "size-9 sm:size-8",
2123 "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",
2226 "icon-sm": "size-8 sm:size-7",
2327 "icon-xl":
2428 "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(
3842 "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",
3943 ghost:
4044 "[--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",
4149 link: "border-transparent underline-offset-4 [:hover,[data-pressed]]:underline",
4250 outline:
4351 "[--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({
170170 >
171171 <span
172172 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%)]",
174174 className,
175175 )}
176176 >
apps/web/src/components/ui/input-group.tsx+2-2
......@@ -8,7 +8,7 @@ import { Input, type InputProps } from "~/components/ui/input";
88import { Textarea, type TextareaProps } from "~/components/ui/textarea";
99
1010const 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)]",
1212 {
1313 defaultVariants: {
1414 variant: "default",
......@@ -16,7 +16,7 @@ const inputGroupVariants = cva(
1616 variants: {
1717 variant: {
1818 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%)]",
2020 ghost:
2121 "border-transparent bg-transparent shadow-none hover:bg-muted/40 has-[input:focus-visible,textarea:focus-visible]:bg-background",
2222 },
apps/web/src/components/ui/input.tsx+5-1
......@@ -6,7 +6,7 @@ import type * as React from "react";
66import { cn } from "~/lib/utils";
77
88type InputProps = Omit<InputPrimitive.Props & React.RefAttributes<HTMLInputElement>, "size"> & {
9 size?: "sm" | "default" | "lg" | number;
9 size?: "sm" | "compact" | "default" | "lg" | number;
1010 unstyled?: boolean;
1111 nativeInput?: boolean;
1212};
......@@ -20,6 +20,7 @@ function Input({
2020}: InputProps) {
2121 const inputClassName = cn(
2222 "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",
2324 size === "sm" && "h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5",
2425 size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5",
2526 props.type === "search" &&
......@@ -59,6 +60,9 @@ function Input({
5960 cn(
6061 !unstyled &&
6162 "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)]",
6266 className,
6367 ) || undefined
6468 }
apps/web/src/components/ui/menu.tsx+9-1
......@@ -36,6 +36,13 @@ function MenuPopup({
3636 side?: MenuPrimitive.Positioner.Props["side"];
3737 anchor?: MenuPrimitive.Positioner.Props["anchor"];
3838}) {
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
3946 return (
4047 <MenuPrimitive.Portal>
4148 <MenuPrimitive.Positioner
......@@ -49,7 +56,8 @@ function MenuPopup({
4956 >
5057 <MenuPrimitive.Popup
5158 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",
5361 className,
5462 )}
5563 data-slot="menu-popup"
apps/web/src/components/ui/popover.tsx+2
......@@ -52,6 +52,8 @@ function PopoverPopup({
5252 "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%)]",
5353 tooltipStyle &&
5454 "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%)]",
5557 className,
5658 )}
5759 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";
44
55import { 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
724function ScrollArea({
825 className,
926 children,
......@@ -71,4 +88,4 @@ function ScrollBar({
7188 );
7289}
7390
74export { ScrollArea, ScrollBar };
91export { getVirtualizedScrollFadeClassName, ScrollArea, ScrollBar };
apps/web/src/components/ui/select.tsx+3-1
......@@ -26,6 +26,8 @@ const selectTriggerVariants = cva(
2626 "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",
2727 },
2828 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",
2931 default: "min-h-9 px-[calc(--spacing(3)-1px)] sm:min-h-8",
3032 lg: "min-h-10 px-[calc(--spacing(3)-1px)] sm:min-h-9",
3133 sm: "min-h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:min-h-7",
......@@ -150,7 +152,7 @@ function SelectPopup({
150152 </SelectPrimitive.ScrollUpArrow>
151153 <div
152154 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%)]",
154156 matchTriggerWidth && "min-w-(--anchor-width)",
155157 popupClassName,
156158 )}
apps/web/src/components/ui/sidebar.tsx+2
......@@ -166,6 +166,8 @@ function SidebarProvider({
166166 {
167167 "--sidebar-width": SIDEBAR_WIDTH,
168168 "--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))",
169171 ...style,
170172 } as React.CSSProperties
171173 }
apps/web/src/components/ui/skeleton.tsx+1-1
......@@ -4,7 +4,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
44 return (
55 <div
66 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%)]",
88 className,
99 )}
1010 data-slot="skeleton"
apps/web/src/components/ui/toast.tsx+13-14
......@@ -26,7 +26,7 @@ import {
2626} from "lucide-react";
2727
2828import { cn } from "~/lib/utils";
29import { buttonVariants } from "~/components/ui/button";
29import { Button, buttonVariants } from "~/components/ui/button";
3030import { useComposerDraftStore } from "~/composerDraftStore";
3131import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
3232import { resolveThreadRouteTarget } from "~/threadRoutes";
......@@ -124,11 +124,12 @@ function CopyErrorButton({ text }: { text: string }) {
124124 <Tooltip>
125125 <TooltipTrigger
126126 render={
127 <button
127 <Button
128 size="icon-micro"
129 variant="ghost-muted"
128130 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"
130132 onClick={() => copyToClipboard(text)}
131 type="button"
132133 />
133134 }
134135 >
......@@ -381,24 +382,22 @@ function ToastBodyContent({
381382 >
382383 {copyErrorText !== null ? <CopyErrorButton text={copyErrorText} /> : null}
383384 {additionalActions.map(({ id, props: { className, ...props } }) => (
384 <button
385 <Button
385386 {...props}
386 className={cn(
387 buttonVariants({ size: "xs", variant: secondaryActionVariant }),
388 className,
389 )}
387 className={className}
390388 key={id}
389 size="xs"
391390 type="button"
391 variant={secondaryActionVariant}
392392 />
393393 ))}
394394 {secondaryActionProps ? (
395 <button
395 <Button
396396 {...secondaryActionRest}
397 className={cn(
398 buttonVariants({ size: "xs", variant: secondaryActionVariant }),
399 secondaryActionClassName,
400 )}
397 className={secondaryActionClassName}
398 size="xs"
401399 type="button"
400 variant={secondaryActionVariant}
402401 />
403402 ) : null}
404403 {actionProps ? (
apps/web/src/components/ui/toggle.tsx+2
......@@ -14,6 +14,8 @@ const toggleVariants = cva(
1414 },
1515 variants: {
1616 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",
1719 default: "h-9 min-w-9 px-[calc(--spacing(2)-1px)] sm:h-8 sm:min-w-8",
1820 lg: "h-10 min-w-10 px-[calc(--spacing(2.5)-1px)] sm:h-9 sm:min-w-9",
1921 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 {
2020 makeWindow,
2121} from "@t3tools/shared/usageFormat";
2222import { ScrollArea } from "../ui/scroll-area";
23import { Button } from "../ui/button";
2324import { SidebarInset } from "../ui/sidebar";
2425import { WorkspaceBreadcrumb, WorkspaceBreadcrumbItem } from "../WorkspaceBreadcrumb";
2526import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../../workspaceTitlebar";
......@@ -106,7 +107,7 @@ export function UsagePage() {
106107 {!isElectron && (
107108 <header
108109 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",
110111 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
111112 )}
112113 >
......@@ -156,14 +157,14 @@ export function UsagePage() {
156157 </button>
157158 ))}
158159 </div>
159 <button
160 type="button"
160 <Button
161 size="icon"
162 variant="outline"
161163 onClick={refreshWindow}
162164 aria-label="Refresh usage"
163 className="cursor-pointer rounded-md border border-border p-2 text-muted-foreground hover:text-foreground"
164165 >
165166 <RefreshCwIcon className="size-3.5" />
166 </button>
167 </Button>
167168 </div>
168169 </div>
169170
apps/web/src/index.css+352-749
......@@ -1,6 +1,7 @@
11@import "tailwindcss";
22
33@custom-variant dark (&:is(.dark, .dark *));
4@custom-variant light (&:not(.dark, .dark *));
45/* Window Controls Overlay: active when Electron exposes native titlebar control geometry. */
56@custom-variant wco (&:is(.wco, .wco *));
67
......@@ -102,20 +103,13 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
102103 --workspace-native-controls-inset: 0px;
103104 --workspace-titlebar-control-size: 1.75rem;
104105 --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"] {
115 --workspace-titlebar-content-left: calc(
116 var(--workspace-controls-left) + var(--workspace-titlebar-control-size) +
117 var(--workspace-titlebar-control-gap)
118 );
107 @variant 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 }
119113}
120114
121115.wco {
......@@ -264,6 +258,179 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
264258 }
265259}
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
267434@layer base {
268435 :root {
269436 /* 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
292459 --stage-night-glow-highlight: oklch(0.553749 0.176543 271.958);
293460 --stage-night-glow-secondary: oklch(0.345571 0.117466 273.568);
294461 --stage-night-sparkle: oklch(0.880867 0.057747 269.011);
295 }
296462
297 .dark {
298 --stage-art-top: oklch(0.581473 0.149124 256.9);
299 --stage-art-mid: oklch(0.456509 0.159377 261.945);
300 --stage-art-bottom: oklch(0.291327 0.136578 267.649);
463 @variant dark {
464 --stage-art-top: oklch(0.581473 0.149124 256.9);
465 --stage-art-mid: oklch(0.456509 0.159377 261.945);
466 --stage-art-bottom: oklch(0.291327 0.136578 267.649);
467 }
301468 }
302469
303470 * {
......@@ -311,63 +478,16 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
311478 ):focus-visible {
312479 @apply outline-none ring-0;
313480 }
314 html {
481 html,
482 body {
315483 background-color: var(--app-chrome-background);
316484 }
317485 body {
318486 @apply text-foreground relative;
319 background-color: var(--app-chrome-background);
320487 }
321488}
322489
323490@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
371491 /* Each maintainer palette gives the same line art its own material: rose
372492 vellum, forest drafting paper, marine cyanotype, copper, and violet ink.
373493 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
380500 --stage-art-secondary: oklch(0.763402 0.163836 352.525);
381501 --stage-art-tertiary: oklch(0.70819 0.180285 311.949);
382502 --stage-art-line: oklch(0.952158 0.034194 336.179);
383 }
384503
385 html.dark[data-theme-id="t3-chat"] {
386 --stage-art-top: oklch(0.540689 0.143665 347.587);
387 --stage-art-mid: oklch(0.396586 0.126592 347.6);
388 --stage-art-bottom: oklch(0.249959 0.079694 340.523);
389 --stage-art-highlight: oklch(0.921297 0.051708 343.229);
390 --stage-art-secondary: oklch(0.667398 0.165674 352.549);
391 --stage-art-tertiary: oklch(0.609315 0.163722 306.315);
392 --stage-art-line: oklch(0.945349 0.036045 341.433);
504 @variant dark {
505 --stage-art-top: oklch(0.540689 0.143665 347.587);
506 --stage-art-mid: oklch(0.396586 0.126592 347.6);
507 --stage-art-bottom: oklch(0.249959 0.079694 340.523);
508 --stage-art-highlight: oklch(0.921297 0.051708 343.229);
509 --stage-art-secondary: oklch(0.667398 0.165674 352.549);
510 --stage-art-tertiary: oklch(0.609315 0.163722 306.315);
511 --stage-art-line: oklch(0.945349 0.036045 341.433);
512 }
393513 }
394514
395515 html[data-theme-id="grove"] {
......@@ -407,23 +527,23 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
407527 --stage-night-secondary: oklch(0.665652 0.109731 156.599);
408528 --stage-night-tertiary: oklch(0.698651 0.103024 89.828);
409529 --stage-night-line: oklch(0.945336 0.041923 157.222);
410 }
411530
412 html.dark[data-theme-id="grove"] {
413 --stage-art-top: oklch(0.58719 0.09869 157.426);
414 --stage-art-mid: oklch(0.454979 0.079031 159.756);
415 --stage-art-bottom: oklch(0.297856 0.050355 161.167);
416 --stage-art-highlight: oklch(0.952407 0.053872 158.44);
417 --stage-art-secondary: oklch(0.732591 0.120606 155.853);
418 --stage-art-tertiary: oklch(0.716282 0.116547 80.563);
419 --stage-art-line: oklch(0.961577 0.035285 157.03);
420 --stage-night-top: oklch(0.398632 0.065534 158.601);
421 --stage-night-mid: oklch(0.290561 0.049694 160.456);
422 --stage-night-bottom: oklch(0.210147 0.03173 169.818);
423 --stage-night-highlight: oklch(0.866303 0.057526 156.796);
424 --stage-night-secondary: oklch(0.586553 0.093722 157.365);
425 --stage-night-tertiary: oklch(0.6364 0.101769 82.985);
426 --stage-night-line: oklch(0.913292 0.035718 156.976);
531 @variant dark {
532 --stage-art-top: oklch(0.58719 0.09869 157.426);
533 --stage-art-mid: oklch(0.454979 0.079031 159.756);
534 --stage-art-bottom: oklch(0.297856 0.050355 161.167);
535 --stage-art-highlight: oklch(0.952407 0.053872 158.44);
536 --stage-art-secondary: oklch(0.732591 0.120606 155.853);
537 --stage-art-tertiary: oklch(0.716282 0.116547 80.563);
538 --stage-art-line: oklch(0.961577 0.035285 157.03);
539 --stage-night-top: oklch(0.398632 0.065534 158.601);
540 --stage-night-mid: oklch(0.290561 0.049694 160.456);
541 --stage-night-bottom: oklch(0.210147 0.03173 169.818);
542 --stage-night-highlight: oklch(0.866303 0.057526 156.796);
543 --stage-night-secondary: oklch(0.586553 0.093722 157.365);
544 --stage-night-tertiary: oklch(0.6364 0.101769 82.985);
545 --stage-night-line: oklch(0.913292 0.035718 156.976);
546 }
427547 }
428548
429549 html[data-theme-id="ocean"] {
......@@ -434,16 +554,16 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
434554 --stage-art-secondary: oklch(0.788391 0.090856 215.684);
435555 --stage-art-tertiary: oklch(0.76441 0.099607 187.893);
436556 --stage-art-line: oklch(0.976025 0.019647 212.543);
437 }
438557
439 html.dark[data-theme-id="ocean"] {
440 --stage-art-top: oklch(0.59663 0.089167 233.427);
441 --stage-art-mid: oklch(0.461094 0.084904 243.478);
442 --stage-art-bottom: oklch(0.294818 0.05947 250.526);
443 --stage-art-highlight: oklch(0.952907 0.032224 221.27);
444 --stage-art-secondary: oklch(0.732079 0.09296 224.414);
445 --stage-art-tertiary: oklch(0.720885 0.095495 190.903);
446 --stage-art-line: oklch(0.961039 0.027355 219.756);
558 @variant dark {
559 --stage-art-top: oklch(0.59663 0.089167 233.427);
560 --stage-art-mid: oklch(0.461094 0.084904 243.478);
561 --stage-art-bottom: oklch(0.294818 0.05947 250.526);
562 --stage-art-highlight: oklch(0.952907 0.032224 221.27);
563 --stage-art-secondary: oklch(0.732079 0.09296 224.414);
564 --stage-art-tertiary: oklch(0.720885 0.095495 190.903);
565 --stage-art-line: oklch(0.961039 0.027355 219.756);
566 }
447567 }
448568
449569 html[data-theme-id="ember"] {
......@@ -461,23 +581,23 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
461581 --stage-night-secondary: oklch(0.641705 0.126508 44.376);
462582 --stage-night-tertiary: oklch(0.538694 0.129931 25.865);
463583 --stage-night-line: oklch(0.926348 0.046029 58.73);
464 }
465584
466 html.dark[data-theme-id="ember"] {
467 --stage-art-top: oklch(0.597533 0.120694 43.455);
468 --stage-art-mid: oklch(0.437763 0.101287 34.86);
469 --stage-art-bottom: oklch(0.264269 0.055858 26.548);
470 --stage-art-highlight: oklch(0.929214 0.042638 55.801);
471 --stage-art-secondary: oklch(0.705592 0.137369 43.176);
472 --stage-art-tertiary: oklch(0.629583 0.158322 24.088);
473 --stage-art-line: oklch(0.945058 0.033906 58.824);
474 --stage-night-top: oklch(0.392352 0.081287 36.444);
475 --stage-night-mid: oklch(0.271305 0.056352 31.135);
476 --stage-night-bottom: oklch(0.182126 0.028154 27.774);
477 --stage-night-highlight: oklch(0.851007 0.061294 53.805);
478 --stage-night-secondary: oklch(0.560789 0.10645 42.953);
479 --stage-night-tertiary: oklch(0.476228 0.106656 24.165);
480 --stage-night-line: oklch(0.884931 0.046607 56.556);
585 @variant dark {
586 --stage-art-top: oklch(0.597533 0.120694 43.455);
587 --stage-art-mid: oklch(0.437763 0.101287 34.86);
588 --stage-art-bottom: oklch(0.264269 0.055858 26.548);
589 --stage-art-highlight: oklch(0.929214 0.042638 55.801);
590 --stage-art-secondary: oklch(0.705592 0.137369 43.176);
591 --stage-art-tertiary: oklch(0.629583 0.158322 24.088);
592 --stage-art-line: oklch(0.945058 0.033906 58.824);
593 --stage-night-top: oklch(0.392352 0.081287 36.444);
594 --stage-night-mid: oklch(0.271305 0.056352 31.135);
595 --stage-night-bottom: oklch(0.182126 0.028154 27.774);
596 --stage-night-highlight: oklch(0.851007 0.061294 53.805);
597 --stage-night-secondary: oklch(0.560789 0.10645 42.953);
598 --stage-night-tertiary: oklch(0.476228 0.106656 24.165);
599 --stage-night-line: oklch(0.884931 0.046607 56.556);
600 }
481601 }
482602
483603 html[data-theme-id="iris"] {
......@@ -488,16 +608,16 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
488608 --stage-art-secondary: oklch(0.745085 0.125892 298.647);
489609 --stage-art-tertiary: oklch(0.73066 0.167815 340.964);
490610 --stage-art-line: oklch(0.960278 0.024064 306.969);
491 }
492611
493 html.dark[data-theme-id="iris"] {
494 --stage-art-top: oklch(0.57297 0.145973 295.185);
495 --stage-art-mid: oklch(0.419499 0.13752 292.131);
496 --stage-art-bottom: oklch(0.274235 0.095798 286.608);
497 --stage-art-highlight: oklch(0.916698 0.047206 300.224);
498 --stage-art-secondary: oklch(0.670994 0.13095 296.689);
499 --stage-art-tertiary: oklch(0.679357 0.165376 340.439);
500 --stage-art-line: oklch(0.940582 0.032921 299.076);
612 @variant dark {
613 --stage-art-top: oklch(0.57297 0.145973 295.185);
614 --stage-art-mid: oklch(0.419499 0.13752 292.131);
615 --stage-art-bottom: oklch(0.274235 0.095798 286.608);
616 --stage-art-highlight: oklch(0.916698 0.047206 300.224);
617 --stage-art-secondary: oklch(0.670994 0.13095 296.689);
618 --stage-art-tertiary: oklch(0.679357 0.165376 340.439);
619 --stage-art-line: oklch(0.940582 0.032921 299.076);
620 }
501621 }
502622
503623 :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
538658 --stage-night-sparkle: var(--stage-night-line);
539659 }
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
600661 @keyframes settings-search-target-pulse {
601662 0%,
602663 100% {
......@@ -607,56 +668,29 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
607668 }
608669 }
609670
610 .settings-page-scroll-fade div.settings-search-target-pulse,
611 .settings-page-scroll-fade section.settings-search-target-pulse > div:first-child {
671 [data-settings-page-scroll] div.settings-search-target-pulse,
672 [data-settings-page-scroll] section.settings-search-target-pulse > div:first-child {
612673 animation: settings-search-target-pulse 650ms ease-in-out 2;
613674 border-radius: 0.75rem;
614675 }
615676
616677 /* The pulse is the destination indicator; without it (reduced motion), the
617678 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 {
619680 outline: none;
620681 }
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
654683 .chat-composer-glass-shell {
655684 --chat-composer-glass-surface: var(--card);
656685 --chat-composer-outline: rgb(0 0 0 / 8%);
657686
658 position: relative;
659687 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 }
660694 }
661695
662696 .chat-composer-glass-shell::before {
......@@ -716,8 +750,15 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
716750 }
717751
718752 .chat-composer-glass-host {
719 position: relative;
720753 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 }
721762 }
722763
723764 .chat-composer-glass-host::after {
......@@ -747,6 +788,21 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
747788 .chat-composer-context-strip {
748789 position: relative;
749790 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 }
750806 }
751807
752808 .chat-composer-context-strip::before {
......@@ -762,33 +818,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
762818 content: "";
763819 }
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
792821 @supports not (clip-path: shape(from 0 0, line to 1px 1px)) {
793822 .chat-composer-glass-shell-with-context::before {
794823 inset-block-end: var(--chat-composer-context-extension);
......@@ -806,106 +835,23 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
806835 backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation));
807836 }
808837
809 .dark .chat-composer-context-strip::before {
810 background:
811 linear-gradient(
812 to bottom,
813 transparent 0 1rem,
814 rgb(0 0 0 / 18%) 1rem,
815 transparent calc(1rem + 10px)
816 ),
817 linear-gradient(rgb(255 255 255 / 2%), rgb(255 255 255 / 2%)),
818 color-mix(in srgb, var(--chat-composer-glass-surface) var(--glass-opacity), transparent);
838 .chat-composer-context-strip {
839 @variant dark {
840 &::before {
841 background:
842 linear-gradient(
843 to bottom,
844 transparent 0 1rem,
845 rgb(0 0 0 / 18%) 1rem,
846 transparent calc(1rem + 10px)
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 }
819852 }
820853 }
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
909855 .settings-slider {
910856 --settings-slider-progress: 0%;
911857 --settings-slider-fill-offset: 0.5rem;
......@@ -1021,32 +967,10 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
1021967 }
1022968 }
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
1036970 @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
1042971 .chat-composer-glass-shell::before {
1043972 background: var(--chat-composer-glass-surface);
1044973 }
1045
1046 .dialog-glass,
1047 .dropdown-glass {
1048 background: var(--popover) !important;
1049 }
1050974 }
1051975}
1052976
......@@ -1152,15 +1076,12 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
11521076 --terminal-foreground: var(--foreground);
11531077 --terminal-cursor: rgb(38 56 78);
11541078 --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
11581080 @variant dark {
11591081 color-scheme: dark;
11601082 /* Keep the workspace in the same neutral-black family as sidebar v2.
11611083 Surfaces lift from this base instead of starting from a milky gray. */
11621084 --background: var(--color-neutral-950);
1163 --app-chrome-background: var(--background);
11641085 --surface-raised: var(--secondary);
11651086 --foreground: var(--color-neutral-100);
11661087 --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
11681089 --popover: color-mix(in srgb, var(--background) 94%, var(--color-white));
11691090 --popover-foreground: var(--color-neutral-100);
11701091 --primary: oklch(0.571 0.21 264);
1171 --primary-foreground: var(--color-white);
11721092 --secondary: --alpha(var(--color-white) / 4%);
11731093 --secondary-foreground: var(--color-neutral-100);
11741094 --muted: --alpha(var(--color-white) / 4%);
11751095 --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));
11841096 --accent: --alpha(var(--color-white) / 4%);
11851097 --accent-foreground: var(--color-neutral-100);
11861098 --error: color-mix(in srgb, var(--color-red-500) 90%, var(--color-white));
11871099 --error-foreground: var(--color-red-400);
11881100 --error-surface: color-mix(in srgb, var(--error) 16%, transparent);
1189 --destructive: var(--error);
11901101 --border: --alpha(var(--color-white) / 6%);
11911102 --input: --alpha(var(--color-white) / 8%);
1192 --ring: var(--primary);
1193 --destructive-foreground: var(--error-foreground);
1194 --info: var(--color-blue-500);
11951103 --info-foreground: var(--color-blue-400);
1196 --success: var(--color-emerald-500);
11971104 --success-foreground: var(--color-emerald-400);
1198 --warning: var(--color-amber-500);
11991105 --warning-foreground: var(--color-amber-400);
12001106 --warning-surface: color-mix(in srgb, var(--warning) 16%, transparent);
1201 --update: var(--primary);
12021107 --update-foreground: var(--color-blue-400);
12031108 --update-surface: color-mix(in srgb, var(--update) 18%, transparent);
12041109 --sidebar: var(--card);
1205 --sidebar-foreground: var(--foreground);
1206 --sidebar-muted-foreground: var(--muted-foreground);
12071110 --sidebar-control-surface: var(--muted);
12081111 --sidebar-row-hover: var(--accent);
12091112 --sidebar-row-active: var(--accent);
12101113 --sidebar-row-selected: var(--muted);
1211 --sidebar-border: var(--border);
12121114 --sidebar-stage-fade: var(--card);
1213 --terminal-background: var(--background);
1214 --terminal-foreground: var(--foreground);
12151115 --terminal-cursor: rgb(180 203 255);
12161116 --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%);
12191117 }
12201118}
12211119
......@@ -1242,32 +1140,28 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
12421140 --sidebar-row-selected: var(--color-white);
12431141 --sidebar-border: var(--color-zinc-200);
12441142 --sidebar-stage-fade: var(--sidebar);
1245 background-color: var(--sidebar);
1246}
1247
1248.dark [data-app-sidebar] {
1249 --background: #000;
1250 --foreground: #f1f3f7;
1251 --card: #000;
1252 --card-foreground: var(--foreground);
1253 --accent: #191a1d;
1254 --accent-foreground: #f7f9ff;
1255 --muted: #0a0a0a;
1256 --muted-foreground: #a3a3a3;
1257 --border: rgb(255 255 255 / 8%);
1258 --input: rgb(255 255 255 / 18%);
1259 --sidebar: var(--card);
1260 --sidebar-foreground: var(--foreground);
1261 --sidebar-muted-foreground: var(--muted-foreground);
1262 --sidebar-control-surface: var(--muted);
1263 --sidebar-row-hover: color-mix(in srgb, var(--foreground) 8%, transparent);
1264 --sidebar-row-active: color-mix(in srgb, var(--foreground) 11%, transparent);
1265 --sidebar-row-selected: color-mix(in srgb, var(--foreground) 7%, transparent);
1266 --sidebar-border: var(--border);
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);
1143
1144 @variant dark {
1145 --background: #000;
1146 --foreground: #f1f3f7;
1147 --card: #000;
1148 --card-foreground: var(--foreground);
1149 --accent: #191a1d;
1150 --accent-foreground: #f7f9ff;
1151 --muted: #0a0a0a;
1152 --muted-foreground: #a3a3a3;
1153 --border: rgb(255 255 255 / 8%);
1154 --input: rgb(255 255 255 / 18%);
1155 --sidebar: var(--card);
1156 --sidebar-foreground: var(--foreground);
1157 --sidebar-muted-foreground: var(--muted-foreground);
1158 --sidebar-control-surface: var(--muted);
1159 --sidebar-row-hover: color-mix(in srgb, var(--foreground) 8%, transparent);
1160 --sidebar-row-active: color-mix(in srgb, var(--foreground) 11%, transparent);
1161 --sidebar-row-selected: color-mix(in srgb, var(--foreground) 7%, transparent);
1162 --sidebar-border: var(--border);
1163 --sidebar-stage-fade: var(--card);
1164 }
12711165}
12721166
12731167/* 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
12751169 compatibility overrides so both navigation implementations receive the
12761170 same palette. Success, info, provider, and channel identity colors remain
12771171 independent; error, warning, and update roles are themeable below. */
1278html[data-theme-id],
1279html.dark[data-theme-id] {
1172html[data-theme-id] {
12801173 --background: var(--app-theme-canvas);
12811174 --app-chrome-background: var(--app-theme-chrome);
12821175 --toolbar-background: var(--app-theme-toolbar);
......@@ -1340,8 +1233,6 @@ html.dark[data-theme-id] {
13401233 --terminal-foreground: var(--app-theme-terminal-foreground);
13411234 --terminal-cursor: var(--app-theme-terminal-cursor);
13421235 --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);
13451236}
13461237
13471238/* T3 Chat's composer is a translucent lift over --chat-background. Route its
......@@ -1349,35 +1240,21 @@ html.dark[data-theme-id] {
13491240 another tint from the canvas, which made the dark composer too red. */
13501241html[data-theme-id] .chat-composer-glass-shell {
13511242 --chat-composer-glass-surface: var(--app-theme-surface-raised);
1352}
1353
1354html[data-theme-id]:not(.dark) .chat-composer-glass-shell {
13551243 --chat-composer-outline: var(--app-theme-toolbar-border);
1356}
13571244
1358html.dark[data-theme-id] .chat-composer-glass-shell {
1359 --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);
1245 @variant dark {
1246 --chat-composer-outline: color-mix(in srgb, var(--app-theme-input) 30%, var(--background));
1247 --chat-composer-highlight: color-mix(in srgb, var(--app-theme-input) 12%, transparent);
1248 }
13611249}
13621250
1363html.dark[data-theme-id="t3-chat"] .chat-composer-glass-shell {
1251html[data-theme-id="t3-chat"] .chat-composer-glass-shell {
13641252 /* T3 Chat's visible composer edge is a dark plum, not the stock translucent
13651253 white outline. Its highlight is derived from --chat-input-gradient. */
1366 --chat-composer-outline: #241e28;
1367 --chat-composer-highlight: color-mix(in srgb, #432d48 12%, transparent);
1368}
1369
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);
1254 @variant dark {
1255 --chat-composer-outline: #241e28;
1256 --chat-composer-highlight: color-mix(in srgb, #432d48 12%, transparent);
1257 }
13811258}
13821259
13831260/* Theme-token dependency probes are restored synchronously, before paint. Keep
......@@ -1477,8 +1354,8 @@ html[data-theme-id] [data-chat-header] [data-toolbar-control] {
14771354 toggle's when the trigger renders the toggle, so match both. */
14781355html[data-theme-id] [data-panel-layout-controls] [data-slot="toggle"],
14791356html[data-theme-id] [data-panel-layout-controls] [data-slot="tooltip-trigger"],
1480html[data-theme-id] .workspace-titlebar-controls [data-slot="toggle"],
1481html[data-theme-id] .workspace-titlebar-controls [data-slot="tooltip-trigger"] {
1357html[data-theme-id] [data-workspace-titlebar-controls] [data-slot="toggle"],
1358html[data-theme-id] [data-workspace-titlebar-controls] [data-slot="tooltip-trigger"] {
14821359 --control-icon-color: var(--toolbar-foreground);
14831360 color: var(--toolbar-foreground);
14841361}
......@@ -1522,19 +1399,23 @@ html[data-theme-id] .chat-markdown .chat-markdown-chrome-action {
15221399/* T3 Chat renders inline code and compact chat artifacts with its translucent
15231400 secondary surface flattened over the light chat canvas. The raw muted and
15241401 secondary tokens are substantially darker than those visible pixels. */
1525html[data-theme-id="t3-chat"]:not(.dark) .chat-markdown :not(pre) > code,
1526html[data-theme-id="t3-chat"]:not(.dark) [data-changed-files-state],
1527html[data-theme-id="t3-chat"]:not(.dark) [data-changed-files-header] {
1528 background-color: var(--message-surface);
1529}
1402html[data-theme-id="t3-chat"] {
1403 @variant light {
1404 & .chat-markdown :not(pre) > code,
1405 & [data-changed-files-state],
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,
1532html[data-theme-id="t3-chat"]:not(.dark) [data-changed-files-state] {
1533 border-color: transparent;
1534}
1410 & .chat-markdown :not(pre) > code,
1411 & [data-changed-files-state] {
1412 border-color: transparent;
1413 }
15351414
1536html[data-theme-id="t3-chat"]:not(.dark) .chat-markdown :not(pre) > code {
1537 color: var(--message-foreground);
1415 & .chat-markdown :not(pre) > code {
1416 color: var(--message-foreground);
1417 }
1418 }
15381419}
15391420
15401421html[data-theme-id] .chat-markdown .chat-markdown-chrome-action:hover,
......@@ -1562,40 +1443,19 @@ html[data-theme-id] [data-app-sidebar] {
15621443 --sidebar-row-selected: var(--app-theme-sidebar-row-selected);
15631444 --sidebar-border: var(--app-theme-sidebar-border);
15641445 --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] {
15721446 border-color: color-mix(in srgb, var(--sidebar-foreground) 10%, transparent);
1573}
15741447
1575html.dark[data-theme-id] [data-app-sidebar] {
1576 border-color: color-mix(in srgb, var(--sidebar-foreground) 8%, transparent);
1448 @variant dark {
1449 border-color: color-mix(in srgb, var(--sidebar-foreground) 8%, transparent);
1450 }
15771451}
15781452
15791453/* T3 Chat's panel divider is deliberately pink, and its resize affordance
15801454 keeps that color while hovered. Do not neutralize this branded edge. */
1581html.dark[data-theme-id="t3-chat"] [data-app-sidebar] {
1582 border-color: var(--sidebar-border);
1583}
1584
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));
1455html[data-theme-id="t3-chat"] [data-app-sidebar] {
1456 @variant dark {
1457 border-color: var(--sidebar-border);
1458 }
15991459}
16001460
16011461body {
......@@ -1715,125 +1575,7 @@ code {
17151575 background: var(--app-scrollbar-thumb-hover);
17161576}
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
18311578/* Chat markdown rendering */
1832.chat-markdown {
1833 min-width: 0;
1834 overflow-wrap: anywhere;
1835 word-break: break-word;
1836}
18371579
18381580.chat-markdown > :first-child {
18391581 margin-top: 0;
......@@ -1945,18 +1687,6 @@ label:has(> select#reasoning-effort) select {
19451687 background-size: 4px 2px;
19461688}
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
19601690.chat-markdown blockquote {
19611691 border-left: 2px solid var(--border);
19621692 padding-left: 0.8rem;
......@@ -2001,11 +1731,7 @@ label:has(> select#reasoning-effort) select {
20011731 font-size: 0.75rem;
20021732}
20031733
2004.chat-markdown a.chat-markdown-file-link {
2005 color: var(--foreground);
2006 text-decoration: none;
2007}
2008
1734.chat-markdown a.chat-markdown-file-link,
20091735.chat-markdown a.chat-markdown-file-link:hover {
20101736 color: var(--foreground);
20111737 text-decoration: none;
......@@ -2023,75 +1749,26 @@ label:has(> select#reasoning-effort) select {
20231749 border-radius: 0.75rem;
20241750 background: var(--muted);
20251751 padding: 0.8rem 0.9rem;
1752 scrollbar-width: thin;
1753 scrollbar-color: color-mix(in srgb, var(--border) 78%, transparent) transparent;
20261754}
20271755
20281756.chat-markdown pre code {
20291757 border: none;
20301758 background: transparent;
20311759 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;
20381760}
20391761
20401762.chat-markdown pre::-webkit-scrollbar {
20411763 height: 7px;
20421764}
20431765
2044.chat-markdown pre::-webkit-scrollbar-track {
2045 background: transparent;
2046}
2047
20481766.chat-markdown pre::-webkit-scrollbar-thumb {
20491767 border-radius: 999px;
20501768 background: color-mix(in srgb, var(--border) 78%, transparent);
20511769}
20521770
2053.markdown-file-link-tooltip-scroll {
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
1771.chat-markdown .chat-markdown-codeblock-header,
20951772.chat-markdown .chat-markdown-chrome-action {
20961773 color: color-mix(in srgb, var(--foreground) 72%, transparent);
20971774}
......@@ -2167,13 +1844,6 @@ label:has(> select#reasoning-effort) select {
21671844 overflow-wrap: anywhere;
21681845}
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
21771847/* Prompt-stash save acknowledgement: the new count fades up from just below
21781848 its resting position, once, then stops. One-shot and event-driven (React
21791849 remounts the element by key on each stash) — no continuous animation. */
......@@ -2188,19 +1858,6 @@ label:has(> select#reasoning-effort) select {
21881858 }
21891859}
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
22041861@keyframes provider-update-pill-countdown {
22051862 from {
22061863 transform: scaleX(1);
......@@ -2210,23 +1867,6 @@ label:has(> select#reasoning-effort) select {
22101867 }
22111868}
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,
22301870.diff-render-surface diffs-container {
22311871 border: 0;
22321872 border-radius: 0;
......@@ -2307,40 +1947,3 @@ label:has(> select#reasoning-effort) select {
23071947.ultrathink-chroma {
23081948 animation: ultrathink-chroma-shift 10s linear infinite;
23091949}
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// @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.
23import * as NodeFS from "node:fs";
34
45import { describe, expect, it } from "vite-plus/test";
......@@ -14,7 +15,9 @@ describe("hosted static onboarding header", () => {
1415
1516 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");
1821 expect(onboardingHeader).not.toMatch(/(?:^|\s)(?:[\w-]+:)*py-/);
1922 });
2023});
apps/web/src/routes/_chat.index.tsx+1-1
......@@ -145,7 +145,7 @@ function HostedStaticOnboardingState() {
145145 <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">
146146 <header
147147 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",
149149 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
150150 )}
151151 >
apps/web/src/routes/_chat.pull-requests.tsx+4-3
......@@ -1308,7 +1308,8 @@ function PullRequestsRouteView() {
13081308 // anchor the thread view's controls and the sidebar trigger use, so
13091309 // every titlebar cluster in the app sits one shared inset from its
13101310 // 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
13121313 >
13131314 {panelToggleControls}
13141315 </div>
......@@ -1828,7 +1829,7 @@ function PullRequestsColumn({
18281829 <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background">
18291830 <header
18301831 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",
18321833 // A closed right panel leaves this column full-width, so its header runs
18331834 // underneath the native window controls on Windows; reserve the inset the
18341835 // way Settings and the chat view do. While the panel is open the column
......@@ -1903,7 +1904,7 @@ function PullRequestsColumn({
19031904
19041905 <div
19051906 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]"
19071908 >
19081909 {/* The top padding is the fade band's own height (1.5rem here), the same pairing the
19091910 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() {
7575 {!isElectron && (
7676 <header
7777 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",
7979 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
8080 )}
8181 >
apps/web/src/terminal/ghostty/surface.ts+5-4
......@@ -578,8 +578,7 @@ export class GhosttyTerminalSurface {
578578 options: GhosttyTerminalSurfaceOptions,
579579 ): Promise<GhosttyTerminalSurface> {
580580 const canvas = document.createElement("canvas");
581 canvas.className = "t3-ghostty-canvas";
582 canvas.style.cssText = "display:block;width:100%;height:100%;";
581 canvas.className = "block size-full cursor-text";
583582 canvas.setAttribute("aria-hidden", "true");
584583
585584 const input = document.createElement("textarea");
......@@ -592,14 +591,16 @@ export class GhosttyTerminalSurface {
592591 "position:absolute;left:4px;top:4px;width:1px;height:1px;opacity:0;padding:0;border:0;resize:none;pointer-events:none;";
593592
594593 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";
596596 scrollbar.setAttribute("role", "scrollbar");
597597 scrollbar.setAttribute("aria-label", "Terminal scrollback");
598598 scrollbar.setAttribute("aria-orientation", "vertical");
599599 scrollbar.tabIndex = 0;
600600 scrollbar.hidden = true;
601601 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)]";
603604 scrollbar.append(scrollbarThumb);
604605 mount.replaceChildren(canvas, input, scrollbar);
605606