| author | |
| committer | |
| log | 03adf215ceebedeb9c5e813406154dd8353b36f6 |
| tree | 8b2d322e6291d54e48e2617763e843d45691512b |
| parent | 916cff733cd00c85fe0419df5bcb9372a6384746 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 @@ | ||
| 1 | import type { ProjectScript, ResolvedKeybindingsConfig } from "@t3tools/contracts"; | ||
| 2 | import { renderToStaticMarkup } from "react-dom/server"; | ||
| 3 | import { describe, expect, it } from "vite-plus/test"; | ||
| 4 | |||
| 5 | import ProjectScriptsControl from "./ProjectScriptsControl"; | ||
| 6 | |||
| 7 | const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = []; | ||
| 8 | const PRIMARY_SCRIPT: ProjectScript = { | ||
| 9 | id: "dev", | ||
| 10 | name: "Dev", | ||
| 11 | command: "vp dev", | ||
| 12 | icon: "play", | ||
| 13 | runOnWorktreeCreate: false, | ||
| 14 | }; | ||
| 15 | |||
| 16 | function 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 | |||
| 29 | function buttonTag(html: string, ariaLabel: string) { | ||
| 30 | return html.match(new RegExp(`<button[^>]*aria-label="${ariaLabel}"[^>]*>`))?.[0]; | ||
| 31 | } | ||
| 32 | |||
| 33 | function 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 | |||
| 47 | describe("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 | <Button | 341 | <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 | <TooltipTrigger | 443 | <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 | <ProjectFavicon | 923 | <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 | <SidebarContent | 2410 | <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 | <CommandDialogTrigger | 2416 | <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 | <TooltipProvider | 2562 | <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]); |
| 120 | 120 | ||
| 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 @@ | ||
| 1 | import { renderToStaticMarkup } from "react-dom/server"; | ||
| 2 | import { describe, expect, it } from "vite-plus/test"; | ||
| 3 | import { FlaskConicalIcon } from "lucide-react"; | ||
| 4 | |||
| 5 | import { Button } from "./button"; | ||
| 6 | |||
| 7 | describe("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"; |
| 8 | import { cn } from "~/lib/utils"; | 8 | import { cn } from "~/lib/utils"; |
| 9 | 9 | ||
| 10 | const buttonVariants = cva( | 10 | const 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 @@ | ||
| 1 | import { renderToStaticMarkup } from "react-dom/server"; | ||
| 2 | import { describe, expect, it } from "vite-plus/test"; | ||
| 3 | |||
| 4 | import { Command, CommandFooter, CommandInput } from "./command"; | ||
| 5 | |||
| 6 | describe("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 | <AutocompleteInput | 112 | <AutocompleteInput |
| 108 | autoFocus | 113 | 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 | <div | 217 | <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", () => { |
| 55 | 55 | ||
| 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 | }); |
| 65 | 68 | ||
| 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({ |
| 712 | function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { | 712 | function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { |
| 713 | return ( | 713 | return ( |
| 714 | <div | 714 | <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 | } |
| 793 | 796 | ||
| 794 | const sidebarMenuButtonVariants = cva( | 797 | const 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; |