diff --git a/ui/goose2/src/features/extensions/ui/ExtensionsSettings.tsx b/ui/goose2/src/features/extensions/ui/ExtensionsSettings.tsx index cb84d233..cb88e7fe 100644 --- a/ui/goose2/src/features/extensions/ui/ExtensionsSettings.tsx +++ b/ui/goose2/src/features/extensions/ui/ExtensionsSettings.tsx @@ -1,9 +1,9 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; -import { IconPlus, IconSearch } from "@tabler/icons-react"; +import { IconPlus } from "@tabler/icons-react"; import { Button } from "@/shared/ui/button"; -import { Input } from "@/shared/ui/input"; +import { SearchBar } from "@/shared/ui/SearchBar"; import { listExtensions, addExtension, @@ -18,6 +18,7 @@ import { } from "../types"; import { ExtensionItem } from "./ExtensionItem"; import { ExtensionModal } from "./ExtensionModal"; +import { SettingsPage } from "@/shared/ui/SettingsPage"; export function ExtensionsSettings() { const { t } = useTranslation("settings"); @@ -140,26 +141,33 @@ export function ExtensionsSettings() { }; return ( -
-
-

- {t("extensions.title")} -

-

- {t("extensions.description")} -

-
- -
- - { + setEditingExtension(null); + setModalMode("add"); + }} + > + + {t("extensions.addExtension")} + + } + controls={ + setSearchTerm(e.target.value)} + onChange={setSearchTerm} placeholder={t("extensions.search")} - className="pl-9" + aria-label={t("extensions.search")} + size="compact" /> -
- + } + > {isLoading ? (
{[1, 2, 3].map((i) => ( @@ -212,19 +220,6 @@ export function ExtensionsSettings() {
)} - - {modalMode === "add" && ( )} @@ -237,6 +232,6 @@ export function ExtensionsSettings() { onClose={handleModalClose} /> )} -
+ ); } diff --git a/ui/goose2/src/features/settings/ui/AboutSettings.tsx b/ui/goose2/src/features/settings/ui/AboutSettings.tsx new file mode 100644 index 00000000..791c076d --- /dev/null +++ b/ui/goose2/src/features/settings/ui/AboutSettings.tsx @@ -0,0 +1,13 @@ +import { useTranslation } from "react-i18next"; +import { SettingsPage } from "@/shared/ui/SettingsPage"; + +export function AboutSettings() { + const { t } = useTranslation("settings"); + + return ( + + ); +} diff --git a/ui/goose2/src/features/settings/ui/AppearanceSettings.tsx b/ui/goose2/src/features/settings/ui/AppearanceSettings.tsx index 8424ffc6..7f6a7ad9 100644 --- a/ui/goose2/src/features/settings/ui/AppearanceSettings.tsx +++ b/ui/goose2/src/features/settings/ui/AppearanceSettings.tsx @@ -4,6 +4,7 @@ import { ToggleGroup, ToggleGroupItem } from "@/shared/ui/toggle-group"; import { useTheme } from "@/shared/theme/ThemeProvider"; import { Sun, Moon, Monitor, Check } from "lucide-react"; import { useTranslation } from "react-i18next"; +import { SettingsPage } from "@/shared/ui/SettingsPage"; const THEME_OPTIONS = [ { value: "light", icon: Sun }, @@ -56,13 +57,7 @@ export function AppearanceSettings() { useTheme(); return ( -
-

- {t("appearance.title")} -

- - - + -
+
); } diff --git a/ui/goose2/src/features/settings/ui/ChatsSettings.tsx b/ui/goose2/src/features/settings/ui/ChatsSettings.tsx new file mode 100644 index 00000000..adddf036 --- /dev/null +++ b/ui/goose2/src/features/settings/ui/ChatsSettings.tsx @@ -0,0 +1,65 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { SettingsPage } from "@/shared/ui/SettingsPage"; +import { Button } from "@/shared/ui/button"; +import { + type ChatSession, + useChatSessionStore, +} from "@/features/chat/stores/chatSessionStore"; +import { getDisplaySessionTitle } from "@/features/chat/lib/sessionTitle"; + +export function ChatsSettings() { + const { t } = useTranslation(["settings", "common"]); + const [archivedChats, setArchivedChats] = useState([]); + const [loadingArchivedChats, setLoadingArchivedChats] = useState(true); + + useEffect(() => { + setArchivedChats(useChatSessionStore.getState().getArchivedSessions()); + setLoadingArchivedChats(false); + }, []); + + async function handleRestoreChat(id: string) { + await useChatSessionStore.getState().unarchiveSession(id); + setArchivedChats((prev) => prev.filter((session) => session.id !== id)); + } + + return ( + +
+

{t("chats.sectionTitle")}

+ {!loadingArchivedChats && archivedChats.length === 0 ? ( +

{t("chats.empty")}

+ ) : null} + {archivedChats.map((session) => ( +
+
+
+ {getDisplaySessionTitle( + session.title, + t("common:session.defaultTitle"), + )} +
+

+ {session.projectId + ? t("chats.types.project") + : t("chats.types.standalone")} +

+
+ +
+ ))} +
+
+ ); +} diff --git a/ui/goose2/src/features/settings/ui/CompactionSettings.tsx b/ui/goose2/src/features/settings/ui/CompactionSettings.tsx index 61f97505..0bb42722 100644 --- a/ui/goose2/src/features/settings/ui/CompactionSettings.tsx +++ b/ui/goose2/src/features/settings/ui/CompactionSettings.tsx @@ -2,22 +2,17 @@ import { useTranslation } from "react-i18next"; import { IconCheck } from "@tabler/icons-react"; import { getProviderIcon } from "@/shared/ui/icons/ProviderIcons"; import { GooseAutoCompactSettings } from "./GooseAutoCompactSettings"; +import { SettingsPage } from "@/shared/ui/SettingsPage"; export function CompactionSettings() { const { t } = useTranslation("settings"); const icon = getProviderIcon("goose", "size-6"); return ( -
-
-

- {t("compaction.title")} -

-

- {t("compaction.description")} -

-
- +
@@ -42,6 +37,6 @@ export function CompactionSettings() {
-
+
); } diff --git a/ui/goose2/src/features/settings/ui/DoctorSettings.tsx b/ui/goose2/src/features/settings/ui/DoctorSettings.tsx index 8dfe6d18..73cfbe63 100644 --- a/ui/goose2/src/features/settings/ui/DoctorSettings.tsx +++ b/ui/goose2/src/features/settings/ui/DoctorSettings.tsx @@ -2,13 +2,13 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { RefreshCw, ClipboardCopy, Check, Loader2 } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/shared/ui/button"; -import { Separator } from "@/shared/ui/separator"; import { runDoctor, type DoctorCheck, type DoctorReport, } from "@/shared/api/doctor"; import { DoctorCheckRow } from "./DoctorCheckRow"; +import { SettingsPage } from "@/shared/ui/SettingsPage"; function formatDebugReport(report: DoctorReport): string { const STATUS_ICONS: Record = { @@ -93,20 +93,15 @@ export function DoctorSettings() { } return ( -
-
-
-

- {t("doctor.title")} -

-
- -
+ {report && !loading && ( )} -
-
- - - + + } + > {loading ? (
@@ -180,6 +173,6 @@ export function DoctorSettings() { {t("doctor.empty")}
)} -
+ ); } diff --git a/ui/goose2/src/features/settings/ui/GeneralSettings.tsx b/ui/goose2/src/features/settings/ui/GeneralSettings.tsx index dae850a4..6d0ed437 100644 --- a/ui/goose2/src/features/settings/ui/GeneralSettings.tsx +++ b/ui/goose2/src/features/settings/ui/GeneralSettings.tsx @@ -7,7 +7,7 @@ import { SelectTrigger, SelectValue, } from "@/shared/ui/select"; -import { Separator } from "@/shared/ui/separator"; +import { SettingsPage } from "@/shared/ui/SettingsPage"; function SettingRow({ label, @@ -36,16 +36,10 @@ export function GeneralSettings() { const { preference, setLocalePreference, systemLocaleLabel } = useLocale(); return ( -
-

- {t("general.title")} -

-

- {t("general.description")} -

- - - + -
+ ); } diff --git a/ui/goose2/src/features/settings/ui/ProjectsSettings.tsx b/ui/goose2/src/features/settings/ui/ProjectsSettings.tsx new file mode 100644 index 00000000..7bfbaaed --- /dev/null +++ b/ui/goose2/src/features/settings/ui/ProjectsSettings.tsx @@ -0,0 +1,145 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { SettingsPage } from "@/shared/ui/SettingsPage"; +import { Button, buttonVariants } from "@/shared/ui/button"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/shared/ui/alert-dialog"; +import { + deleteProject, + listArchivedProjects, + restoreProject, + type ProjectInfo, +} from "@/features/projects/api/projects"; +import { ProjectIcon } from "@/features/projects/ui/ProjectIcon"; +import { useProjectStore } from "@/features/projects/stores/projectStore"; + +export function ProjectsSettings() { + const { t } = useTranslation(["settings", "common"]); + const [archivedProjects, setArchivedProjects] = useState([]); + const [loadingArchived, setLoadingArchived] = useState(true); + const [deletingProject, setDeletingProject] = useState( + null, + ); + + useEffect(() => { + listArchivedProjects() + .then(setArchivedProjects) + .catch(() => setArchivedProjects([])) + .finally(() => setLoadingArchived(false)); + }, []); + + async function handleRestoreProject(id: string) { + try { + await restoreProject(id); + await useProjectStore.getState().fetchProjects(); + setArchivedProjects((prev) => prev.filter((p) => p.id !== id)); + } catch { + // best-effort + } + } + + async function handleDelete(id: string) { + try { + await deleteProject(id); + setArchivedProjects((prev) => prev.filter((p) => p.id !== id)); + } catch { + // best-effort + } + } + + return ( + <> + +
+

+ {t("projects.sectionTitle")} +

+ {!loadingArchived && archivedProjects.length === 0 ? ( +

+ {t("projects.empty")} +

+ ) : null} + {archivedProjects.map((project) => ( +
+
+ + {project.name} +
+
+ + +
+
+ ))} +
+
+ + !open && setDeletingProject(null)} + > + + + + {t("deleteProject.title", { + name: deletingProject?.name ?? "", + })} + + + {t("deleteProject.description", { + name: deletingProject?.name ?? "", + })} + + + + {t("common:actions.cancel")} + { + if (deletingProject) { + void handleDelete(deletingProject.id); + setDeletingProject(null); + } + }} + > + {t("common:actions.delete")} + + + + + + ); +} diff --git a/ui/goose2/src/features/settings/ui/ProvidersSettings.tsx b/ui/goose2/src/features/settings/ui/ProvidersSettings.tsx index 76818217..f20deaa1 100644 --- a/ui/goose2/src/features/settings/ui/ProvidersSettings.tsx +++ b/ui/goose2/src/features/settings/ui/ProvidersSettings.tsx @@ -35,6 +35,7 @@ import type { import { useProviderInventoryStore } from "@/features/providers/stores/providerInventoryStore"; import { AgentProviderCard } from "./AgentProviderCard"; import { ModelProviderRow } from "./ModelProviderRow"; +import { SettingsPage } from "@/shared/ui/SettingsPage"; import { catalogEntryToTemplate, formValueToDraft, @@ -306,13 +307,21 @@ export function ProvidersSettings() { } return ( -
-

- {t("providers.title")} -

- - - + void openCreateCustomProvider()} + leftIcon={} + className="shrink-0" + > + {t("providers.custom.addButton")} + + } + >

@@ -334,34 +343,20 @@ export function ProvidersSettings() {
-
-
-
-

- {t("providers.models.title")} -

- {loading ? ( - - - {t("providers.models.checkingStatus")} - - ) : null} -
-

- {t("providers.models.description")} -

-
- +
+

+ {t("providers.models.title")} +

+ {loading ? ( + + + {t("providers.models.checkingStatus")} + + ) : null}
+

+ {t("providers.models.description")} +

{customProviderError ? ( @@ -487,6 +482,6 @@ export function ProvidersSettings() { -
+ ); } diff --git a/ui/goose2/src/features/settings/ui/SettingsModal.tsx b/ui/goose2/src/features/settings/ui/SettingsModal.tsx index b43b3d13..99a079e0 100644 --- a/ui/goose2/src/features/settings/ui/SettingsModal.tsx +++ b/ui/goose2/src/features/settings/ui/SettingsModal.tsx @@ -1,17 +1,7 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { cn } from "@/shared/lib/cn"; -import { Button, buttonVariants } from "@/shared/ui/button"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/shared/ui/alert-dialog"; +import { Button } from "@/shared/ui/button"; import { Mic, Minimize2, @@ -31,18 +21,9 @@ import { ExtensionsSettings } from "@/features/extensions/ui/ExtensionsSettings" import { VoiceInputSettings } from "./VoiceInputSettings"; import { GeneralSettings } from "./GeneralSettings"; import { CompactionSettings } from "./CompactionSettings"; -import { - listArchivedProjects, - restoreProject, - deleteProject, - type ProjectInfo, -} from "@/features/projects/api/projects"; -import { ProjectIcon } from "@/features/projects/ui/ProjectIcon"; -import { useChatSessionStore } from "@/features/chat/stores/chatSessionStore"; -import { useProjectStore } from "@/features/projects/stores/projectStore"; -import { getDisplaySessionTitle } from "@/features/chat/lib/sessionTitle"; - -import type { Session } from "@/shared/types/chat"; +import { ProjectsSettings } from "./ProjectsSettings"; +import { ChatsSettings } from "./ChatsSettings"; +import { AboutSettings } from "./AboutSettings"; const NAV_ITEMS = [ { id: "appearance", labelKey: "nav.appearance", icon: Palette }, @@ -71,14 +52,7 @@ export function SettingsModal({ const { t } = useTranslation(["settings", "common"]); const [activeSection, setActiveSection] = useState(initialSection); const [isLoaded, setIsLoaded] = useState(false); - const [isTransitioning, setIsTransitioning] = useState(false); - const [archivedProjects, setArchivedProjects] = useState([]); - const [archivedChats, setArchivedChats] = useState([]); - const [loadingArchived, setLoadingArchived] = useState(true); - const [loadingArchivedChats, setLoadingArchivedChats] = useState(true); - const [deletingProject, setDeletingProject] = useState( - null, - ); + const modalRootRef = useRef(null); // Trigger entrance animations after mount useEffect(() => { @@ -86,69 +60,41 @@ export function SettingsModal({ return () => clearTimeout(timer); }, []); - // Load archived projects on mount useEffect(() => { - listArchivedProjects() - .then(setArchivedProjects) - .catch(() => setArchivedProjects([])) - .finally(() => setLoadingArchived(false)); - }, []); + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === "Escape" && + !event.defaultPrevented && + event.target instanceof Node && + modalRootRef.current?.contains(event.target) + ) { + onClose(); + } + }; - // Load archived chats from the session store (persisted in localStorage) - useEffect(() => { - const archived = useChatSessionStore.getState().getArchivedSessions(); - setArchivedChats(archived as unknown as Session[]); - setLoadingArchivedChats(false); - }, []); - - const handleRestoreProject = async (id: string) => { - try { - await restoreProject(id); - await useProjectStore.getState().fetchProjects(); - setArchivedProjects((prev) => prev.filter((p) => p.id !== id)); - } catch { - // best-effort - } - }; - - const handleRestoreChat = async (id: string) => { - await useChatSessionStore.getState().unarchiveSession(id); - setArchivedChats((prev) => prev.filter((session) => session.id !== id)); - }; - - const handleDelete = async (id: string) => { - try { - await deleteProject(id); - setArchivedProjects((prev) => prev.filter((p) => p.id !== id)); - } catch { - // best-effort - } - }; - - // Content transition on section change - // biome-ignore lint/correctness/useExhaustiveDependencies: activeSection triggers the transition effect intentionally - useEffect(() => { - setIsTransitioning(true); - const timer = setTimeout(() => setIsTransitioning(false), 150); - return () => clearTimeout(timer); - }, [activeSection]); + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [onClose]); const navItems = NAV_ITEMS.map((item) => ({ ...item, label: t(item.labelKey), })); + const activeSectionLabel = + navItems.find((item) => item.id === activeSection)?.label ?? t("title"); return ( + // biome-ignore lint/a11y/useKeyWithClickEvents: Escape is handled by the document listener while the backdrop only handles pointer dismissal.
{ - if (e.key === "Escape") onClose(); - }} > {/* biome-ignore lint/a11y/useKeyWithClickEvents: stopPropagation on inner container is not a meaningful interaction */} {/* biome-ignore lint/a11y/noStaticElementInteractions: click handler only prevents backdrop dismiss propagation */} @@ -207,37 +153,20 @@ export function SettingsModal({
{/* Content */} -
+
-
-
+
+
{activeSection === "appearance" && } {activeSection === "providers" && } {activeSection === "compaction" && } @@ -245,166 +174,13 @@ export function SettingsModal({ {activeSection === "voice" && } {activeSection === "doctor" && } {activeSection === "general" && } - {activeSection === "projects" && ( -
-
-

- {t("projects.title")} -

-

- {t("projects.description")} -

-
- - {/* Archived Projects */} -
-

- {t("projects.sectionTitle")} -

- {!loadingArchived && archivedProjects.length === 0 && ( -

- {t("projects.empty")} -

- )} - {archivedProjects.map((project) => ( -
-
- - - {project.name} - -
-
- - -
-
- ))} -
-
- )} - {activeSection === "chats" && ( -
-
-

- {t("chats.title")} -

-

- {t("chats.description")} -

-
- -
-

- {t("chats.sectionTitle")} -

- {!loadingArchivedChats && archivedChats.length === 0 && ( -

- {t("chats.empty")} -

- )} - {archivedChats.map((session) => ( -
-
-
- {getDisplaySessionTitle( - session.title, - t("common:session.defaultTitle"), - )} -
-

- {session.projectId - ? t("chats.types.project") - : t("chats.types.standalone")} -

-
- -
- ))} -
-
- )} - {activeSection === "about" && ( -
-

- {t("about.title")} -

-

- {t("about.description")} -

-
- )} + {activeSection === "projects" && } + {activeSection === "chats" && } + {activeSection === "about" && }
- - !open && setDeletingProject(null)} - > - - - - {t("deleteProject.title", { - name: deletingProject?.name ?? "", - })} - - - {t("deleteProject.description", { - name: deletingProject?.name ?? "", - })} - - - - {t("common:actions.cancel")} - { - if (deletingProject) { - handleDelete(deletingProject.id); - setDeletingProject(null); - } - }} - > - {t("common:actions.delete")} - - - -
); } diff --git a/ui/goose2/src/features/settings/ui/VoiceInputSettings.tsx b/ui/goose2/src/features/settings/ui/VoiceInputSettings.tsx index 3203608c..7c449f67 100644 --- a/ui/goose2/src/features/settings/ui/VoiceInputSettings.tsx +++ b/ui/goose2/src/features/settings/ui/VoiceInputSettings.tsx @@ -26,6 +26,7 @@ import { SelectTrigger, SelectValue, } from "@/shared/ui/select"; +import { SettingsPage } from "@/shared/ui/SettingsPage"; const DISABLED_PROVIDER = "__disabled__"; @@ -221,28 +222,23 @@ export function VoiceInputSettings() { if (loading) { return ( -
-

- {t("general.voiceInput.label")} -

+

{t("common:labels.loading")}

-
+ ); } return ( -
-
-

- {t("general.voiceInput.label")} -

-

- {t("general.voiceInput.description")} -

-
- +

{t("general.voiceInput.providerLabel")} @@ -484,6 +480,6 @@ export function VoiceInputSettings() {

{error ?

{error}

: null} -
+ ); } diff --git a/ui/goose2/src/shared/ui/SearchBar.tsx b/ui/goose2/src/shared/ui/SearchBar.tsx index 37a6cf14..0b7330c4 100644 --- a/ui/goose2/src/shared/ui/SearchBar.tsx +++ b/ui/goose2/src/shared/ui/SearchBar.tsx @@ -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; + /** 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)} />
diff --git a/ui/goose2/src/shared/ui/SettingsPage.test.tsx b/ui/goose2/src/shared/ui/SettingsPage.test.tsx new file mode 100644 index 00000000..147f1b16 --- /dev/null +++ b/ui/goose2/src/shared/ui/SettingsPage.test.tsx @@ -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(); + + expect( + screen.getByRole("heading", { name: "General" }), + ).toBeInTheDocument(); + }); + + it("renders description, actions, controls, and children", () => { + render( + Add} + controls={} + contentClassName="custom-content" + > +
Extension list
+
, + ); + + 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", + ); + }); +}); diff --git a/ui/goose2/src/shared/ui/SettingsPage.tsx b/ui/goose2/src/shared/ui/SettingsPage.tsx new file mode 100644 index 00000000..73b2a945 --- /dev/null +++ b/ui/goose2/src/shared/ui/SettingsPage.tsx @@ -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 ( +
+
+
+
+

+ {title} +

+ {description ? ( +

+ {description} +

+ ) : null} +
+ {actions ? ( +
+ {actions} +
+ ) : null} +
+ {controls ?
{controls}
: null} +
+ {children ? ( +
{children}
+ ) : null} +
+ ); +} diff --git a/ui/goose2/src/shared/ui/button.tsx b/ui/goose2/src/shared/ui/button.tsx index 471af3d4..c4e31132 100644 --- a/ui/goose2/src/shared/ui/button.tsx +++ b/ui/goose2/src/shared/ui/button.tsx @@ -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",