authorgravatar for 91240327+shivamhwp@users.noreply.github.comshivam <91240327+shivamhwp@users.noreply.github.com> 2026-08-01 01:41:28+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-31 16:11:28-04:00
log03adf215ceebedeb9c5e813406154dd8353b36f6
tree8b2d322e6291d54e48e2617763e843d45691512b
parent916cff733cd00c85fe0419df5bcb9372a6384746
signature Signed by PGP key B5690EEEBB952194

follow-up normalization after #4700. (#4498)

Co-authored-by: T3 Code Test <t3code-test@example.com>

12 files changed, 181 insertions(+), 23 deletions(-)

apps/web/src/components/ProjectScriptsControl.test.tsx created+65
...@@ -0,0 +1,65 @@
1import type { ProjectScript, ResolvedKeybindingsConfig } from "@t3tools/contracts";
2import { renderToStaticMarkup } from "react-dom/server";
3import { describe, expect, it } from "vite-plus/test";
4
5import ProjectScriptsControl from "./ProjectScriptsControl";
6
7const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = [];
8const PRIMARY_SCRIPT: ProjectScript = {
9 id: "dev",
10 name: "Dev",
11 command: "vp dev",
12 icon: "play",
13 runOnWorktreeCreate: false,
14};
15
16function renderControl(scripts: ReadonlyArray<ProjectScript>) {
17 return renderToStaticMarkup(
18 <ProjectScriptsControl
19 scripts={scripts}
20 keybindings={EMPTY_KEYBINDINGS}
21 onRunScript={() => {}}
22 onAddScript={async () => undefined as never}
23 onUpdateScript={async () => undefined as never}
24 onDeleteScript={async () => undefined as never}
25 />,
26 );
27}
28
29function buttonTag(html: string, ariaLabel: string) {
30 return html.match(new RegExp(`<button[^>]*aria-label="${ariaLabel}"[^>]*>`))?.[0];
31}
32
33function expectResponsiveXsControl(markup: string | undefined) {
34 expect(markup).toBeDefined();
35 expect(markup).toContain("h-7");
36 expect(markup).toContain("gap-1");
37 expect(markup).toContain("text-sm");
38 expect(markup).toContain("sm:h-6");
39 expect(markup).toContain("sm:text-xs");
40 expect(markup).toContain("w-7");
41 expect(markup).toContain("px-0");
42 expect(markup).toContain("sm:w-6");
43 expect(markup).toContain("@3xl/header-actions:w-auto!");
44 expect(markup).toContain("@3xl/header-actions:px-[calc(--spacing(2)-1px)]");
45}
46
47describe("ProjectScriptsControl compact controls", () => {
48 it("keeps the primary Run control compact and expands it with its label", () => {
49 const html = renderControl([PRIMARY_SCRIPT]);
50
51 expectResponsiveXsControl(buttonTag(html, "Run Dev"));
52 expect(html).toContain(
53 'class="sr-only @3xl/header-actions:not-sr-only @3xl/header-actions:ml-0.5"',
54 );
55 });
56
57 it("keeps the standalone Add control compact and expands it with its label", () => {
58 const html = renderControl([]);
59
60 expectResponsiveXsControl(buttonTag(html, "Add action"));
61 expect(html).toContain(
62 'class="sr-only @3xl/header-actions:not-sr-only @3xl/header-actions:ml-0.5"',
63 );
64 });
65});
apps/web/src/components/ProjectScriptsControl.tsx+8-1
...@@ -341,6 +341,7 @@ export default function ProjectScriptsControl({...@@ -341,6 +341,7 @@ export default function ProjectScriptsControl({
341 <Button341 <Button
342 size="xs"342 size="xs"
343 variant="outline"343 variant="outline"
344 className="w-7 px-0 sm:w-6 @3xl/header-actions:w-auto! @3xl/header-actions:px-[calc(--spacing(2)-1px)]"
344 aria-label={`Run ${primaryScript.name}`}345 aria-label={`Run ${primaryScript.name}`}
345 onClick={() => onRunScript(primaryScript)}346 onClick={() => onRunScript(primaryScript)}
346 />347 />
...@@ -441,7 +442,13 @@ export default function ProjectScriptsControl({...@@ -441,7 +442,13 @@ export default function ProjectScriptsControl({
441 <Tooltip>442 <Tooltip>
442 <TooltipTrigger443 <TooltipTrigger
443 render={444 render={
444 <Button size="xs" variant="outline" aria-label="Add action" onClick={openAddDialog} />445 <Button
446 size="xs"
447 variant="outline"
448 className="w-7 px-0 sm:w-6 @3xl/header-actions:w-auto! @3xl/header-actions:px-[calc(--spacing(2)-1px)]"
449 aria-label="Add action"
450 onClick={openAddDialog}
451 />
445 }452 }
446 >453 >
447 <PlusIcon className="size-3.5" />454 <PlusIcon className="size-3.5" />
apps/web/src/components/SidebarV2.tsx+5-5
...@@ -261,9 +261,9 @@ function SidebarV2ThreadTooltip({...@@ -261,9 +261,9 @@ function SidebarV2ThreadTooltip({
261 align="start"261 align="start"
262 sideOffset={4}262 sideOffset={4}
263 variant="glass"263 variant="glass"
264 className="max-w-80 text-left whitespace-normal"264 className="max-w-80 text-left whitespace-normal [&_[data-slot=tooltip-viewport]]:p-0"
265 >265 >
266 <div className="flex min-w-0 max-w-80 flex-col gap-2 px-0.5 py-1.5">266 <div className="flex min-w-0 max-w-80 flex-col gap-2 p-[var(--floating-content-inset)]">
267 <div className="min-w-0 truncate text-xs leading-none font-medium text-foreground">267 <div className="min-w-0 truncate text-xs leading-none font-medium text-foreground">
268 {thread.title}268 {thread.title}
269 </div>269 </div>
...@@ -918,7 +918,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {...@@ -918,7 +918,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
918 />918 />
919 }919 }
920 >920 >
921 <div className="relative z-10 h-[4.875rem] px-2.5 py-2">921 <div className="relative z-10 h-[4.875rem] px-[var(--sidebar-row-content-inset)] py-[var(--sidebar-content-inset)]">
922 <div className="flex h-5 min-w-0 items-center gap-1.5">922 <div className="flex h-5 min-w-0 items-center gap-1.5">
923 <ProjectFavicon923 <ProjectFavicon
924 environmentId={thread.environmentId}924 environmentId={thread.environmentId}
...@@ -2410,7 +2410,7 @@ export default function SidebarV2() {...@@ -2410,7 +2410,7 @@ export default function SidebarV2() {
2410 <SidebarContent2410 <SidebarContent
2411 className="gap-0"2411 className="gap-0"
2412 fixedHeader={2412 fixedHeader={
2413 <SidebarGroup className="gap-1 p-2">2413 <SidebarGroup className="gap-1 p-[var(--sidebar-content-inset)]">
2414 <div className="flex items-center gap-1">2414 <div className="flex items-center gap-1">
2415 <div className="min-w-0 flex-1">2415 <div className="min-w-0 flex-1">
2416 <CommandDialogTrigger2416 <CommandDialogTrigger
...@@ -2558,7 +2558,7 @@ export default function SidebarV2() {...@@ -2558,7 +2558,7 @@ export default function SidebarV2() {
2558 </SidebarGroup>2558 </SidebarGroup>
2559 }2559 }
2560 >2560 >
2561 <SidebarGroup className="px-2 pb-1 pt-0">2561 <SidebarGroup className="px-[var(--sidebar-content-inset)] pb-1 pt-0">
2562 <TooltipProvider2562 <TooltipProvider
2563 key="sidebar-thread-tooltips-150"2563 key="sidebar-thread-tooltips-150"
2564 delay={150}2564 delay={150}
apps/web/src/components/settings/SettingsSidebarNav.tsx+2-2
...@@ -75,7 +75,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -75,7 +75,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
75 return (75 return (
76 <>76 <>
77 <SidebarContent className="overflow-x-hidden">77 <SidebarContent className="overflow-x-hidden">
78 <SidebarGroup className="p-2">78 <SidebarGroup className="p-[var(--sidebar-content-inset)]">
79 <SidebarMenu>79 <SidebarMenu>
80 {SETTINGS_NAV_ITEMS.map((item) => {80 {SETTINGS_NAV_ITEMS.map((item) => {
81 const Icon = item.icon;81 const Icon = item.icon;
...@@ -95,7 +95,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -95,7 +95,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
95 </SidebarMenu>95 </SidebarMenu>
96 </SidebarGroup>96 </SidebarGroup>
97 </SidebarContent>97 </SidebarContent>
98 <SidebarFooter className="p-2">98 <SidebarFooter className="p-[var(--sidebar-content-inset)]">
99 <T3ConnectSidebarSignIn />99 <T3ConnectSidebarSignIn />
100 <div className="flex items-center gap-1">100 <div className="flex items-center gap-1">
101 <SidebarMenu className="min-w-0 flex-1">101 <SidebarMenu className="min-w-0 flex-1">
apps/web/src/components/sidebar/SidebarChrome.tsx+1-1
...@@ -119,7 +119,7 @@ export const SidebarChromeFooter = memo(function SidebarChromeFooter() {...@@ -119,7 +119,7 @@ export const SidebarChromeFooter = memo(function SidebarChromeFooter() {
119 }, [isMobile, navigate, setOpenMobile]);119 }, [isMobile, navigate, setOpenMobile]);
120120
121 return (121 return (
122 <SidebarFooter className="p-2">122 <SidebarFooter className="p-[var(--sidebar-content-inset)]">
123 <SidebarProviderUpdatePill />123 <SidebarProviderUpdatePill />
124 <SidebarUpdatePill />124 <SidebarUpdatePill />
125 <SidebarMenu>125 <SidebarMenu>
apps/web/src/components/ui/button.test.tsx created+31
...@@ -0,0 +1,31 @@
1import { renderToStaticMarkup } from "react-dom/server";
2import { describe, expect, it } from "vite-plus/test";
3import { FlaskConicalIcon } from "lucide-react";
4
5import { Button } from "./button";
6
7describe("button geometry tokens", () => {
8 it("uses the shared control radius and an opaque semantic icon color", () => {
9 const html = renderToStaticMarkup(
10 <Button size="icon-xs" variant="outline" aria-label="Run tests">
11 <FlaskConicalIcon />
12 </Button>,
13 );
14
15 expect(html).toContain("rounded-[var(--control-radius)]");
16 expect(html).toContain("[--control-icon-color:var(--muted-foreground)]");
17 expect(html).toContain("text-[var(--control-icon-color)]");
18 expect(html).not.toContain("opacity-80");
19 });
20
21 it("keeps compact icon buttons square at every breakpoint", () => {
22 const html = renderToStaticMarkup(
23 <Button size="icon-xs" aria-label="Add action">
24 <span>+</span>
25 </Button>,
26 );
27
28 expect(html).toContain("size-7");
29 expect(html).toContain("sm:size-6");
30 });
31});
apps/web/src/components/ui/button.tsx+5-5
...@@ -8,7 +8,7 @@ import type * as React from "react";...@@ -8,7 +8,7 @@ import type * as React from "react";
8import { cn } from "~/lib/utils";8import { cn } from "~/lib/utils";
99
10const buttonVariants = cva(10const buttonVariants = cva(
11 "[&_svg]:-mx-0.5 relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border font-medium text-base outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 sm:text-sm [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",11 "[--control-icon-color:currentColor] [&_svg]:-mx-0.5 relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-[var(--control-radius)] border font-medium text-base outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--control-radius)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 sm:text-sm [&_svg:not([class*='text-'])]:text-[var(--control-icon-color)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
12 {12 {
13 defaultVariants: {13 defaultVariants: {
14 size: "default",14 size: "default",
...@@ -23,11 +23,11 @@ const buttonVariants = cva(...@@ -23,11 +23,11 @@ const buttonVariants = cva(
23 "icon-xl":23 "icon-xl":
24 "size-11 sm:size-10 [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",24 "size-11 sm:size-10 [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",
25 "icon-xs":25 "icon-xs":
26 "size-7 rounded-md before:rounded-[calc(var(--radius-md)-1px)] sm:size-6 not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-4 sm:not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-3.5",26 "size-7 sm:size-6 not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-4 sm:not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-3.5",
27 lg: "h-10 px-[calc(--spacing(3.5)-1px)] sm:h-9",27 lg: "h-10 px-[calc(--spacing(3.5)-1px)] sm:h-9",
28 sm: "h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:h-7",28 sm: "h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:h-7",
29 xl: "h-11 px-[calc(--spacing(4)-1px)] text-lg sm:h-10 sm:text-base [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",29 xl: "h-11 px-[calc(--spacing(4)-1px)] text-lg sm:h-10 sm:text-base [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",
30 xs: "h-7 gap-1 rounded-md px-[calc(--spacing(2)-1px)] text-sm before:rounded-[calc(var(--radius-md)-1px)] sm:h-6 sm:text-xs [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5",30 xs: "h-7 gap-1 px-[calc(--spacing(2)-1px)] text-sm sm:h-6 sm:text-xs [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5",
31 },31 },
32 variant: {32 variant: {
33 default:33 default:
...@@ -37,10 +37,10 @@ const buttonVariants = cva(...@@ -37,10 +37,10 @@ const buttonVariants = cva(
37 "destructive-outline":37 "destructive-outline":
38 "border-input bg-popover not-dark:bg-clip-padding text-destructive-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:border-destructive/32 [:hover,[data-pressed]]:bg-destructive/4",38 "border-input bg-popover not-dark:bg-clip-padding text-destructive-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:border-destructive/32 [:hover,[data-pressed]]:bg-destructive/4",
39 ghost:39 ghost:
40 "border-transparent text-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent [&_svg:not([class*='text-'])]:text-muted-foreground",40 "[--control-icon-color:var(--muted-foreground)] border-transparent text-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent",
41 link: "border-transparent underline-offset-4 [:hover,[data-pressed]]:underline",41 link: "border-transparent underline-offset-4 [:hover,[data-pressed]]:underline",
42 outline:42 outline:
43 "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 [&_svg:not([class*='text-'])]:text-muted-foreground",43 "[--control-icon-color:var(--muted-foreground)] border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-accent/50 dark:[:hover,[data-pressed]]:bg-input/64",
44 secondary:44 secondary:
45 "border-transparent bg-secondary text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80 [:hover,[data-pressed]]:bg-secondary/90",45 "border-transparent bg-secondary text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80 [:hover,[data-pressed]]:bg-secondary/90",
46 },46 },
apps/web/src/components/ui/command.test.tsx created+31
...@@ -0,0 +1,31 @@
1import { renderToStaticMarkup } from "react-dom/server";
2import { describe, expect, it } from "vite-plus/test";
3
4import { Command, CommandFooter, CommandInput } from "./command";
5
6describe("command compact geometry", () => {
7 it("keeps shell selectors on the wrapper and direct-input padding on AutocompleteInput", () => {
8 const html = renderToStaticMarkup(
9 <Command>
10 <CommandInput placeholder="Search commands" />
11 </Command>,
12 );
13 const shellClass = html.match(/class="([^"]*px-\[var\(--command-shell-inset\)[^"]*)"/)?.[1];
14 const inputClass = html.match(/class="([^"]*has-focus-visible:ring-0[^"]*)"/)?.[1];
15
16 expect(shellClass).toContain(
17 "[&amp;_[data-slot=autocomplete-start-addon]]:ps-[calc(var(--command-shell-inset)+0.0625rem)]",
18 );
19 expect(shellClass).not.toContain("sm:*:data-[slot=autocomplete-input]");
20 expect(inputClass).toContain(
21 "sm:*:data-[slot=autocomplete-input]:ps-[calc(var(--command-shell-inset)+1.5rem)]!",
22 );
23 });
24
25 it("uses the semantic footer inset without changing compact vertical padding", () => {
26 const html = renderToStaticMarkup(<CommandFooter>Shortcuts</CommandFooter>);
27
28 expect(html).toContain("px-[var(--command-content-inset)]");
29 expect(html).toContain("py-2.5");
30 });
31});
apps/web/src/components/ui/command.tsx+8-3
...@@ -103,11 +103,16 @@ function CommandInput({...@@ -103,11 +103,16 @@ function CommandInput({
103 wrapperClassName?: string | undefined;103 wrapperClassName?: string | undefined;
104}) {104}) {
105 return (105 return (
106 <div className={cn("px-1.25 py-1.5", wrapperClassName)}>106 <div
107 className={cn(
108 "px-[var(--command-shell-inset)] py-1.5 [&_[data-slot=autocomplete-start-addon]]:ps-[calc(var(--command-shell-inset)+0.0625rem)]",
109 wrapperClassName,
110 )}
111 >
107 <AutocompleteInput112 <AutocompleteInput
108 autoFocus113 autoFocus
109 className={cn(114 className={cn(
110 "border-transparent! bg-transparent! shadow-none before:hidden has-focus-visible:ring-0 placeholder:text-muted-foreground/80 *:data-[slot=autocomplete-input]:ps-9! sm:*:data-[slot=autocomplete-input]:ps-8.5!",115 "border-transparent! bg-transparent! shadow-none before:hidden has-focus-visible:ring-0 placeholder:text-muted-foreground/80 *:data-[slot=autocomplete-input]:ps-9! sm:*:data-[slot=autocomplete-input]:ps-[calc(var(--command-shell-inset)+1.5rem)]!",
111 className,116 className,
112 )}117 )}
113 placeholder={placeholder}118 placeholder={placeholder}
...@@ -211,7 +216,7 @@ function CommandFooter({ className, ...props }: React.ComponentProps<"div">) {...@@ -211,7 +216,7 @@ function CommandFooter({ className, ...props }: React.ComponentProps<"div">) {
211 return (216 return (
212 <div217 <div
213 className={cn(218 className={cn(
214 "relative flex items-center justify-between gap-2 rounded-b-[calc(var(--radius-2xl)-1px)] bg-foreground/[0.025] px-4 py-2.5 font-medium text-sm text-muted-foreground [&_[data-slot=kbd-group]]:font-sans [&_[data-slot=kbd]]:bg-foreground/[0.08] [&_[data-slot=kbd]]:text-foreground [&_[data-slot=kbd]]:ring-0",219 "relative flex items-center justify-between gap-2 rounded-b-[calc(var(--radius-2xl)-1px)] bg-foreground/[0.025] px-[var(--command-content-inset)] py-2.5 font-medium text-sm text-muted-foreground [&_[data-slot=kbd-group]]:font-sans [&_[data-slot=kbd]]:bg-foreground/[0.08] [&_[data-slot=kbd]]:text-foreground [&_[data-slot=kbd]]:ring-0",
215 className,220 className,
216 )}221 )}
217 data-slot="command-footer"222 data-slot="command-footer"
apps/web/src/components/ui/sidebar.test.tsx+5-2
...@@ -55,12 +55,15 @@ describe("sidebar interactive cursors", () => {...@@ -55,12 +55,15 @@ describe("sidebar interactive cursors", () => {
5555
56 expect(html).toContain('data-slot="sidebar-menu-button"');56 expect(html).toContain('data-slot="sidebar-menu-button"');
57 expect(html).toContain("h-8");57 expect(html).toContain("h-8");
58 expect(html).toContain("rounded-md");58 expect(html).toContain("rounded-[var(--control-radius)]");
59 expect(html).toContain("px-2");59 expect(html).toContain("px-[var(--sidebar-row-content-inset)]");
60 expect(html).toContain("py-1.5");60 expect(html).toContain("py-1.5");
61 expect(html).toContain("]:size-4");61 expect(html).toContain("]:size-4");
62 expect(html).toContain("]:shrink-0");62 expect(html).toContain("]:shrink-0");
63 expect(html).toContain("cursor-pointer");63 expect(html).toContain("cursor-pointer");
64 expect(html).toContain("gap-[var(--sidebar-control-gap)]");
65 expect(html).toContain("text-[var(--sidebar-icon-color)]");
66 expect(html).not.toContain("[&amp;&gt;svg]:opacity-60");
64 });67 });
6568
66 it("applies the shared default treatment to icon-only menu buttons", () => {69 it("applies the shared default treatment to icon-only menu buttons", () => {
apps/web/src/components/ui/sidebar.tsx+8-4
...@@ -712,7 +712,10 @@ function SidebarContent({...@@ -712,7 +712,10 @@ function SidebarContent({
712function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {712function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
713 return (713 return (
714 <div714 <div
715 className={cn("relative flex w-full min-w-0 flex-col p-2", className)}715 className={cn(
716 "relative flex w-full min-w-0 flex-col p-[var(--sidebar-content-inset)]",
717 className,
718 )}
716 data-sidebar="group"719 data-sidebar="group"
717 data-slot="sidebar-group"720 data-slot="sidebar-group"
718 {...props}721 {...props}
...@@ -792,7 +795,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {...@@ -792,7 +795,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
792}795}
793796
794const sidebarMenuButtonVariants = cva(797const sidebarMenuButtonVariants = cva(
795 "peer/menu-button flex w-full cursor-pointer items-center gap-2 overflow-hidden text-left outline-hidden ring-ring transition-[width,height,padding] hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 active:bg-sidebar-row-active active:text-sidebar-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pe-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-row-selected data-[active=true]:font-medium data-[active=true]:text-sidebar-foreground data-[state=open]:hover:bg-sidebar-row-hover data-[state=open]:hover:text-sidebar-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-muted-foreground [&>svg]:opacity-60 hover:[&>svg]:text-sidebar-foreground hover:[&>svg]:opacity-100 active:[&>svg]:text-sidebar-foreground active:[&>svg]:opacity-100 data-[active=true]:[&>svg]:text-sidebar-foreground data-[active=true]:[&>svg]:opacity-100",798 "peer/menu-button flex w-full cursor-pointer items-center gap-[var(--sidebar-control-gap)] overflow-hidden text-left outline-hidden ring-ring transition-[width,height,padding] hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 active:bg-sidebar-row-active active:text-sidebar-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pe-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-row-selected data-[active=true]:font-medium data-[active=true]:text-sidebar-foreground data-[state=open]:hover:bg-sidebar-row-hover data-[state=open]:hover:text-sidebar-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-[var(--sidebar-content-inset)]! [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0 [&>svg]:text-[var(--sidebar-icon-color)] hover:[&>svg]:text-sidebar-foreground active:[&>svg]:text-sidebar-foreground data-[active=true]:[&>svg]:text-sidebar-foreground",
796 {799 {
797 defaultVariants: {800 defaultVariants: {
798 size: "default",801 size: "default",
...@@ -800,8 +803,9 @@ const sidebarMenuButtonVariants = cva(...@@ -800,8 +803,9 @@ const sidebarMenuButtonVariants = cva(
800 },803 },
801 variants: {804 variants: {
802 size: {805 size: {
803 default: "h-8 rounded-md px-2.5 py-1.5 text-sm",806 default:
804 icon: "size-8 justify-center rounded-md p-0",807 "h-8 rounded-[var(--control-radius)] px-[var(--sidebar-row-content-inset)] py-1.5 text-sm",
808 icon: "size-8 justify-center rounded-[var(--control-radius)] p-0",
805 lg: "h-12 rounded-lg p-2 text-sm group-data-[collapsible=icon]:p-0!",809 lg: "h-12 rounded-lg p-2 text-sm group-data-[collapsible=icon]:p-0!",
806 sm: "h-7 rounded-lg p-2 text-xs",810 sm: "h-7 rounded-lg p-2 text-xs",
807 },811 },
apps/web/src/index.css+12
...@@ -79,6 +79,18 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -79,6 +79,18 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
79 --app-scrollbar-width: 6px;79 --app-scrollbar-width: 6px;
80 --app-scrollbar-thumb: rgb(217 217 217);80 --app-scrollbar-thumb: rgb(217 217 217);
81 --app-scrollbar-thumb-hover: rgb(191 191 191);81 --app-scrollbar-thumb-hover: rgb(191 191 191);
82 /*
83 * Compact UI geometry. Keep these values semantic so sidebar, palette,
84 * tooltip, and toolbar controls cannot quietly drift apart.
85 */
86 --control-radius: 0.5rem;
87 --sidebar-content-inset: 0.5rem;
88 --sidebar-control-gap: 0.5rem;
89 --sidebar-icon-color: color-mix(in srgb, var(--sidebar-muted-foreground) 60%, var(--sidebar));
90 --sidebar-row-content-inset: 0.625rem;
91 --command-shell-inset: 0.5rem;
92 --command-content-inset: 1rem;
93 --floating-content-inset: 0.75rem;
82 --glass-blur: 12px;94 --glass-blur: 12px;
83 --glass-opacity: 80%;95 --glass-opacity: 80%;
84 --glass-saturation: 1.14;96 --glass-saturation: 1.14;