feat: goose2 compact settings modal headers (#8950)
This commit is contained in:
@@ -4,6 +4,14 @@ import { cn } from "@/shared/lib/cn";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
|
||||
const searchBarSizes = {
|
||||
compact: {
|
||||
wrapper:
|
||||
"rounded-md border border-border-soft px-2 py-1 text-xs text-muted-foreground hover:bg-transparent hover:text-foreground",
|
||||
icon: "left-2.5 size-3",
|
||||
input:
|
||||
"h-auto border-none bg-transparent px-0 pl-5 pr-0 text-[11px] font-normal shadow-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:ring-offset-0",
|
||||
inputVariant: "ghost" as const,
|
||||
},
|
||||
small: {
|
||||
wrapper:
|
||||
"rounded-md border border-border-soft px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-transparent hover:text-foreground",
|
||||
@@ -36,6 +44,8 @@ interface SearchBarProps {
|
||||
size?: keyof typeof searchBarSizes;
|
||||
/** Optional ref for the underlying input */
|
||||
inputRef?: React.Ref<HTMLInputElement>;
|
||||
/** Accessible label for the search input */
|
||||
"aria-label"?: string;
|
||||
}
|
||||
|
||||
export function SearchBar({
|
||||
@@ -46,6 +56,7 @@ export function SearchBar({
|
||||
className,
|
||||
size = "default",
|
||||
inputRef,
|
||||
"aria-label": ariaLabel,
|
||||
}: SearchBarProps) {
|
||||
const styles = searchBarSizes[size];
|
||||
|
||||
@@ -62,11 +73,14 @@ export function SearchBar({
|
||||
variant={styles.inputVariant}
|
||||
type="search"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="none"
|
||||
spellCheck={false}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={placeholder}
|
||||
aria-label={ariaLabel}
|
||||
className={cn("w-full placeholder:text-placeholder", styles.input)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { SettingsPage } from "./SettingsPage";
|
||||
|
||||
describe("SettingsPage", () => {
|
||||
it("renders title-only headers", () => {
|
||||
render(<SettingsPage title="General" />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "General" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders description, actions, controls, and children", () => {
|
||||
render(
|
||||
<SettingsPage
|
||||
title="Extensions"
|
||||
description="Manage extensions"
|
||||
actions={<button type="button">Add</button>}
|
||||
controls={<input aria-label="Search extensions" />}
|
||||
contentClassName="custom-content"
|
||||
>
|
||||
<div>Extension list</div>
|
||||
</SettingsPage>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Manage extensions")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Add" })).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Search extensions")).toBeInTheDocument();
|
||||
expect(screen.getByText("Extension list").parentElement).toHaveClass(
|
||||
"custom-content",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
|
||||
interface SettingsPageProps {
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
controls?: ReactNode;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
}
|
||||
|
||||
export function SettingsPage({
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
controls,
|
||||
children,
|
||||
className,
|
||||
contentClassName,
|
||||
}: SettingsPageProps) {
|
||||
return (
|
||||
<div className={cn("min-h-full", className)}>
|
||||
<div className="sticky top-0 z-20 -mx-6 border-b bg-background px-6 py-4">
|
||||
<div className="flex items-center justify-between gap-3 pr-12">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="max-w-prose truncate font-display text-sm font-semibold leading-5 tracking-tight">
|
||||
{title}
|
||||
</h3>
|
||||
{description ? (
|
||||
<p className="mt-0.5 max-w-prose text-xs leading-4 text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{actions ? (
|
||||
<div className="flex flex-shrink-0 items-center gap-1.5">
|
||||
{actions}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{controls ? <div className="mt-3 pr-12">{controls}</div> : null}
|
||||
</div>
|
||||
{children ? (
|
||||
<div className={cn("py-3", contentClassName)}>{children}</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -33,6 +33,7 @@ const buttonVariants = cva(
|
||||
link: "text-brand underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
xxs: "h-6 gap-1.5 px-2 text-[11px]",
|
||||
xs: "h-7 px-2.5 text-xs",
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 px-3 text-xs",
|
||||
@@ -97,6 +98,7 @@ const buttonVariants = cva(
|
||||
);
|
||||
|
||||
const buttonIconSizeClasses = {
|
||||
xxs: "size-3",
|
||||
xs: "size-3",
|
||||
default: "size-3.5",
|
||||
sm: "size-3",
|
||||
|
||||
Reference in New Issue
Block a user