Skip to content

Commit 5e9c854

Browse files
committed
feat(web): redesign usage insights
1 parent 8f1ea22 commit 5e9c854

11 files changed

Lines changed: 457 additions & 329 deletions
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { renderToStaticMarkup } from "react-dom/server";
2+
import { describe, expect, it } from "vite-plus/test";
3+
4+
import { HeaderFilterMenu } from "./HeaderFilterMenu";
5+
6+
describe("HeaderFilterMenu", () => {
7+
it("uses the shared button contract for its trigger", () => {
8+
const markup = renderToStaticMarkup(
9+
<HeaderFilterMenu
10+
label="Usage metric"
11+
value="cost"
12+
options={[
13+
{ value: "cost", label: "Cost" },
14+
{ value: "tokens", label: "Tokens" },
15+
]}
16+
onChange={() => {}}
17+
/>,
18+
);
19+
20+
expect(markup).toContain('data-slot="menu-trigger"');
21+
expect(markup).toContain("focus-visible:ring-2");
22+
expect(markup).toContain('aria-label="Usage metric"');
23+
expect(markup).toContain("Cost");
24+
});
25+
26+
it("renders nothing when there are no options", () => {
27+
const markup = renderToStaticMarkup(
28+
<HeaderFilterMenu label="Empty filter" value="none" options={[]} onChange={() => {}} />,
29+
);
30+
31+
expect(markup).toBe("");
32+
});
33+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ChevronDownIcon } from "lucide-react";
2+
3+
import { Button } from "./ui/button";
4+
import { Menu, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu";
5+
6+
export interface HeaderFilterMenuOption<Value extends string> {
7+
value: Value;
8+
label: string;
9+
}
10+
11+
export function HeaderFilterMenu<Value extends string>({
12+
label,
13+
value,
14+
options,
15+
onChange,
16+
align = "start",
17+
popupClassName,
18+
}: {
19+
label: string;
20+
value: Value;
21+
options: ReadonlyArray<HeaderFilterMenuOption<Value>>;
22+
onChange: (value: Value) => void;
23+
align?: "start" | "center" | "end";
24+
popupClassName?: string;
25+
}) {
26+
const current = options.find((option) => option.value === value) ?? options[0];
27+
if (!current) return null;
28+
return (
29+
<Menu>
30+
<MenuTrigger
31+
render={
32+
<Button size="compact" variant="ghost-muted" aria-label={label} className="text-sm" />
33+
}
34+
>
35+
{current.label}
36+
<ChevronDownIcon aria-hidden className="size-3 text-muted-foreground/70" />
37+
</MenuTrigger>
38+
<MenuPopup align={align} side="bottom" className={popupClassName ?? "min-w-40"}>
39+
<MenuRadioGroup value={current.value} onValueChange={(next) => onChange(next as Value)}>
40+
{options.map((option) => (
41+
<MenuRadioItem key={option.value} value={option.value}>
42+
{option.label}
43+
</MenuRadioItem>
44+
))}
45+
</MenuRadioGroup>
46+
</MenuPopup>
47+
</Menu>
48+
);
49+
}

apps/web/src/components/WorkspaceBreadcrumb.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ export function WorkspaceBreadcrumbItem({
4545
);
4646
}
4747

48-
export function WorkspaceBreadcrumbSeparator() {
48+
export function WorkspaceBreadcrumbSeparator({ className }: { readonly className?: string }) {
4949
return (
50-
<li aria-hidden="true" className="flex shrink-0 items-center text-icon-muted">
50+
<li aria-hidden="true" className={cn("flex shrink-0 items-center text-icon-muted", className)}>
5151
/
5252
</li>
5353
);

apps/web/src/components/WorkspacePageContainer.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,11 @@ export function WorkspacePageHeader({
5252
/>
5353
);
5454
}
55+
56+
/** Keeps an icon glyph on the content edge while its larger hit target extends outward. */
57+
export function WorkspacePageHeaderEdgeControl({
58+
className,
59+
...props
60+
}: ComponentPropsWithoutRef<"div">) {
61+
return <div className={cn("-me-[7px] flex shrink-0", className)} {...props} />;
62+
}

apps/web/src/components/ui/toggle-group.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@ function ToggleGroup({
3030
orientation === "horizontal"
3131
? "*:pointer-coarse:after:min-w-auto"
3232
: "*:pointer-coarse:after:min-h-auto",
33-
variant === "default"
34-
? "gap-0.5"
35-
: orientation === "horizontal"
36-
? "*:not-first:not-data-[slot=separator]:before:-start-[0.5px] *:not-last:not-data-[slot=separator]:before:-end-[0.5px] *:not-first:rounded-s-none *:not-last:rounded-e-none *:not-first:border-s-0 *:not-last:border-e-0 *:not-first:before:rounded-s-none *:not-last:before:rounded-e-none"
37-
: "*:not-first:not-data-[slot=separator]:before:-top-[0.5px] *:not-last:not-data-[slot=separator]:before:-bottom-[0.5px] flex-col *:not-first:rounded-t-none *:not-last:rounded-b-none *:not-first:border-t-0 *:not-last:border-b-0 *:not-first:before:rounded-t-none *:not-last:before:rounded-b-none *:data-[slot=toggle]:not-last:before:hidden dark:*:last:before:hidden dark:*:first:before:block",
33+
variant === "segmented"
34+
? "gap-0.5 rounded-lg bg-muted/45 p-0.5"
35+
: variant === "default"
36+
? "gap-0.5"
37+
: orientation === "horizontal"
38+
? "*:not-first:not-data-[slot=separator]:before:-start-[0.5px] *:not-last:not-data-[slot=separator]:before:-end-[0.5px] *:not-first:rounded-s-none *:not-last:rounded-e-none *:not-first:border-s-0 *:not-last:border-e-0 *:not-first:before:rounded-s-none *:not-last:before:rounded-e-none"
39+
: "*:not-first:not-data-[slot=separator]:before:-top-[0.5px] *:not-last:not-data-[slot=separator]:before:-bottom-[0.5px] flex-col *:not-first:rounded-t-none *:not-last:rounded-b-none *:not-first:border-t-0 *:not-last:border-b-0 *:not-first:before:rounded-t-none *:not-last:before:rounded-b-none *:data-[slot=toggle]:not-last:before:hidden dark:*:last:before:hidden dark:*:first:before:block",
3840
className,
3941
)}
4042
data-size={size}

apps/web/src/components/ui/toggle.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const toggleVariants = cva(
1818
"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",
1919
default: "h-9 min-w-9 px-[calc(--spacing(2)-1px)] sm:h-8 sm:min-w-8",
2020
lg: "h-10 min-w-10 px-[calc(--spacing(2.5)-1px)] sm:h-9 sm:min-w-9",
21+
segmented:
22+
"h-6 min-w-0 rounded-md px-2.5 text-xs before:rounded-[calc(var(--radius-md)-1px)]",
2123
sm: "h-8 min-w-8 px-[calc(--spacing(1.5)-1px)] sm:h-7 sm:min-w-7",
2224
xs: "h-7 min-w-7 px-[calc(--spacing(1)-1px)] sm:h-6 sm:min-w-6 rounded-md",
2325
},
@@ -27,6 +29,8 @@ const toggleVariants = cva(
2729
"border-transparent text-foreground shadow-none [:disabled,:active,[data-pressed]]:shadow-none before:shadow-none data-pressed:bg-accent data-pressed:text-accent-foreground disabled:opacity-100 disabled:text-muted-foreground disabled:[&_svg]:opacity-100",
2830
outline:
2931
"border-input bg-background not-dark:bg-clip-padding shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:data-pressed:bg-input dark:hover:bg-input/64 dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] dark:not-disabled:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/2%)] [:disabled,:active,[data-pressed]]:shadow-none",
32+
segmented:
33+
"border-transparent text-muted-foreground shadow-none transition-colors before:shadow-none hover:bg-accent/45 hover:text-foreground data-pressed:bg-accent data-pressed:text-foreground data-pressed:shadow-xs/5",
3034
},
3135
},
3236
},

0 commit comments

Comments
 (0)