diff --git a/apps/app/src/components/layout/AppBreadcrumbs.test.tsx b/apps/app/src/components/layout/AppBreadcrumbs.test.tsx new file mode 100644 index 000000000..4fc7240e1 --- /dev/null +++ b/apps/app/src/components/layout/AppBreadcrumbs.test.tsx @@ -0,0 +1,91 @@ +// @vitest-environment jsdom + +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { MemoryRouter, useLocation } from "react-router-dom"; +import { afterEach, describe, expect, it } from "vitest"; +import { AppBreadcrumbs } from "./AppBreadcrumbs"; + +afterEach(cleanup); + +function LocationProbe() { + return ( + {useLocation().pathname} + ); +} + +describe("AppBreadcrumbs", () => { + it("navigates through ancestors while keeping the resource passive", () => { + render( + + + + , + ); + + expect(screen.getByText("Weekly review").getAttribute("aria-current")).toBe( + "page", + ); + expect(screen.queryByRole("link", { name: "Weekly review" })).toBeNull(); + + fireEvent.click(screen.getByRole("link", { name: "Installed" })); + expect(screen.getByLabelText("Current location").textContent).toBe( + "/plugins/automations/automations", + ); + }); + + it("keeps ancestors fixed and truncates only the current crumb in narrow layouts", () => { + render( + +
+ +
+
, + ); + + const navigation = screen.getByRole("navigation", { name: "Breadcrumb" }); + const current = screen.getByText( + "A very long automation name that must fit a narrow header", + ); + + expect(navigation.className).toContain("min-w-0"); + expect(navigation.querySelector("ol")?.className).toContain("min-w-0"); + expect( + screen.getByRole("link", { name: "Automations" }).className, + ).toContain("shrink-0"); + expect(current.className).toContain("min-w-0"); + expect(current.className).toContain("truncate"); + }); +}); diff --git a/apps/app/src/components/layout/AppBreadcrumbs.tsx b/apps/app/src/components/layout/AppBreadcrumbs.tsx new file mode 100644 index 000000000..daa75292b --- /dev/null +++ b/apps/app/src/components/layout/AppBreadcrumbs.tsx @@ -0,0 +1,62 @@ +import { Link } from "react-router-dom"; +import { Icon } from "@bb/shared-ui/icon"; +import { cn } from "@bb/shared-ui/lib/utils"; +import { MACOS_WINDOW_NO_DRAG_CLASS } from "@/lib/bb-desktop"; + +export interface AppBreadcrumbSegment { + label: string; + to?: string; +} + +export function AppBreadcrumbs({ + breadcrumbs, + usesDesktopChrome, +}: { + breadcrumbs: readonly AppBreadcrumbSegment[]; + usesDesktopChrome: boolean; +}) { + return ( + + ); +} diff --git a/apps/app/src/components/layout/AppLayout.tools-breadcrumbs.test.ts b/apps/app/src/components/layout/AppLayout.tools-breadcrumbs.test.ts index 1856bec06..693e16586 100644 --- a/apps/app/src/components/layout/AppLayout.tools-breadcrumbs.test.ts +++ b/apps/app/src/components/layout/AppLayout.tools-breadcrumbs.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { + resolveAutomationBreadcrumbs, resolveToolsBreadcrumbs, TOOLS_NAV_ITEMS, } from "@/components/tools/tools-navigation"; @@ -93,3 +94,81 @@ describe("resolveToolsBreadcrumbs", () => { ).toBeNull(); }); }); + +describe("resolveAutomationBreadcrumbs", () => { + it("maps the installed and browse surfaces to automation breadcrumbs", () => { + expect( + resolveAutomationBreadcrumbs("/plugins/automations/automations"), + ).toEqual([ + { + label: "Automations", + to: "/plugins/automations/automations", + }, + { label: "Installed" }, + ]); + expect( + resolveAutomationBreadcrumbs("/plugins/automations/automations/browse"), + ).toEqual([ + { + label: "Automations", + to: "/plugins/automations/automations", + }, + { label: "Browse" }, + ]); + }); + + it("keeps detail ancestors clickable and replaces the loading fallback label", () => { + const detailPath = + "/plugins/automations/automations/proj_personal/weekly-review"; + + expect(resolveAutomationBreadcrumbs(detailPath)).toEqual([ + { + label: "Automations", + to: "/plugins/automations/automations", + }, + { + label: "Installed", + to: "/plugins/automations/automations", + }, + { label: "weekly-review" }, + ]); + expect(resolveAutomationBreadcrumbs(detailPath, "Weekly review")).toEqual([ + { + label: "Automations", + to: "/plugins/automations/automations", + }, + { + label: "Installed", + to: "/plugins/automations/automations", + }, + { label: "Weekly review" }, + ]); + expect( + resolveAutomationBreadcrumbs(`${detailPath}/edit`, "Weekly review"), + ).toEqual([ + { + label: "Automations", + to: "/plugins/automations/automations", + }, + { + label: "Installed", + to: "/plugins/automations/automations", + }, + { label: "Weekly review" }, + ]); + }); + + it("uses the route id when automation data is missing", () => { + expect( + resolveAutomationBreadcrumbs( + "/plugins/automations/automations/proj_personal/missing%20automation", + )?.at(-1), + ).toEqual({ label: "missing automation" }); + }); + + it("does not claim unrelated plugin routes", () => { + expect( + resolveAutomationBreadcrumbs("/plugins/simple-notes/simple-notes"), + ).toBeNull(); + }); +}); diff --git a/apps/app/src/components/layout/AppLayout.tsx b/apps/app/src/components/layout/AppLayout.tsx index f8522d540..b9cf6240f 100644 --- a/apps/app/src/components/layout/AppLayout.tsx +++ b/apps/app/src/components/layout/AppLayout.tsx @@ -22,7 +22,12 @@ import { AppCommandShortcutHint } from "@/components/commands/AppCommandShortcut import { SettingsSidebar } from "@/components/settings/SettingsSidebar"; import { ToolsSidebar } from "@/components/tools/ToolsSidebar"; import { ToolsHubExperimentProvider } from "@/components/tools/tools-experiment-context"; -import { resolveToolsBreadcrumbs } from "@/components/tools/tools-navigation"; +import { + resolveAutomationBreadcrumbs, + resolveToolsBreadcrumbs, +} from "@/components/tools/tools-navigation"; +import { AppBreadcrumbs } from "./AppBreadcrumbs"; +import { resourceRouteLabelAtom } from "./resourceRouteLabelAtom"; import { AppPageHeader, HEADER_ICON_BUTTON_CLASS } from "./AppPageHeader"; import { stripProjectThreads } from "@/hooks/queries/project-queries"; import { useSidebarNavigation } from "@/hooks/queries/sidebar-navigation-query"; @@ -55,7 +60,6 @@ import { MACOS_CHROME_TRAFFIC_LIGHT_AXIS_NUDGE_CLASS, MACOS_TRAFFIC_LIGHT_RESERVE_OFFSET_CLASS, MACOS_WINDOW_DRAG_CLASS, - MACOS_WINDOW_NO_DRAG_CLASS, shouldReserveMacosTrafficLights, shouldUseMacosDesktopChrome, } from "@/lib/bb-desktop"; @@ -331,54 +335,17 @@ function AppHeader({ Boolean(headerTitle) || Boolean(meta.subtitle); - const center = pluginPanel ? ( + const center = headerBreadcrumbs ? ( +
+ +
+ ) : pluginPanel ? ( ) : hasCenterContent ? (
- {headerBreadcrumbs ? ( - - ) : null} {headerTitle ? (

{headerTitle}

) : null} @@ -437,8 +404,8 @@ export function AppLayout({ children }: AppLayoutProps) { const contentShellRef = useRef(null); useMobileVisualViewportHeight(contentShellRef, isCompactViewport); const location = useLocation(); - const [resourceRouteLabel, setResourceRouteLabel] = useState( - null, + const [resourceRouteLabel, setResourceRouteLabel] = useAtom( + resourceRouteLabelAtom, ); useEffect(() => { setResourceRouteLabel(null); @@ -465,7 +432,7 @@ export function AppLayout({ children }: AppLayoutProps) { handleResourceRouteLabel, ); }; - }, [location.pathname]); + }, [location.pathname, setResourceRouteLabel]); const navigate = useNavigate(); const { appRoutePath, @@ -647,16 +614,21 @@ export function AppLayout({ children }: AppLayoutProps) { resourceRouteLabel, ) : null; + const automationBreadcrumbs = resolveAutomationBreadcrumbs( + location.pathname, + resourceRouteLabel, + ); + const routeBreadcrumbs = toolsBreadcrumbs ?? automationBreadcrumbs; const meta = isThreadView ? { title: thread ? getThreadDisplayTitle(thread) : "Thread", subtitle: undefined, } - : toolsBreadcrumbs + : routeBreadcrumbs ? { title: "", subtitle: undefined, - breadcrumbs: toolsBreadcrumbs, + breadcrumbs: routeBreadcrumbs, } : isArchivedView && projectId ? isProjectlessProjectId(projectId) @@ -708,9 +680,9 @@ export function AppLayout({ children }: AppLayoutProps) { if (pluginPanel) { return pluginPanel.title; } - if (toolsBreadcrumbs) { - const sectionLabel = toolsBreadcrumbs[0]?.label ?? "BB"; - const pageLabel = toolsBreadcrumbs.at(-1)?.label ?? sectionLabel; + if (routeBreadcrumbs) { + const sectionLabel = routeBreadcrumbs[0]?.label ?? "BB"; + const pageLabel = routeBreadcrumbs.at(-1)?.label ?? sectionLabel; return pageLabel === sectionLabel ? sectionLabel : `${pageLabel} · ${sectionLabel}`; diff --git a/apps/app/src/components/layout/AppPageHeader.tsx b/apps/app/src/components/layout/AppPageHeader.tsx index 48a8c909b..038c9dc15 100644 --- a/apps/app/src/components/layout/AppPageHeader.tsx +++ b/apps/app/src/components/layout/AppPageHeader.tsx @@ -36,7 +36,12 @@ export const HEADER_ICON_BUTTON_CLASS = COARSE_POINTER_HEADER_ICON_BUTTON_CLASS; export const HEADER_REDUCED_GLYPH_ICON_BUTTON_CLASS = COARSE_POINTER_HEADER_REDUCED_GLYPH_ICON_BUTTON_CLASS; -export const HEADER_MAXIMIZE_ICON_BUTTON_CLASS = +/** + * Shared geometry for the maximize and close controls at the end of a pane + * header. Keeping both controls on one class gives their button boxes and + * glyphs the same center axis. + */ +export const HEADER_PANE_ACTION_ICON_BUTTON_CLASS = HEADER_REDUCED_GLYPH_ICON_BUTTON_CLASS; interface AppPageHeaderProps { diff --git a/apps/app/src/components/layout/headerIconButtonSizing.test.ts b/apps/app/src/components/layout/headerIconButtonSizing.test.ts index 9296b6040..f448b4e2c 100644 --- a/apps/app/src/components/layout/headerIconButtonSizing.test.ts +++ b/apps/app/src/components/layout/headerIconButtonSizing.test.ts @@ -3,7 +3,7 @@ import { COARSE_POINTER_HEADER_ICON_BUTTON_CLASS, COARSE_POINTER_HEADER_REDUCED_GLYPH_ICON_BUTTON_CLASS, } from "@bb/shared-ui/coarse-pointer-sizing"; -import { HEADER_MAXIMIZE_ICON_BUTTON_CLASS } from "./AppPageHeader"; +import { HEADER_PANE_ACTION_ICON_BUTTON_CLASS } from "./AppPageHeader"; // Guards the shared header-control geometry that BB-63 depends on: the reduced // glyph variant (used by the pane maximize/restore control) must keep the exact @@ -50,8 +50,8 @@ describe("header icon button sizing contract", () => { expect(reduced.has("max-md:pointer-coarse:[&_svg]:size-[20px]")).toBe(false); }); - it("wires the pane maximize control to the reduced-glyph geometry", () => { - expect(HEADER_MAXIMIZE_ICON_BUTTON_CLASS).toBe( + it("keeps pane maximize and close controls on one centered geometry", () => { + expect(HEADER_PANE_ACTION_ICON_BUTTON_CLASS).toBe( COARSE_POINTER_HEADER_REDUCED_GLYPH_ICON_BUTTON_CLASS, ); }); diff --git a/apps/app/src/components/layout/resourceRouteLabelAtom.ts b/apps/app/src/components/layout/resourceRouteLabelAtom.ts new file mode 100644 index 000000000..577ec51fe --- /dev/null +++ b/apps/app/src/components/layout/resourceRouteLabelAtom.ts @@ -0,0 +1,4 @@ +import { atom } from "jotai"; + +/** Loaded resource label shared by the app shell and focused split-pane header. */ +export const resourceRouteLabelAtom = atom(null); diff --git a/apps/app/src/components/tools/Automations.stories.tsx b/apps/app/src/components/tools/Automations.stories.tsx index 3f7407376..c738b4aca 100644 --- a/apps/app/src/components/tools/Automations.stories.tsx +++ b/apps/app/src/components/tools/Automations.stories.tsx @@ -1,4 +1,5 @@ import { useState, type CSSProperties, type ReactNode } from "react"; +import { Link, MemoryRouter, useLocation } from "react-router-dom"; import { AutomationDetailView } from "bb-plugin-automations/detail-view"; import { AutomationOverviewView, @@ -10,6 +11,8 @@ import type { AutomationsOverviewResponse, } from "bb-plugin-automations/rpc-types"; import { ResourceListState } from "@bb/shared-ui/resource-list"; +import { AppBreadcrumbs } from "@/components/layout/AppBreadcrumbs"; +import { resolveAutomationBreadcrumbs } from "@/components/tools/tools-navigation"; export default { title: "Automations", @@ -175,6 +178,81 @@ export function BrowseTemplates() { return ; } +const AUTOMATIONS_ROOT = "/plugins/automations/automations"; +const AUTOMATION_DETAIL = `${AUTOMATIONS_ROOT}/proj_personal/nightly-digest`; +const AUTOMATION_MISSING = `${AUTOMATIONS_ROOT}/proj_personal/missing-automation`; + +function BreadcrumbFlowHarness() { + const location = useLocation(); + const loadedLabel = + location.pathname === AUTOMATION_DETAIL ? "Nightly digest" : null; + const breadcrumbs = resolveAutomationBreadcrumbs( + location.pathname, + loadedLabel, + ); + + return ( + +
+
+ {breadcrumbs ? ( + + ) : null} +
+ +

+ Current route: {location.pathname} +

+
+

+ Narrow detail header +

+
+ +
+
+
+
+ ); +} + +export function BreadcrumbNavigation() { + return ( + + + + ); +} + const DETAIL_AUTOMATION = automation("nightly-digest", "Nightly digest", { trigger: { triggerType: "schedule", diff --git a/apps/app/src/components/tools/tools-navigation.ts b/apps/app/src/components/tools/tools-navigation.ts index 308e691ff..fb124b906 100644 --- a/apps/app/src/components/tools/tools-navigation.ts +++ b/apps/app/src/components/tools/tools-navigation.ts @@ -10,6 +10,10 @@ import { TOOLS_REGISTRY_SKILL_DETAIL_ROUTE_PATH, TOOLS_SKILL_DETAIL_ROUTE_PATH, LEGACY_TOOLS_SKILL_DETAIL_ROUTE_PATH, + AUTOMATIONS_BROWSE_ROUTE_PATH, + AUTOMATIONS_ROUTE_PATH, + AUTOMATION_DETAIL_ROUTE_PATH, + AUTOMATION_EDIT_ROUTE_PATH, } from "@/lib/route-paths"; export type ToolsSectionId = "skills" | "plugins"; @@ -53,6 +57,36 @@ export interface ToolsBreadcrumbSegment { to?: string; } +export function resolveAutomationBreadcrumbs( + pathname: string, + resourceLabel?: string | null, +): ToolsBreadcrumbSegment[] | null { + const root = { label: "Automations", to: AUTOMATIONS_ROUTE_PATH }; + if (pathname === AUTOMATIONS_BROWSE_ROUTE_PATH) { + return [root, { label: "Browse" }]; + } + for (const pattern of [ + AUTOMATION_DETAIL_ROUTE_PATH, + AUTOMATION_EDIT_ROUTE_PATH, + ]) { + const match = matchPath(pattern, pathname); + if (!match) continue; + return [ + root, + { label: "Installed", to: AUTOMATIONS_ROUTE_PATH }, + { + label: + resourceLabel ?? + routeResourceLabel(match.params.automationId, "Automation"), + }, + ]; + } + if (pathname === AUTOMATIONS_ROUTE_PATH) { + return [root, { label: "Installed" }]; + } + return null; +} + function belongsToRoute(pathname: string, route: string): boolean { return pathname === route || pathname.startsWith(`${route}/`); } diff --git a/apps/app/src/views/thread-detail/PaneContext.tsx b/apps/app/src/views/thread-detail/PaneContext.tsx index 72360f4bf..402785891 100644 --- a/apps/app/src/views/thread-detail/PaneContext.tsx +++ b/apps/app/src/views/thread-detail/PaneContext.tsx @@ -14,6 +14,7 @@ import { type ThreadRoutePathArgs, } from "@/lib/route-paths"; import type { PluginComposerHost } from "@/components/plugin/plugin-composer-host"; +import type { SplitSide } from "@/lib/split-layout"; export interface PaneContextValue { paneId: string; @@ -42,6 +43,8 @@ export interface PaneContextValue { isMaximized: boolean; /** Toggles this pane between its split position and full-workspace display. */ onToggleMaximize: (() => void) | null; + /** Moves this pane to one of the split workspace's supported edges. */ + onMoveToSide?: (side: SplitSide) => void; /** * True when this pane renders inside a bounded split card (multi-pane * layouts). Bounded panes suppress the page-bleed negative margins in diff --git a/apps/app/src/views/thread-detail/PaneMaximizeButton.test.tsx b/apps/app/src/views/thread-detail/PaneMaximizeButton.test.tsx index c5d79eed8..94993e1fc 100644 --- a/apps/app/src/views/thread-detail/PaneMaximizeButton.test.tsx +++ b/apps/app/src/views/thread-detail/PaneMaximizeButton.test.tsx @@ -1,9 +1,15 @@ // @vitest-environment jsdom -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { + cleanup, + fireEvent, + render, + screen, + waitFor, +} from "@testing-library/react"; import { TooltipProvider } from "@bb/shared-ui/tooltip"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { HEADER_MAXIMIZE_ICON_BUTTON_CLASS } from "@/components/layout/AppPageHeader"; +import { HEADER_PANE_ACTION_ICON_BUTTON_CLASS } from "@/components/layout/AppPageHeader"; import { PaneContext, type PaneContextValue } from "./PaneContext"; import { PaneMaximizeButton } from "./PaneMaximizeButton"; @@ -19,6 +25,7 @@ const noop = () => {}; function renderButton( isMaximized: boolean, onToggleMaximize: () => void = noop, + onMoveToSide: PaneContextValue["onMoveToSide"] = noop, ) { const value: PaneContextValue = { paneId: "pane-1", @@ -29,13 +36,14 @@ function renderButton( onRequestClose: noop, isMaximized, onToggleMaximize, + onMoveToSide, isBoundedPane: true, isTopRow: true, ownsWindowTopLeft: true, navigateInPane: noop, }; return render( - + @@ -46,10 +54,10 @@ function renderButton( afterEach(cleanup); describe("PaneMaximizeButton", () => { - it("exposes the maximize action, shortcut, and pressed state", () => { + it("enters full screen immediately on the default click", () => { const onToggle = vi.fn(); renderButton(false, onToggle); - const button = screen.getByRole("button", { name: "Maximize pane (⌘⇧E)" }); + const button = screen.getByRole("button", { name: "Full Screen (⌘⇧E)" }); expect(button.getAttribute("aria-keyshortcuts")).toBe("Meta+Shift+E"); expect(button.getAttribute("aria-pressed")).toBe("false"); @@ -57,18 +65,55 @@ describe("PaneMaximizeButton", () => { expect(onToggle).toHaveBeenCalledOnce(); }); - it("changes to restore while maximized", () => { - renderButton(true); - const button = screen.getByRole("button", { name: "Restore pane (⌘⇧E)" }); + it("exits full screen on click and uses the clear tooltip copy", async () => { + const onToggle = vi.fn(); + renderButton(true, onToggle); + const button = screen.getByRole("button", { + name: "Exit Full Screen (⌘⇧E)", + }); expect(button.getAttribute("aria-pressed")).toBe("true"); + + fireEvent.focus(button); + await waitFor(() => { + expect( + screen + .getAllByRole("tooltip") + .some((tooltip) => tooltip.textContent?.includes("Exit Full Screen")), + ).toBe(true); + }); + + fireEvent.click(button); + expect(onToggle).toHaveBeenCalledOnce(); + }); + + it("shows only BB's supported split arrangement actions on hover", async () => { + const onMoveToSide = vi.fn(); + renderButton(false, noop, onMoveToSide); + const button = screen.getByRole("button", { name: "Full Screen (⌘⇧E)" }); + + fireEvent.pointerEnter(button); + const menu = await screen.findByRole("menu", { name: "Pane arrangement" }); + expect(menu.textContent).toContain("Full Screen"); + expect( + screen.getAllByRole("menuitem").map((item) => item.textContent), + ).toEqual([ + "Full Screen⌘⇧E", + "Move left", + "Move right", + "Move top", + "Move bottom", + ]); + + fireEvent.click(screen.getByRole("menuitem", { name: "Move left" })); + expect(onMoveToSide).toHaveBeenCalledWith("left"); }); it("uses the reduced-glyph header geometry so the arrows do not outsize the close/panel controls", () => { renderButton(false); - const button = screen.getByRole("button", { name: "Maximize pane (⌘⇧E)" }); + const button = screen.getByRole("button", { name: "Full Screen (⌘⇧E)" }); // The maximize/restore double-arrows paint larger than the compact close X, // so they render one optical step down while keeping the shared hit target. - for (const token of HEADER_MAXIMIZE_ICON_BUTTON_CLASS.split(/\s+/)) { + for (const token of HEADER_PANE_ACTION_ICON_BUTTON_CLASS.split(/\s+/)) { expect(button.classList.contains(token), `missing ${token}`).toBe(true); } expect(button.classList.contains("[&_svg]:size-[13px]")).toBe(true); diff --git a/apps/app/src/views/thread-detail/PaneMaximizeButton.tsx b/apps/app/src/views/thread-detail/PaneMaximizeButton.tsx index 6230f2b1f..dfe6c2190 100644 --- a/apps/app/src/views/thread-detail/PaneMaximizeButton.tsx +++ b/apps/app/src/views/thread-detail/PaneMaximizeButton.tsx @@ -1,44 +1,136 @@ import { Button } from "@bb/shared-ui/button"; -import { Icon } from "@bb/shared-ui/icon"; +import { Icon, type IconName } from "@bb/shared-ui/icon"; +import { Popover, PopoverAnchor, PopoverContent } from "@bb/shared-ui/popover"; import { Tooltip, TooltipContent, TooltipTrigger } from "@bb/shared-ui/tooltip"; import { useAppCommandShortcut } from "@/components/commands/AppCommandProvider"; -import { HEADER_MAXIMIZE_ICON_BUTTON_CLASS } from "@/components/layout/AppPageHeader"; +import { HEADER_PANE_ACTION_ICON_BUTTON_CLASS } from "@/components/layout/AppPageHeader"; import { CHROME_SUBTLE_ICON_BUTTON_FOREGROUND_CLASS } from "@/components/ui/chromeStyleTokens"; +import { useHoverPopover } from "@/components/ui/hooks/use-hover-popover"; +import type { SplitSide } from "@/lib/split-layout"; import { cn } from "@bb/shared-ui/lib/utils"; import { usePaneContext } from "./PaneContext"; -export function PaneMaximizeButton() { - const { isMaximized, onToggleMaximize } = usePaneContext(); +const ARRANGEMENT_ACTIONS: ReadonlyArray<{ + icon: IconName; + label: string; + side: SplitSide; +}> = [ + { icon: "ChevronLeft", label: "Move left", side: "left" }, + { icon: "ChevronRight", label: "Move right", side: "right" }, + { icon: "ChevronUp", label: "Move top", side: "top" }, + { icon: "ChevronDown", label: "Move bottom", side: "bottom" }, +]; + +const MENU_ITEM_CLASS = + "flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-left text-xs text-foreground outline-none transition-colors hover:bg-state-hover focus-visible:bg-state-hover focus-visible:outline-none [&>svg]:size-4 [&>svg]:shrink-0"; + +export function PaneMaximizeButton({ + defaultMenuOpen = false, + defaultTooltipOpen = false, +}: { + /** Keeps the hover menu visible in its focused Ladle story. */ + defaultMenuOpen?: boolean; + /** Keeps the full-screen tooltip visible in its focused Ladle story. */ + defaultTooltipOpen?: boolean; +}) { + const { isMaximized, onToggleMaximize, onMoveToSide } = usePaneContext(); const shortcut = useAppCommandShortcut("pane.maximize.toggle"); + const { + open: hoverOpen, + triggerHoverProps, + contentHoverProps, + handleOpenChange, + } = useHoverPopover({ closeDelayMs: 100 }); if (onToggleMaximize === null) return null; - const label = isMaximized ? "Restore pane" : "Maximize pane"; + const label = isMaximized ? "Exit Full Screen" : "Full Screen"; const accessibleLabel = shortcut ? `${label} (${shortcut.label})` : label; + const menuOpen = !isMaximized && (defaultMenuOpen || hoverOpen); + const button = ( + + ); + + if (isMaximized) { + return ( + + {button} + + Exit Full Screen + {shortcut ? ` (${shortcut.label})` : ""} + + + ); + } return ( - - - - - - {label} - {shortcut ? ` (${shortcut.label})` : ""} - - + + Full Screen + {shortcut ? ( + {shortcut.label} + ) : null} + + {onMoveToSide ? ( +
+ {ARRANGEMENT_ACTIONS.map((action) => ( + + ))} +
+ ) : null} + + ); } diff --git a/apps/app/src/views/thread-detail/SplitThreadArea.stories.tsx b/apps/app/src/views/thread-detail/SplitThreadArea.stories.tsx index bdf5a7a57..08c1c6d54 100644 --- a/apps/app/src/views/thread-detail/SplitThreadArea.stories.tsx +++ b/apps/app/src/views/thread-detail/SplitThreadArea.stories.tsx @@ -16,6 +16,10 @@ import { import { maximizedPaneIdAtom, splitLayoutAtom } from "@/lib/split-layout/atoms"; import type { SplitLayout } from "@/lib/split-layout"; import { SidebarProvider } from "@/components/ui/sidebar"; +import { AppPageHeader } from "@/components/layout/AppPageHeader"; +import { TooltipProvider } from "@bb/shared-ui/tooltip"; +import { PaneContext, type PaneContextValue } from "./PaneContext"; +import { PaneMaximizeButton } from "./PaneMaximizeButton"; import { SplitThreadArea } from "./SplitThreadArea"; export default { @@ -220,3 +224,62 @@ export function ActiveAndIdle() { export function MaximizedWithoutRail() { return ; } + +const CONTROL_CONTEXT: PaneContextValue = { + paneId: "pane-control", + isFocused: true, + isSplitPane: true, + secondaryPanelHost: null, + reservesWindowPanelToggle: false, + onRequestClose: () => {}, + isMaximized: false, + onToggleMaximize: () => {}, + onMoveToSide: () => {}, + isBoundedPane: true, + isTopRow: true, + ownsWindowTopLeft: false, + navigateInPane: () => {}, +}; + +export function FullScreenControlStates() { + return ( + +
+
+

+ Normal · hover menu +

+
+ + Normal pane + } + actions={} + /> + +
+
+
+

+ Full screen · exit tooltip +

+
+ + + Full-screen pane + + } + actions={} + /> + +
+
+
+
+ ); +} diff --git a/apps/app/src/views/thread-detail/SplitThreadArea.test.tsx b/apps/app/src/views/thread-detail/SplitThreadArea.test.tsx index 8b3d351ef..a207433b2 100644 --- a/apps/app/src/views/thread-detail/SplitThreadArea.test.tsx +++ b/apps/app/src/views/thread-detail/SplitThreadArea.test.tsx @@ -18,6 +18,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createQueryClientTestHarness } from "@/test/queryClientTestHarness"; import { maximizedPaneIdAtom, splitLayoutAtom } from "@/lib/split-layout/atoms"; import { + listPanes, movePane, serializeSplitLayout, SPLIT_LAYOUT_STORAGE_KEY, @@ -25,6 +26,7 @@ import { import type { PaneContent, SplitLayout } from "@/lib/split-layout"; import { usePromptDraftStorage } from "@/hooks/usePromptDraftStorage"; import { createBbDesktopApi } from "@/test/bb-desktop-test-utils"; +import { resourceRouteLabelAtom } from "@/components/layout/resourceRouteLabelAtom"; import { resetPluginSlotStoreForTest, setPluginSlotRegistrations, @@ -242,6 +244,15 @@ vi.mock("./ThreadDetailView", () => ({ {pane.isMaximized ? "restore" : "maximize"} ) : null} + {pane?.onMoveToSide ? ( + + ) : null}
); }, @@ -588,6 +599,22 @@ describe("SplitThreadArea", () => { await waitFor(() => expect(store.get(maximizedPaneIdAtom)).toBeNull()); }); + it("routes arrangement actions through the existing split move operation", async () => { + const store = renderSplitArea({ + path: threadPath("thr-a"), + layout: twoPaneLayout("pane-1"), + }); + + fireEvent.click(await screen.findByTestId("move-right-thr-a")); + + expect( + listPanes( + store.get(splitLayoutAtom)?.root ?? twoPaneLayout("pane-1").root, + ).map((pane) => pane.paneId), + ).toEqual(["pane-2", "pane-1"]); + expect(store.get(splitLayoutAtom)?.focusedPaneId).toBe("pane-1"); + }); + it("carries maximization through focus, CLI-style open, and pane move", async () => { const store = renderSplitArea({ path: threadPath("thr-a"), @@ -1388,9 +1415,7 @@ describe("SplitThreadArea", () => { expect((await contentRow(path))?.className).not.toContain("pl-[104px]"); } - fireEvent.click( - screen.getAllByRole("button", { name: /Maximize pane/ })[3]!, - ); + fireEvent.click(screen.getAllByRole("button", { name: /Full Screen/ })[3]!); await waitFor(() => expect(contentRow("bottom-right")).resolves.toHaveProperty( "className", @@ -1401,7 +1426,7 @@ describe("SplitThreadArea", () => { "pl-[104px]", ); - fireEvent.click(screen.getByRole("button", { name: /Restore pane/ })); + fireEvent.click(screen.getByRole("button", { name: /Exit Full Screen/ })); await waitFor(() => expect(contentRow("top-left")).resolves.toHaveProperty( "className", @@ -1481,12 +1506,69 @@ describe("SplitThreadArea", () => { name: "Toggle docs sidebar", }); const close = screen.getByRole("button", { name: "Close pane" }); - expect(close.querySelector('[data-icon="ClosePluginPane"]')).not.toBeNull(); + const closeIcon = close.querySelector('[data-icon="ClosePluginPane"]'); + expect(closeIcon).not.toBeNull(); + expect(closeIcon?.querySelectorAll("path")).toHaveLength(1); + expect(closeIcon?.querySelector("path")?.getAttribute("d")).toContain( + "M18 6L6.00081 17.9992", + ); expect( toggle.compareDocumentPosition(close) & Node.DOCUMENT_POSITION_FOLLOWING, ).not.toBe(0); }); + it("uses automation breadcrumbs in the split-owned plugin header", async () => { + setPluginSlotRegistrations("automations", { + homepageSections: [], + settingsSections: [], + navPanels: [ + { + id: "automations", + title: "Automations", + icon: "Clock", + path: "automations", + component: () =>
Automation detail
, + }, + ], + threadPanelActions: [], + pendingInteractions: [], + sidebarFooterActions: [], + fileOpeners: [], + messageDirectives: [], + }); + const content: PaneContent = { + kind: "plugin-panel", + pluginId: "automations", + panelPath: "automations", + subPath: "proj_personal/weekly-review", + }; + const store = renderSplitArea({ + path: "/plugins/automations/automations/proj_personal/weekly-review", + layout: { + root: { type: "pane", paneId: "pane-automation", content }, + focusedPaneId: "pane-automation", + }, + routeContent: content, + }); + + const breadcrumb = await screen.findByRole("navigation", { + name: "Breadcrumb", + }); + expect(breadcrumb.textContent).toContain("Automations"); + expect(breadcrumb.textContent).toContain("Installed"); + expect(breadcrumb.textContent).toContain("weekly-review"); + + store.set(resourceRouteLabelAtom, "Weekly review"); + await waitFor(() => + expect(breadcrumb.textContent).toContain("Weekly review"), + ); + + fireEvent.click(screen.getByRole("link", { name: "Installed" })); + expect(screen.getByTestId("location").textContent).toBe( + "/plugins/automations/automations", + ); + }); + it("ignores a layout written by another tab (issue #873)", async () => { renderSplitArea({ path: threadPath("thr-a") }); expect(await screen.findByTestId("pane-thr-a")).toBeTruthy(); diff --git a/apps/app/src/views/thread-detail/SplitThreadArea.tsx b/apps/app/src/views/thread-detail/SplitThreadArea.tsx index f13feae73..a3d926de4 100644 --- a/apps/app/src/views/thread-detail/SplitThreadArea.tsx +++ b/apps/app/src/views/thread-detail/SplitThreadArea.tsx @@ -1,7 +1,7 @@ import { useIsCompactViewport } from "@bb/shared-ui/hooks/use-compact-viewport"; import { cn } from "@bb/shared-ui/lib/utils"; import { PANE_FOCUS_APP_COMMAND_IDS } from "@bb/domain"; -import { useAtom, useStore } from "jotai"; +import { useAtom, useAtomValue, useStore } from "jotai"; import { Fragment, useCallback, @@ -27,6 +27,7 @@ import { useThreadSplitsEnabled } from "@/hooks/useThreadSplitsEnabled"; import { maximizedPaneIdAtom, splitLayoutAtom } from "@/lib/split-layout/atoms"; import { clampSplitPairFraction, + computePaneRects, countPanes, findPane, listPanes, @@ -43,6 +44,7 @@ import type { PaneNode, SplitLayout, SplitPath, + SplitSide, } from "@/lib/split-layout"; import { beginSplitDrag, @@ -68,8 +70,11 @@ import { PluginPanelView } from "@/views/PluginPanelView"; import { AppPageHeader, HEADER_ICON_BUTTON_CLASS, - HEADER_REDUCED_GLYPH_ICON_BUTTON_CLASS, + HEADER_PANE_ACTION_ICON_BUTTON_CLASS, } from "@/components/layout/AppPageHeader"; +import { AppBreadcrumbs } from "@/components/layout/AppBreadcrumbs"; +import { resourceRouteLabelAtom } from "@/components/layout/resourceRouteLabelAtom"; +import { resolveAutomationBreadcrumbs } from "@/components/tools/tools-navigation"; import { Button } from "@bb/shared-ui/button"; import { Icon } from "@bb/shared-ui/icon"; import { CHROME_SUBTLE_ICON_BUTTON_FOREGROUND_CLASS } from "@/components/ui/chromeStyleTokens"; @@ -410,6 +415,44 @@ function SplitThreadAreaContent({ routeContent }: SplitThreadAreaProps) { [navigate, setMaximizedPaneId, store], ); + const movePaneToSide = useCallback( + (paneId: string, side: SplitSide) => { + const current = store.get(splitLayoutAtom); + if (current === null || countPanes(current.root) < 2) return; + + const rects = computePaneRects(current.root); + const candidates = listPanes(current.root).filter( + (pane) => pane.paneId !== paneId, + ); + const edgePosition = (candidateId: string) => { + const rect = rects.get(candidateId); + if (rect === undefined) return 0; + switch (side) { + case "left": + return rect.x; + case "right": + return -(rect.x + rect.w); + case "top": + return rect.y; + case "bottom": + return -(rect.y + rect.h); + } + }; + const target = candidates.sort( + (first, second) => + edgePosition(first.paneId) - edgePosition(second.paneId), + )[0]; + if (target === undefined) return; + + const next = movePane(current, paneId, target.paneId, side); + if (next === current) return; + store.set(splitLayoutAtom, next); + const route = focusedPaneRoute(next); + if (route !== null) navigate(route, { replace: true }); + }, + [navigate, store], + ); + const resize = useCallback( (splitPath: SplitPath, childIndex: number, fraction: number) => { setLayout((previous) => @@ -607,6 +650,7 @@ function SplitThreadAreaContent({ routeContent }: SplitThreadAreaProps) { onFocusPane={focusPane} onClosePane={closePane} onToggleMaximizePane={toggleMaximizePane} + onMovePaneToSide={movePaneToSide} onResize={resize} onNavigateInPane={navigateInPane} onBeginPaneDrag={beginPaneDrag} @@ -685,6 +729,7 @@ interface SplitTreeProps { onFocusPane: (paneId: string) => void; onClosePane: (paneId: string) => void; onToggleMaximizePane: (paneId: string) => void; + onMovePaneToSide: (paneId: string, side: SplitSide) => void; onResize: ( splitPath: SplitPath, childIndex: number, @@ -742,6 +787,7 @@ function SplitTree(props: SplitTreeProps) { onRequestClose={() => props.onClosePane(node.paneId)} isMaximized={isMaximized} onToggleMaximize={() => props.onToggleMaximizePane(node.paneId)} + onMoveToSide={(side) => props.onMovePaneToSide(node.paneId, side)} isBoundedPane isTopRow={isMaximized || isTopRow} ownsWindowTopLeft={ @@ -820,6 +866,7 @@ interface WorkspacePaneContentProps { onRequestClose: (() => void) | null; isMaximized: boolean; onToggleMaximize: (() => void) | null; + onMoveToSide?: (side: SplitSide) => void; // True inside multi-pane split cards; suppresses the page-bleed margins so // content fills the card exactly (see PaneContextValue.isBoundedPane). isBoundedPane: boolean; @@ -840,6 +887,7 @@ function WorkspacePaneContent({ onRequestClose, isMaximized, onToggleMaximize, + onMoveToSide, isBoundedPane, isTopRow, ownsWindowTopLeft, @@ -878,6 +926,7 @@ function WorkspacePaneContent({ onRequestClose, isMaximized, onToggleMaximize, + onMoveToSide, isBoundedPane, isTopRow, ownsWindowTopLeft, @@ -895,6 +944,7 @@ function WorkspacePaneContent({ onRequestClose, isMaximized, onToggleMaximize, + onMoveToSide, paneId, reservesWindowPanelToggle, secondaryPanelHost, @@ -959,6 +1009,7 @@ function NonThreadPaneContent({ ownsWindowTopLeft: boolean; }) { const { navPanels } = usePluginSlots(); + const resourceRouteLabel = useAtomValue(resourceRouteLabelAtom); const { reservesWindowPanelToggle, isFocused } = useOptionalPaneContext() ?? { reservesWindowPanelToggle: false, isFocused: true, @@ -975,8 +1026,21 @@ function NonThreadPaneContent({ candidate.path === content.panelPath, ) : undefined; + const automationBreadcrumbs = + content.kind === "plugin-panel" + ? resolveAutomationBreadcrumbs( + paneContentRoute(content), + isFocused ? resourceRouteLabel : null, + ) + : null; const label = panel?.title ?? "New thread"; const handlePointerDown = (event: ReactPointerEvent) => { + if ( + event.target instanceof Element && + event.target.closest("a, button") !== null + ) { + return; + } if (event.button === 0) beginPaneDrag?.(event, label); }; const actions = ( @@ -994,7 +1058,7 @@ function NonThreadPaneContent({ variant="ghost" size="icon" className={cn( - HEADER_REDUCED_GLYPH_ICON_BUTTON_CLASS, + HEADER_PANE_ACTION_ICON_BUTTON_CLASS, CHROME_SUBTLE_ICON_BUTTON_FOREGROUND_CLASS, )} aria-label="Close pane" @@ -1056,7 +1120,12 @@ function NonThreadPaneContent({ )} onPointerDown={beginPaneDrag ? handlePointerDown : undefined} > - {panel ? ( + {automationBreadcrumbs ? ( + + ) : panel ? ( ) : (

({ HEADER_ICON_BUTTON_CLASS: "header-icon-button", - HEADER_REDUCED_GLYPH_ICON_BUTTON_CLASS: "header-reduced-glyph-button", + HEADER_PANE_ACTION_ICON_BUTTON_CLASS: "header-pane-action-button", AppPageHeader: ({ actions, center, @@ -141,10 +141,15 @@ describe("ThreadDetailHeader", () => { expect(screen.getByText("Thread menu")).not.toBeNull(); expect(screen.getByText("Responsive menu actions")).not.toBeNull(); const closePane = screen.getByRole("button", { name: "Close pane" }); - expect(closePane.classList).toContain("header-reduced-glyph-button"); - expect( - closePane.querySelector('[data-icon="CloseThreadPane"]'), - ).not.toBeNull(); + expect(closePane.classList).toContain("header-pane-action-button"); + const closeIcon = closePane.querySelector( + '[data-icon="CloseThreadPane"]', + ); + expect(closeIcon).not.toBeNull(); + expect(closeIcon?.querySelectorAll("path")).toHaveLength(1); + expect(closeIcon?.querySelector("path")?.getAttribute("d")).toContain( + "M18 6L6.00081 17.9992", + ); }); it("keeps responsive controls inline and out of the menu for wide split panes", () => { diff --git a/apps/app/src/views/thread-detail/ThreadDetailHeader.tsx b/apps/app/src/views/thread-detail/ThreadDetailHeader.tsx index 4b1ec4eb4..e7926a04c 100644 --- a/apps/app/src/views/thread-detail/ThreadDetailHeader.tsx +++ b/apps/app/src/views/thread-detail/ThreadDetailHeader.tsx @@ -15,7 +15,7 @@ import { SplitButton } from "@/components/ui/split-button.js"; import { AppPageHeader, HEADER_ICON_BUTTON_CLASS, - HEADER_REDUCED_GLYPH_ICON_BUTTON_CLASS, + HEADER_PANE_ACTION_ICON_BUTTON_CLASS, } from "@/components/layout/AppPageHeader"; import type { ThreadGitActionDialogTarget } from "@/components/dialogs/ThreadGitActionDialog"; import { @@ -272,7 +272,7 @@ export function ThreadDetailHeader({ variant="ghost" size="icon" className={cn( - HEADER_REDUCED_GLYPH_ICON_BUTTON_CLASS, + HEADER_PANE_ACTION_ICON_BUTTON_CLASS, CHROME_SUBTLE_ICON_BUTTON_FOREGROUND_CLASS, )} aria-label="Close pane" diff --git a/packages/plugin-registry/r/icon.json b/packages/plugin-registry/r/icon.json index f19b4f846..02e5c11d5 100644 --- a/packages/plugin-registry/r/icon.json +++ b/packages/plugin-registry/r/icon.json @@ -14,7 +14,7 @@ "files": [ { "path": "registry/components/ui/icon.tsx", - "content": "import { HugeiconsIcon, type IconSvgElement } from \"@hugeicons/react\";\nimport {\n AiContentGenerator01Icon,\n Alert02Icon,\n AlertCircleIcon,\n Archive03Icon,\n ArrowDown01Icon,\n ArrowDown02Icon,\n ArrowDownDoubleIcon,\n ArrowLeft01Icon,\n ArrowMoveDownLeftIcon,\n ArrowMoveDownRightIcon,\n ArrowRight01Icon,\n ArrowRight02Icon,\n ArrowReloadHorizontalIcon,\n ArrowUp01Icon,\n ArrowUp02Icon,\n ArrowUpDoubleIcon,\n ArrowUpDownIcon,\n ArrowTurnBackwardIcon,\n ArrowTurnForwardIcon,\n ArrowUpRight01Icon,\n AttachmentIcon,\n AudioWaveIcon,\n BrowserIcon,\n BubbleChatAddIcon,\n BubbleChatIcon,\n BubbleChatQuestionIcon,\n Calendar03Icon,\n CalendarCheckOut02Icon,\n CalendarSyncIcon,\n Cancel01Icon,\n CancelCircleIcon,\n ChatFeedback01Icon,\n Bug01Icon,\n ChartColumnIcon,\n CheckListIcon,\n CheckmarkCircle02Icon,\n CircleArrowShrink01Icon,\n CircleIcon,\n Clock01Icon,\n CloudIcon,\n CollapseIcon,\n Book02Icon,\n BrainIcon,\n ComputerTerminal01Icon,\n Copy01Icon,\n DashedLine02Icon,\n DashedLineCircleIcon,\n DateTimeIcon,\n Delete02Icon,\n Download01Icon,\n DiscordIcon,\n GithubIcon,\n DragDropHorizontalIcon,\n DragDropVerticalIcon,\n Edit02Icon,\n Edit04Icon,\n ElectricPlugsIcon,\n ExpandIcon,\n ViewIcon,\n ViewOffIcon,\n File01Icon,\n FileAttachmentIcon,\n FileEmpty02Icon,\n FileQuestionMarkIcon,\n FileXIcon,\n Folder02Icon,\n FolderAddIcon,\n FolderGitTwoIcon,\n FolderIcon,\n FolderRemoveIcon,\n GitBranchIcon,\n GitForkIcon,\n GitMergeIcon,\n GitPullRequestArrow,\n GitPullRequestClosedIcon,\n GitPullRequestDraftIcon,\n GitPullRequestIcon,\n GridViewIcon,\n HelpCircleIcon,\n InformationCircleIcon,\n InternetIcon,\n LaptopIcon,\n Layers01Icon,\n ListViewIcon,\n LockIcon,\n Loading03Icon,\n LayoutThreeRowIcon,\n LayoutTwoColumnIcon,\n LayoutTwoRowIcon,\n LinkSquare02Icon,\n Mail02Icon,\n MailOpen01Icon,\n Menu02Icon,\n MessageAdd02Icon,\n MessageQuestionIcon,\n Mic02Icon,\n MoreHorizontalIcon,\n MultiplicationSignSquareIcon,\n PackageReceiveIcon,\n PauseIcon,\n PinIcon,\n PinOffIcon,\n PlayIcon,\n PlusMinusSquare01Icon,\n PlusSignIcon,\n PuzzleIcon,\n Refresh01Icon,\n RepeatIcon,\n Search01Icon,\n SentIcon,\n Settings01Icon,\n SidebarBottomIcon,\n SidebarLeftIcon,\n SidebarRightIcon,\n SlidersHorizontalIcon,\n SmartPhone01Icon,\n Sorting01Icon,\n SourceCodeIcon,\n SquareIcon,\n StarIcon,\n Target02Icon,\n TestTube01Icon,\n TextWrapIcon,\n Tick02Icon,\n TimeScheduleIcon,\n ToolboxIcon,\n Unarchive03Icon,\n UserAdd01Icon,\n UserIcon,\n WorkflowCircle03Icon,\n ZapIcon,\n ZoomInAreaIcon,\n ZoomOutAreaIcon,\n} from \"@hugeicons/core-free-icons\";\nimport { cn } from \"../../lib/utils\";\n\n// The free hugeicons set ships no artist-palette glyph (its `Palette` export\n// is a pen nib), so this inlines the stroke-rounded palette artwork in the\n// same element format the set uses.\nconst PaletteStrokeRoundedIcon: IconSvgElement = [\n [\n \"path\",\n {\n d: \"M21.8205 10.4127C22.062 11.8519 22.1827 12.5715 21.2423 13.9326C21.1459 14.0722 20.8966 14.3713 20.777 14.4911C19.6103 15.6586 18.4308 15.6586 16.0716 15.6586H14.1392C13.5085 15.6586 13.1931 15.6586 12.9639 15.7142C11.9586 15.9581 11.3031 16.9391 11.453 17.9755C11.4872 18.2118 11.6043 18.5085 11.8386 19.102C11.9345 19.3449 11.9824 19.4664 12.0136 19.7304C12.1292 20.7084 11.0869 21.9508 10.1158 21.9926C9.85358 22.0039 9.83681 22.0002 9.80326 21.9926C7.66174 21.51 5.66204 20.3123 4.18389 18.4421C0.736789 14.0808 1.43146 7.71364 5.73548 4.22064C10.0395 0.727643 16.323 1.43156 19.7701 5.79289C20.868 7.1819 21.5457 8.77438 21.8205 10.4127Z\",\n fill: \"none\",\n fillRule: \"evenodd\",\n clipRule: \"evenodd\",\n stroke: \"currentColor\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"0\",\n },\n ],\n [\n \"path\",\n {\n d: \"M7.36719 7.74976H7.24219M7.49219 7.74976C7.49219 7.88783 7.38026 7.99976 7.24219 7.99976C7.10412 7.99976 6.99219 7.88783 6.99219 7.74976C6.99219 7.61169 7.10412 7.49976 7.24219 7.49976C7.38026 7.49976 7.49219 7.61169 7.49219 7.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"1\",\n },\n ],\n [\n \"path\",\n {\n d: \"M7.36719 15.7498H7.24219M7.49219 15.7498C7.49219 15.8878 7.38026 15.9998 7.24219 15.9998C7.10412 15.9998 6.99219 15.8878 6.99219 15.7498C6.99219 15.6117 7.10412 15.4998 7.24219 15.4998C7.38026 15.4998 7.49219 15.6117 7.49219 15.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"2\",\n },\n ],\n [\n \"path\",\n {\n d: \"M11.8672 5.74976H11.7422M11.9922 5.74976C11.9922 5.88783 11.8803 5.99976 11.7422 5.99976C11.6041 5.99976 11.4922 5.88783 11.4922 5.74976C11.4922 5.61169 11.6041 5.49976 11.7422 5.49976C11.8803 5.49976 11.9922 5.61169 11.9922 5.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"3\",\n },\n ],\n [\n \"path\",\n {\n d: \"M16.3672 7.74976H16.2422M16.4922 7.74976C16.4922 7.88783 16.3803 7.99976 16.2422 7.99976C16.1041 7.99976 15.9922 7.88783 15.9922 7.74976C15.9922 7.61169 16.1041 7.49976 16.2422 7.49976C16.3803 7.49976 16.4922 7.61169 16.4922 7.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"4\",\n },\n ],\n [\n \"path\",\n {\n d: \"M18.3672 11.7498H18.2422M18.4922 11.7498C18.4922 11.8878 18.3803 11.9998 18.2422 11.9998C18.1041 11.9998 17.9922 11.8878 17.9922 11.7498C17.9922 11.6117 18.1041 11.4998 18.2422 11.4998C18.3803 11.4998 18.4922 11.6117 18.4922 11.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"5\",\n },\n ],\n [\n \"path\",\n {\n d: \"M5.86719 11.7498H5.74219M5.99219 11.7498C5.99219 11.8878 5.88026 11.9998 5.74219 11.9998C5.60412 11.9998 5.49219 11.8878 5.49219 11.7498C5.49219 11.6117 5.60412 11.4998 5.74219 11.4998C5.88026 11.4998 5.99219 11.6117 5.99219 11.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"6\",\n },\n ],\n];\n\n// Custom \"new section\" glyph: the set's ListView rows with the middle and\n// bottom rows shortened so the plus owns the lower-right quadrant, matching\n// FolderAdd's non-overlapping plus placement (same plus geometry). Hugeicons\n// has no list-with-plus variant that keeps the ListView row shape, so this\n// inlines the artwork in the same element format the set uses.\nconst SectionAddStrokeRoundedIcon: IconSvgElement = [\n [\n \"path\",\n {\n d: \"M2 3.4C2 2.24173 2.24173 2 3.4 2H20.6C21.7583 2 22 2.24173 22 3.4V4.6C22 5.75827 21.7583 6 20.6 6H3.4C2.24173 6 2 5.75827 2 4.6V3.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"0\",\n },\n ],\n [\n \"path\",\n {\n d: \"M2 11.4C2 10.2417 2.24173 10 3.4 10H10.6C11.7583 10 12 10.2417 12 11.4V12.6C12 13.7583 11.7583 14 10.6 14H3.4C2.24173 14 2 13.7583 2 12.6V11.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"1\",\n },\n ],\n [\n \"path\",\n {\n d: \"M2 19.4C2 18.2417 2.24173 18 3.4 18H10.6C11.7583 18 12 18.2417 12 19.4V20.6C12 21.7583 11.7583 22 10.6 22H3.4C2.24173 22 2 21.7583 2 20.6V19.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"2\",\n },\n ],\n [\n \"path\",\n {\n d: \"M18 13V21M22 17H14\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"3\",\n },\n ],\n];\n\nconst ICON_MAP = {\n AiContentGenerator01: AiContentGenerator01Icon,\n AlertCircle: AlertCircleIcon,\n AlertTriangle: Alert02Icon,\n AlignLeft: Menu02Icon,\n AppWindow: BrowserIcon,\n Archive: Archive03Icon,\n ArchiveRestore: Unarchive03Icon,\n ArrowDown: ArrowDown02Icon,\n ArrowRight: ArrowRight02Icon,\n ArrowReloadHorizontal: ArrowReloadHorizontalIcon,\n ArrowUp: ArrowUp02Icon,\n ArrowUpDown: ArrowUpDownIcon,\n ArrowTurnBackward: ArrowTurnBackwardIcon,\n ArrowTurnForward: ArrowTurnForwardIcon,\n ArrowUpRight: ArrowUpRight01Icon,\n AudioLines: AudioWaveIcon,\n Beaker: TestTube01Icon,\n BubbleChatQuestion: BubbleChatQuestionIcon,\n Browser: BrowserIcon,\n Brain: BrainIcon,\n Bug: Bug01Icon,\n Calendar: Calendar03Icon,\n CalendarCheckOut02: CalendarCheckOut02Icon,\n CalendarSync: CalendarSyncIcon,\n ChatFeedback: ChatFeedback01Icon,\n ChartColumn: ChartColumnIcon,\n Check: Tick02Icon,\n ChevronDown: ArrowDown01Icon,\n ChevronLeft: ArrowLeft01Icon,\n ChevronRight: ArrowRight01Icon,\n ChevronUp: ArrowUp01Icon,\n ChevronsDown: ArrowDownDoubleIcon,\n ChevronsUp: ArrowUpDoubleIcon,\n Circle: CircleIcon,\n CircleArrowShrink: CircleArrowShrink01Icon,\n CircleCheck: CheckmarkCircle02Icon,\n CircleDashed: DashedLineCircleIcon,\n CircleQuestion: HelpCircleIcon,\n CircleX: CancelCircleIcon,\n Clock: Clock01Icon,\n Code: SourceCodeIcon,\n ComputerTerminal01: ComputerTerminal01Icon,\n Columns2: LayoutTwoColumnIcon,\n Container: CloudIcon,\n Copy: Copy01Icon,\n CornerDownLeft: ArrowMoveDownLeftIcon,\n CornerDownRight: ArrowMoveDownRightIcon,\n Discord: DiscordIcon,\n DateTime: DateTimeIcon,\n Github: GithubIcon,\n DragDropHorizontal: DragDropHorizontalIcon,\n DragDropVertical: DragDropVerticalIcon,\n Download: Download01Icon,\n Edit: Edit02Icon,\n EditFile: Edit04Icon,\n ElectricPlugs: ElectricPlugsIcon,\n Eye: ViewIcon,\n EyeOff: ViewOffIcon,\n Explore: Book02Icon,\n ExternalLink: LinkSquare02Icon,\n FileDiff: PlusMinusSquare01Icon,\n File: FileEmpty02Icon,\n FileAttachment: FileAttachmentIcon,\n FileQuestion: FileQuestionMarkIcon,\n FileText: File01Icon,\n FileX2: FileXIcon,\n Folder: FolderIcon,\n FolderGit: FolderGitTwoIcon,\n FolderOpen: Folder02Icon,\n FolderMinus: FolderRemoveIcon,\n FolderPlus: FolderAddIcon,\n Fork: GitForkIcon,\n GitBranch: GitBranchIcon,\n GitMerge: GitMergeIcon,\n GitPullRequest: GitPullRequestIcon,\n GitPullRequestArrow: GitPullRequestArrow,\n GitPullRequestClosed: GitPullRequestClosedIcon,\n GitPullRequestDraft: GitPullRequestDraftIcon,\n Globe: InternetIcon,\n GridView: GridViewIcon,\n Info: InformationCircleIcon,\n Laptop: LaptopIcon,\n Layers: Layers01Icon,\n ListView: ListViewIcon,\n SectionAdd: SectionAddStrokeRoundedIcon,\n ListTodo: CheckListIcon,\n Loading: Loading03Icon,\n Lock: LockIcon,\n Mail: Mail02Icon,\n MailOpen: MailOpen01Icon,\n Maximize2: ExpandIcon,\n MessageQuestion: MessageQuestionIcon,\n MessageCirclePlus: BubbleChatAddIcon,\n MessageSquarePlus: BubbleChatAddIcon,\n MessageSquare: BubbleChatIcon,\n Mic: Mic02Icon,\n Minimize2: CollapseIcon,\n MoreHorizontal: MoreHorizontalIcon,\n NewTab: DashedLine02Icon,\n PackageReceive: PackageReceiveIcon,\n Palette: PaletteStrokeRoundedIcon,\n PanelBottom: SidebarBottomIcon,\n PanelLeft: SidebarLeftIcon,\n PanelRight: SidebarRightIcon,\n Paperclip: AttachmentIcon,\n Pause: PauseIcon,\n Pin: PinIcon,\n PinOff: PinOffIcon,\n Play: PlayIcon,\n Plus: PlusSignIcon,\n Puzzle: PuzzleIcon,\n Repeat: RepeatIcon,\n RotateCcw: Refresh01Icon,\n Rows2: LayoutTwoRowIcon,\n Rows3: LayoutThreeRowIcon,\n Search: Search01Icon,\n Sent: SentIcon,\n Settings: Settings01Icon,\n SideChat: MessageAdd02Icon,\n ClosePluginPane: MultiplicationSignSquareIcon,\n CloseThreadPane: MultiplicationSignSquareIcon,\n SlidersHorizontal: SlidersHorizontalIcon,\n Smartphone: SmartPhone01Icon,\n Sort: Sorting01Icon,\n Spinner: DashedLineCircleIcon,\n Square: SquareIcon,\n Star: StarIcon,\n Target: Target02Icon,\n Terminal: ComputerTerminal01Icon,\n TextWrap: TextWrapIcon,\n TimeSchedule: TimeScheduleIcon,\n Toolbox: ToolboxIcon,\n Trash2: Delete02Icon,\n UserRound: UserIcon,\n UserRoundPlus: UserAdd01Icon,\n Workflow: WorkflowCircle03Icon,\n X: Cancel01Icon,\n Zap: ZapIcon,\n ZoomIn: ZoomInAreaIcon,\n ZoomOut: ZoomOutAreaIcon,\n} as const satisfies Record;\n\nexport type IconName = keyof typeof ICON_MAP;\n\nexport const ICON_NAMES = Object.keys(ICON_MAP) as readonly IconName[];\n\nexport interface IconProps {\n name: IconName;\n className?: string;\n \"aria-hidden\"?: boolean | \"true\" | \"false\";\n \"aria-label\"?: string;\n}\n\nexport function Icon({\n name,\n className,\n \"aria-hidden\": ariaHidden,\n \"aria-label\": ariaLabel,\n}: IconProps) {\n return (\n \n );\n}\n", + "content": "import { HugeiconsIcon, type IconSvgElement } from \"@hugeicons/react\";\nimport {\n AiContentGenerator01Icon,\n Alert02Icon,\n AlertCircleIcon,\n Archive03Icon,\n ArrowDown01Icon,\n ArrowDown02Icon,\n ArrowDownDoubleIcon,\n ArrowLeft01Icon,\n ArrowMoveDownLeftIcon,\n ArrowMoveDownRightIcon,\n ArrowRight01Icon,\n ArrowRight02Icon,\n ArrowReloadHorizontalIcon,\n ArrowUp01Icon,\n ArrowUp02Icon,\n ArrowUpDoubleIcon,\n ArrowUpDownIcon,\n ArrowTurnBackwardIcon,\n ArrowTurnForwardIcon,\n ArrowUpRight01Icon,\n AttachmentIcon,\n AudioWaveIcon,\n BrowserIcon,\n BubbleChatAddIcon,\n BubbleChatIcon,\n BubbleChatQuestionIcon,\n Calendar03Icon,\n CalendarCheckOut02Icon,\n CalendarSyncIcon,\n Cancel01Icon,\n CancelCircleIcon,\n ChatFeedback01Icon,\n Bug01Icon,\n ChartColumnIcon,\n CheckListIcon,\n CheckmarkCircle02Icon,\n CircleArrowShrink01Icon,\n CircleIcon,\n Clock01Icon,\n CloudIcon,\n CollapseIcon,\n Book02Icon,\n BrainIcon,\n ComputerTerminal01Icon,\n Copy01Icon,\n DashedLine02Icon,\n DashedLineCircleIcon,\n DateTimeIcon,\n Delete02Icon,\n Download01Icon,\n DiscordIcon,\n GithubIcon,\n DragDropHorizontalIcon,\n DragDropVerticalIcon,\n Edit02Icon,\n Edit04Icon,\n ElectricPlugsIcon,\n ExpandIcon,\n ViewIcon,\n ViewOffIcon,\n File01Icon,\n FileAttachmentIcon,\n FileEmpty02Icon,\n FileQuestionMarkIcon,\n FileXIcon,\n Folder02Icon,\n FolderAddIcon,\n FolderGitTwoIcon,\n FolderIcon,\n FolderRemoveIcon,\n GitBranchIcon,\n GitForkIcon,\n GitMergeIcon,\n GitPullRequestArrow,\n GitPullRequestClosedIcon,\n GitPullRequestDraftIcon,\n GitPullRequestIcon,\n GridViewIcon,\n HelpCircleIcon,\n InformationCircleIcon,\n InternetIcon,\n LaptopIcon,\n Layers01Icon,\n ListViewIcon,\n LockIcon,\n Loading03Icon,\n LayoutThreeRowIcon,\n LayoutTwoColumnIcon,\n LayoutTwoRowIcon,\n LinkSquare02Icon,\n Mail02Icon,\n MailOpen01Icon,\n Menu02Icon,\n MessageAdd02Icon,\n MessageQuestionIcon,\n Mic02Icon,\n MoreHorizontalIcon,\n PackageReceiveIcon,\n PauseIcon,\n PinIcon,\n PinOffIcon,\n PlayIcon,\n PlusMinusSquare01Icon,\n PlusSignIcon,\n PuzzleIcon,\n Refresh01Icon,\n RepeatIcon,\n Search01Icon,\n SentIcon,\n Settings01Icon,\n SidebarBottomIcon,\n SidebarLeftIcon,\n SidebarRightIcon,\n SlidersHorizontalIcon,\n SmartPhone01Icon,\n Sorting01Icon,\n SourceCodeIcon,\n SquareIcon,\n StarIcon,\n Target02Icon,\n TestTube01Icon,\n TextWrapIcon,\n Tick02Icon,\n TimeScheduleIcon,\n ToolboxIcon,\n Unarchive03Icon,\n UserAdd01Icon,\n UserIcon,\n WorkflowCircle03Icon,\n ZapIcon,\n ZoomInAreaIcon,\n ZoomOutAreaIcon,\n} from \"@hugeicons/core-free-icons\";\nimport { cn } from \"../../lib/utils\";\n\n// The free hugeicons set ships no artist-palette glyph (its `Palette` export\n// is a pen nib), so this inlines the stroke-rounded palette artwork in the\n// same element format the set uses.\nconst PaletteStrokeRoundedIcon: IconSvgElement = [\n [\n \"path\",\n {\n d: \"M21.8205 10.4127C22.062 11.8519 22.1827 12.5715 21.2423 13.9326C21.1459 14.0722 20.8966 14.3713 20.777 14.4911C19.6103 15.6586 18.4308 15.6586 16.0716 15.6586H14.1392C13.5085 15.6586 13.1931 15.6586 12.9639 15.7142C11.9586 15.9581 11.3031 16.9391 11.453 17.9755C11.4872 18.2118 11.6043 18.5085 11.8386 19.102C11.9345 19.3449 11.9824 19.4664 12.0136 19.7304C12.1292 20.7084 11.0869 21.9508 10.1158 21.9926C9.85358 22.0039 9.83681 22.0002 9.80326 21.9926C7.66174 21.51 5.66204 20.3123 4.18389 18.4421C0.736789 14.0808 1.43146 7.71364 5.73548 4.22064C10.0395 0.727643 16.323 1.43156 19.7701 5.79289C20.868 7.1819 21.5457 8.77438 21.8205 10.4127Z\",\n fill: \"none\",\n fillRule: \"evenodd\",\n clipRule: \"evenodd\",\n stroke: \"currentColor\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"0\",\n },\n ],\n [\n \"path\",\n {\n d: \"M7.36719 7.74976H7.24219M7.49219 7.74976C7.49219 7.88783 7.38026 7.99976 7.24219 7.99976C7.10412 7.99976 6.99219 7.88783 6.99219 7.74976C6.99219 7.61169 7.10412 7.49976 7.24219 7.49976C7.38026 7.49976 7.49219 7.61169 7.49219 7.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"1\",\n },\n ],\n [\n \"path\",\n {\n d: \"M7.36719 15.7498H7.24219M7.49219 15.7498C7.49219 15.8878 7.38026 15.9998 7.24219 15.9998C7.10412 15.9998 6.99219 15.8878 6.99219 15.7498C6.99219 15.6117 7.10412 15.4998 7.24219 15.4998C7.38026 15.4998 7.49219 15.6117 7.49219 15.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"2\",\n },\n ],\n [\n \"path\",\n {\n d: \"M11.8672 5.74976H11.7422M11.9922 5.74976C11.9922 5.88783 11.8803 5.99976 11.7422 5.99976C11.6041 5.99976 11.4922 5.88783 11.4922 5.74976C11.4922 5.61169 11.6041 5.49976 11.7422 5.49976C11.8803 5.49976 11.9922 5.61169 11.9922 5.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"3\",\n },\n ],\n [\n \"path\",\n {\n d: \"M16.3672 7.74976H16.2422M16.4922 7.74976C16.4922 7.88783 16.3803 7.99976 16.2422 7.99976C16.1041 7.99976 15.9922 7.88783 15.9922 7.74976C15.9922 7.61169 16.1041 7.49976 16.2422 7.49976C16.3803 7.49976 16.4922 7.61169 16.4922 7.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"4\",\n },\n ],\n [\n \"path\",\n {\n d: \"M18.3672 11.7498H18.2422M18.4922 11.7498C18.4922 11.8878 18.3803 11.9998 18.2422 11.9998C18.1041 11.9998 17.9922 11.8878 17.9922 11.7498C17.9922 11.6117 18.1041 11.4998 18.2422 11.4998C18.3803 11.4998 18.4922 11.6117 18.4922 11.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"5\",\n },\n ],\n [\n \"path\",\n {\n d: \"M5.86719 11.7498H5.74219M5.99219 11.7498C5.99219 11.8878 5.88026 11.9998 5.74219 11.9998C5.60412 11.9998 5.49219 11.8878 5.49219 11.7498C5.49219 11.6117 5.60412 11.4998 5.74219 11.4998C5.88026 11.4998 5.99219 11.6117 5.99219 11.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"6\",\n },\n ],\n];\n\n// Custom \"new section\" glyph: the set's ListView rows with the middle and\n// bottom rows shortened so the plus owns the lower-right quadrant, matching\n// FolderAdd's non-overlapping plus placement (same plus geometry). Hugeicons\n// has no list-with-plus variant that keeps the ListView row shape, so this\n// inlines the artwork in the same element format the set uses.\nconst SectionAddStrokeRoundedIcon: IconSvgElement = [\n [\n \"path\",\n {\n d: \"M2 3.4C2 2.24173 2.24173 2 3.4 2H20.6C21.7583 2 22 2.24173 22 3.4V4.6C22 5.75827 21.7583 6 20.6 6H3.4C2.24173 6 2 5.75827 2 4.6V3.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"0\",\n },\n ],\n [\n \"path\",\n {\n d: \"M2 11.4C2 10.2417 2.24173 10 3.4 10H10.6C11.7583 10 12 10.2417 12 11.4V12.6C12 13.7583 11.7583 14 10.6 14H3.4C2.24173 14 2 13.7583 2 12.6V11.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"1\",\n },\n ],\n [\n \"path\",\n {\n d: \"M2 19.4C2 18.2417 2.24173 18 3.4 18H10.6C11.7583 18 12 18.2417 12 19.4V20.6C12 21.7583 11.7583 22 10.6 22H3.4C2.24173 22 2 21.7583 2 20.6V19.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"2\",\n },\n ],\n [\n \"path\",\n {\n d: \"M18 13V21M22 17H14\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"3\",\n },\n ],\n];\n\nconst ICON_MAP = {\n AiContentGenerator01: AiContentGenerator01Icon,\n AlertCircle: AlertCircleIcon,\n AlertTriangle: Alert02Icon,\n AlignLeft: Menu02Icon,\n AppWindow: BrowserIcon,\n Archive: Archive03Icon,\n ArchiveRestore: Unarchive03Icon,\n ArrowDown: ArrowDown02Icon,\n ArrowRight: ArrowRight02Icon,\n ArrowReloadHorizontal: ArrowReloadHorizontalIcon,\n ArrowUp: ArrowUp02Icon,\n ArrowUpDown: ArrowUpDownIcon,\n ArrowTurnBackward: ArrowTurnBackwardIcon,\n ArrowTurnForward: ArrowTurnForwardIcon,\n ArrowUpRight: ArrowUpRight01Icon,\n AudioLines: AudioWaveIcon,\n Beaker: TestTube01Icon,\n BubbleChatQuestion: BubbleChatQuestionIcon,\n Browser: BrowserIcon,\n Brain: BrainIcon,\n Bug: Bug01Icon,\n Calendar: Calendar03Icon,\n CalendarCheckOut02: CalendarCheckOut02Icon,\n CalendarSync: CalendarSyncIcon,\n ChatFeedback: ChatFeedback01Icon,\n ChartColumn: ChartColumnIcon,\n Check: Tick02Icon,\n ChevronDown: ArrowDown01Icon,\n ChevronLeft: ArrowLeft01Icon,\n ChevronRight: ArrowRight01Icon,\n ChevronUp: ArrowUp01Icon,\n ChevronsDown: ArrowDownDoubleIcon,\n ChevronsUp: ArrowUpDoubleIcon,\n Circle: CircleIcon,\n CircleArrowShrink: CircleArrowShrink01Icon,\n CircleCheck: CheckmarkCircle02Icon,\n CircleDashed: DashedLineCircleIcon,\n CircleQuestion: HelpCircleIcon,\n CircleX: CancelCircleIcon,\n Clock: Clock01Icon,\n Code: SourceCodeIcon,\n ComputerTerminal01: ComputerTerminal01Icon,\n Columns2: LayoutTwoColumnIcon,\n Container: CloudIcon,\n Copy: Copy01Icon,\n CornerDownLeft: ArrowMoveDownLeftIcon,\n CornerDownRight: ArrowMoveDownRightIcon,\n Discord: DiscordIcon,\n DateTime: DateTimeIcon,\n Github: GithubIcon,\n DragDropHorizontal: DragDropHorizontalIcon,\n DragDropVertical: DragDropVerticalIcon,\n Download: Download01Icon,\n Edit: Edit02Icon,\n EditFile: Edit04Icon,\n ElectricPlugs: ElectricPlugsIcon,\n Eye: ViewIcon,\n EyeOff: ViewOffIcon,\n Explore: Book02Icon,\n ExternalLink: LinkSquare02Icon,\n FileDiff: PlusMinusSquare01Icon,\n File: FileEmpty02Icon,\n FileAttachment: FileAttachmentIcon,\n FileQuestion: FileQuestionMarkIcon,\n FileText: File01Icon,\n FileX2: FileXIcon,\n Folder: FolderIcon,\n FolderGit: FolderGitTwoIcon,\n FolderOpen: Folder02Icon,\n FolderMinus: FolderRemoveIcon,\n FolderPlus: FolderAddIcon,\n Fork: GitForkIcon,\n GitBranch: GitBranchIcon,\n GitMerge: GitMergeIcon,\n GitPullRequest: GitPullRequestIcon,\n GitPullRequestArrow: GitPullRequestArrow,\n GitPullRequestClosed: GitPullRequestClosedIcon,\n GitPullRequestDraft: GitPullRequestDraftIcon,\n Globe: InternetIcon,\n GridView: GridViewIcon,\n Info: InformationCircleIcon,\n Laptop: LaptopIcon,\n Layers: Layers01Icon,\n ListView: ListViewIcon,\n SectionAdd: SectionAddStrokeRoundedIcon,\n ListTodo: CheckListIcon,\n Loading: Loading03Icon,\n Lock: LockIcon,\n Mail: Mail02Icon,\n MailOpen: MailOpen01Icon,\n Maximize2: ExpandIcon,\n MessageQuestion: MessageQuestionIcon,\n MessageCirclePlus: BubbleChatAddIcon,\n MessageSquarePlus: BubbleChatAddIcon,\n MessageSquare: BubbleChatIcon,\n Mic: Mic02Icon,\n Minimize2: CollapseIcon,\n MoreHorizontal: MoreHorizontalIcon,\n NewTab: DashedLine02Icon,\n PackageReceive: PackageReceiveIcon,\n Palette: PaletteStrokeRoundedIcon,\n PanelBottom: SidebarBottomIcon,\n PanelLeft: SidebarLeftIcon,\n PanelRight: SidebarRightIcon,\n Paperclip: AttachmentIcon,\n Pause: PauseIcon,\n Pin: PinIcon,\n PinOff: PinOffIcon,\n Play: PlayIcon,\n Plus: PlusSignIcon,\n Puzzle: PuzzleIcon,\n Repeat: RepeatIcon,\n RotateCcw: Refresh01Icon,\n Rows2: LayoutTwoRowIcon,\n Rows3: LayoutThreeRowIcon,\n Search: Search01Icon,\n Sent: SentIcon,\n Settings: Settings01Icon,\n SideChat: MessageAdd02Icon,\n ClosePluginPane: Cancel01Icon,\n CloseThreadPane: Cancel01Icon,\n SlidersHorizontal: SlidersHorizontalIcon,\n Smartphone: SmartPhone01Icon,\n Sort: Sorting01Icon,\n Spinner: DashedLineCircleIcon,\n Square: SquareIcon,\n Star: StarIcon,\n Target: Target02Icon,\n Terminal: ComputerTerminal01Icon,\n TextWrap: TextWrapIcon,\n TimeSchedule: TimeScheduleIcon,\n Toolbox: ToolboxIcon,\n Trash2: Delete02Icon,\n UserRound: UserIcon,\n UserRoundPlus: UserAdd01Icon,\n Workflow: WorkflowCircle03Icon,\n X: Cancel01Icon,\n Zap: ZapIcon,\n ZoomIn: ZoomInAreaIcon,\n ZoomOut: ZoomOutAreaIcon,\n} as const satisfies Record;\n\nexport type IconName = keyof typeof ICON_MAP;\n\nexport const ICON_NAMES = Object.keys(ICON_MAP) as readonly IconName[];\n\nexport interface IconProps {\n name: IconName;\n className?: string;\n \"aria-hidden\"?: boolean | \"true\" | \"false\";\n \"aria-label\"?: string;\n}\n\nexport function Icon({\n name,\n className,\n \"aria-hidden\": ariaHidden,\n \"aria-label\": ariaLabel,\n}: IconProps) {\n return (\n \n );\n}\n", "type": "registry:ui", "target": "components/ui/icon.tsx" } diff --git a/packages/shared-ui/src/components/ui/icon.tsx b/packages/shared-ui/src/components/ui/icon.tsx index 59dfafe40..0e7c1ca82 100644 --- a/packages/shared-ui/src/components/ui/icon.tsx +++ b/packages/shared-ui/src/components/ui/icon.tsx @@ -97,7 +97,6 @@ import { MessageQuestionIcon, Mic02Icon, MoreHorizontalIcon, - MultiplicationSignSquareIcon, PackageReceiveIcon, PauseIcon, PinIcon, @@ -389,8 +388,8 @@ const ICON_MAP = { Sent: SentIcon, Settings: Settings01Icon, SideChat: MessageAdd02Icon, - ClosePluginPane: MultiplicationSignSquareIcon, - CloseThreadPane: MultiplicationSignSquareIcon, + ClosePluginPane: Cancel01Icon, + CloseThreadPane: Cancel01Icon, SlidersHorizontal: SlidersHorizontalIcon, Smartphone: SmartPhone01Icon, Sort: Sorting01Icon, diff --git a/packages/templates/src/generated/plugin-starter-files.generated.ts b/packages/templates/src/generated/plugin-starter-files.generated.ts index 6c0c4ccf9..3b30a647e 100644 --- a/packages/templates/src/generated/plugin-starter-files.generated.ts +++ b/packages/templates/src/generated/plugin-starter-files.generated.ts @@ -44,7 +44,7 @@ export const PLUGIN_STARTER_FILES: readonly PluginStarterFile[] = [ }, { "target": "components/ui/icon.tsx", - "content": "import { HugeiconsIcon, type IconSvgElement } from \"@hugeicons/react\";\nimport {\n AiContentGenerator01Icon,\n Alert02Icon,\n AlertCircleIcon,\n Archive03Icon,\n ArrowDown01Icon,\n ArrowDown02Icon,\n ArrowDownDoubleIcon,\n ArrowLeft01Icon,\n ArrowMoveDownLeftIcon,\n ArrowMoveDownRightIcon,\n ArrowRight01Icon,\n ArrowRight02Icon,\n ArrowReloadHorizontalIcon,\n ArrowUp01Icon,\n ArrowUp02Icon,\n ArrowUpDoubleIcon,\n ArrowUpDownIcon,\n ArrowTurnBackwardIcon,\n ArrowTurnForwardIcon,\n ArrowUpRight01Icon,\n AttachmentIcon,\n AudioWaveIcon,\n BrowserIcon,\n BubbleChatAddIcon,\n BubbleChatIcon,\n BubbleChatQuestionIcon,\n Calendar03Icon,\n CalendarCheckOut02Icon,\n CalendarSyncIcon,\n Cancel01Icon,\n CancelCircleIcon,\n ChatFeedback01Icon,\n Bug01Icon,\n ChartColumnIcon,\n CheckListIcon,\n CheckmarkCircle02Icon,\n CircleArrowShrink01Icon,\n CircleIcon,\n Clock01Icon,\n CloudIcon,\n CollapseIcon,\n Book02Icon,\n BrainIcon,\n ComputerTerminal01Icon,\n Copy01Icon,\n DashedLine02Icon,\n DashedLineCircleIcon,\n DateTimeIcon,\n Delete02Icon,\n Download01Icon,\n DiscordIcon,\n GithubIcon,\n DragDropHorizontalIcon,\n DragDropVerticalIcon,\n Edit02Icon,\n Edit04Icon,\n ElectricPlugsIcon,\n ExpandIcon,\n ViewIcon,\n ViewOffIcon,\n File01Icon,\n FileAttachmentIcon,\n FileEmpty02Icon,\n FileQuestionMarkIcon,\n FileXIcon,\n Folder02Icon,\n FolderAddIcon,\n FolderGitTwoIcon,\n FolderIcon,\n FolderRemoveIcon,\n GitBranchIcon,\n GitForkIcon,\n GitMergeIcon,\n GitPullRequestArrow,\n GitPullRequestClosedIcon,\n GitPullRequestDraftIcon,\n GitPullRequestIcon,\n GridViewIcon,\n HelpCircleIcon,\n InformationCircleIcon,\n InternetIcon,\n LaptopIcon,\n Layers01Icon,\n ListViewIcon,\n LockIcon,\n Loading03Icon,\n LayoutThreeRowIcon,\n LayoutTwoColumnIcon,\n LayoutTwoRowIcon,\n LinkSquare02Icon,\n Mail02Icon,\n MailOpen01Icon,\n Menu02Icon,\n MessageAdd02Icon,\n MessageQuestionIcon,\n Mic02Icon,\n MoreHorizontalIcon,\n MultiplicationSignSquareIcon,\n PackageReceiveIcon,\n PauseIcon,\n PinIcon,\n PinOffIcon,\n PlayIcon,\n PlusMinusSquare01Icon,\n PlusSignIcon,\n PuzzleIcon,\n Refresh01Icon,\n RepeatIcon,\n Search01Icon,\n SentIcon,\n Settings01Icon,\n SidebarBottomIcon,\n SidebarLeftIcon,\n SidebarRightIcon,\n SlidersHorizontalIcon,\n SmartPhone01Icon,\n Sorting01Icon,\n SourceCodeIcon,\n SquareIcon,\n StarIcon,\n Target02Icon,\n TestTube01Icon,\n TextWrapIcon,\n Tick02Icon,\n TimeScheduleIcon,\n ToolboxIcon,\n Unarchive03Icon,\n UserAdd01Icon,\n UserIcon,\n WorkflowCircle03Icon,\n ZapIcon,\n ZoomInAreaIcon,\n ZoomOutAreaIcon,\n} from \"@hugeicons/core-free-icons\";\nimport { cn } from \"../../lib/utils\";\n\n// The free hugeicons set ships no artist-palette glyph (its `Palette` export\n// is a pen nib), so this inlines the stroke-rounded palette artwork in the\n// same element format the set uses.\nconst PaletteStrokeRoundedIcon: IconSvgElement = [\n [\n \"path\",\n {\n d: \"M21.8205 10.4127C22.062 11.8519 22.1827 12.5715 21.2423 13.9326C21.1459 14.0722 20.8966 14.3713 20.777 14.4911C19.6103 15.6586 18.4308 15.6586 16.0716 15.6586H14.1392C13.5085 15.6586 13.1931 15.6586 12.9639 15.7142C11.9586 15.9581 11.3031 16.9391 11.453 17.9755C11.4872 18.2118 11.6043 18.5085 11.8386 19.102C11.9345 19.3449 11.9824 19.4664 12.0136 19.7304C12.1292 20.7084 11.0869 21.9508 10.1158 21.9926C9.85358 22.0039 9.83681 22.0002 9.80326 21.9926C7.66174 21.51 5.66204 20.3123 4.18389 18.4421C0.736789 14.0808 1.43146 7.71364 5.73548 4.22064C10.0395 0.727643 16.323 1.43156 19.7701 5.79289C20.868 7.1819 21.5457 8.77438 21.8205 10.4127Z\",\n fill: \"none\",\n fillRule: \"evenodd\",\n clipRule: \"evenodd\",\n stroke: \"currentColor\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"0\",\n },\n ],\n [\n \"path\",\n {\n d: \"M7.36719 7.74976H7.24219M7.49219 7.74976C7.49219 7.88783 7.38026 7.99976 7.24219 7.99976C7.10412 7.99976 6.99219 7.88783 6.99219 7.74976C6.99219 7.61169 7.10412 7.49976 7.24219 7.49976C7.38026 7.49976 7.49219 7.61169 7.49219 7.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"1\",\n },\n ],\n [\n \"path\",\n {\n d: \"M7.36719 15.7498H7.24219M7.49219 15.7498C7.49219 15.8878 7.38026 15.9998 7.24219 15.9998C7.10412 15.9998 6.99219 15.8878 6.99219 15.7498C6.99219 15.6117 7.10412 15.4998 7.24219 15.4998C7.38026 15.4998 7.49219 15.6117 7.49219 15.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"2\",\n },\n ],\n [\n \"path\",\n {\n d: \"M11.8672 5.74976H11.7422M11.9922 5.74976C11.9922 5.88783 11.8803 5.99976 11.7422 5.99976C11.6041 5.99976 11.4922 5.88783 11.4922 5.74976C11.4922 5.61169 11.6041 5.49976 11.7422 5.49976C11.8803 5.49976 11.9922 5.61169 11.9922 5.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"3\",\n },\n ],\n [\n \"path\",\n {\n d: \"M16.3672 7.74976H16.2422M16.4922 7.74976C16.4922 7.88783 16.3803 7.99976 16.2422 7.99976C16.1041 7.99976 15.9922 7.88783 15.9922 7.74976C15.9922 7.61169 16.1041 7.49976 16.2422 7.49976C16.3803 7.49976 16.4922 7.61169 16.4922 7.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"4\",\n },\n ],\n [\n \"path\",\n {\n d: \"M18.3672 11.7498H18.2422M18.4922 11.7498C18.4922 11.8878 18.3803 11.9998 18.2422 11.9998C18.1041 11.9998 17.9922 11.8878 17.9922 11.7498C17.9922 11.6117 18.1041 11.4998 18.2422 11.4998C18.3803 11.4998 18.4922 11.6117 18.4922 11.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"5\",\n },\n ],\n [\n \"path\",\n {\n d: \"M5.86719 11.7498H5.74219M5.99219 11.7498C5.99219 11.8878 5.88026 11.9998 5.74219 11.9998C5.60412 11.9998 5.49219 11.8878 5.49219 11.7498C5.49219 11.6117 5.60412 11.4998 5.74219 11.4998C5.88026 11.4998 5.99219 11.6117 5.99219 11.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"6\",\n },\n ],\n];\n\n// Custom \"new section\" glyph: the set's ListView rows with the middle and\n// bottom rows shortened so the plus owns the lower-right quadrant, matching\n// FolderAdd's non-overlapping plus placement (same plus geometry). Hugeicons\n// has no list-with-plus variant that keeps the ListView row shape, so this\n// inlines the artwork in the same element format the set uses.\nconst SectionAddStrokeRoundedIcon: IconSvgElement = [\n [\n \"path\",\n {\n d: \"M2 3.4C2 2.24173 2.24173 2 3.4 2H20.6C21.7583 2 22 2.24173 22 3.4V4.6C22 5.75827 21.7583 6 20.6 6H3.4C2.24173 6 2 5.75827 2 4.6V3.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"0\",\n },\n ],\n [\n \"path\",\n {\n d: \"M2 11.4C2 10.2417 2.24173 10 3.4 10H10.6C11.7583 10 12 10.2417 12 11.4V12.6C12 13.7583 11.7583 14 10.6 14H3.4C2.24173 14 2 13.7583 2 12.6V11.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"1\",\n },\n ],\n [\n \"path\",\n {\n d: \"M2 19.4C2 18.2417 2.24173 18 3.4 18H10.6C11.7583 18 12 18.2417 12 19.4V20.6C12 21.7583 11.7583 22 10.6 22H3.4C2.24173 22 2 21.7583 2 20.6V19.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"2\",\n },\n ],\n [\n \"path\",\n {\n d: \"M18 13V21M22 17H14\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"3\",\n },\n ],\n];\n\nconst ICON_MAP = {\n AiContentGenerator01: AiContentGenerator01Icon,\n AlertCircle: AlertCircleIcon,\n AlertTriangle: Alert02Icon,\n AlignLeft: Menu02Icon,\n AppWindow: BrowserIcon,\n Archive: Archive03Icon,\n ArchiveRestore: Unarchive03Icon,\n ArrowDown: ArrowDown02Icon,\n ArrowRight: ArrowRight02Icon,\n ArrowReloadHorizontal: ArrowReloadHorizontalIcon,\n ArrowUp: ArrowUp02Icon,\n ArrowUpDown: ArrowUpDownIcon,\n ArrowTurnBackward: ArrowTurnBackwardIcon,\n ArrowTurnForward: ArrowTurnForwardIcon,\n ArrowUpRight: ArrowUpRight01Icon,\n AudioLines: AudioWaveIcon,\n Beaker: TestTube01Icon,\n BubbleChatQuestion: BubbleChatQuestionIcon,\n Browser: BrowserIcon,\n Brain: BrainIcon,\n Bug: Bug01Icon,\n Calendar: Calendar03Icon,\n CalendarCheckOut02: CalendarCheckOut02Icon,\n CalendarSync: CalendarSyncIcon,\n ChatFeedback: ChatFeedback01Icon,\n ChartColumn: ChartColumnIcon,\n Check: Tick02Icon,\n ChevronDown: ArrowDown01Icon,\n ChevronLeft: ArrowLeft01Icon,\n ChevronRight: ArrowRight01Icon,\n ChevronUp: ArrowUp01Icon,\n ChevronsDown: ArrowDownDoubleIcon,\n ChevronsUp: ArrowUpDoubleIcon,\n Circle: CircleIcon,\n CircleArrowShrink: CircleArrowShrink01Icon,\n CircleCheck: CheckmarkCircle02Icon,\n CircleDashed: DashedLineCircleIcon,\n CircleQuestion: HelpCircleIcon,\n CircleX: CancelCircleIcon,\n Clock: Clock01Icon,\n Code: SourceCodeIcon,\n ComputerTerminal01: ComputerTerminal01Icon,\n Columns2: LayoutTwoColumnIcon,\n Container: CloudIcon,\n Copy: Copy01Icon,\n CornerDownLeft: ArrowMoveDownLeftIcon,\n CornerDownRight: ArrowMoveDownRightIcon,\n Discord: DiscordIcon,\n DateTime: DateTimeIcon,\n Github: GithubIcon,\n DragDropHorizontal: DragDropHorizontalIcon,\n DragDropVertical: DragDropVerticalIcon,\n Download: Download01Icon,\n Edit: Edit02Icon,\n EditFile: Edit04Icon,\n ElectricPlugs: ElectricPlugsIcon,\n Eye: ViewIcon,\n EyeOff: ViewOffIcon,\n Explore: Book02Icon,\n ExternalLink: LinkSquare02Icon,\n FileDiff: PlusMinusSquare01Icon,\n File: FileEmpty02Icon,\n FileAttachment: FileAttachmentIcon,\n FileQuestion: FileQuestionMarkIcon,\n FileText: File01Icon,\n FileX2: FileXIcon,\n Folder: FolderIcon,\n FolderGit: FolderGitTwoIcon,\n FolderOpen: Folder02Icon,\n FolderMinus: FolderRemoveIcon,\n FolderPlus: FolderAddIcon,\n Fork: GitForkIcon,\n GitBranch: GitBranchIcon,\n GitMerge: GitMergeIcon,\n GitPullRequest: GitPullRequestIcon,\n GitPullRequestArrow: GitPullRequestArrow,\n GitPullRequestClosed: GitPullRequestClosedIcon,\n GitPullRequestDraft: GitPullRequestDraftIcon,\n Globe: InternetIcon,\n GridView: GridViewIcon,\n Info: InformationCircleIcon,\n Laptop: LaptopIcon,\n Layers: Layers01Icon,\n ListView: ListViewIcon,\n SectionAdd: SectionAddStrokeRoundedIcon,\n ListTodo: CheckListIcon,\n Loading: Loading03Icon,\n Lock: LockIcon,\n Mail: Mail02Icon,\n MailOpen: MailOpen01Icon,\n Maximize2: ExpandIcon,\n MessageQuestion: MessageQuestionIcon,\n MessageCirclePlus: BubbleChatAddIcon,\n MessageSquarePlus: BubbleChatAddIcon,\n MessageSquare: BubbleChatIcon,\n Mic: Mic02Icon,\n Minimize2: CollapseIcon,\n MoreHorizontal: MoreHorizontalIcon,\n NewTab: DashedLine02Icon,\n PackageReceive: PackageReceiveIcon,\n Palette: PaletteStrokeRoundedIcon,\n PanelBottom: SidebarBottomIcon,\n PanelLeft: SidebarLeftIcon,\n PanelRight: SidebarRightIcon,\n Paperclip: AttachmentIcon,\n Pause: PauseIcon,\n Pin: PinIcon,\n PinOff: PinOffIcon,\n Play: PlayIcon,\n Plus: PlusSignIcon,\n Puzzle: PuzzleIcon,\n Repeat: RepeatIcon,\n RotateCcw: Refresh01Icon,\n Rows2: LayoutTwoRowIcon,\n Rows3: LayoutThreeRowIcon,\n Search: Search01Icon,\n Sent: SentIcon,\n Settings: Settings01Icon,\n SideChat: MessageAdd02Icon,\n ClosePluginPane: MultiplicationSignSquareIcon,\n CloseThreadPane: MultiplicationSignSquareIcon,\n SlidersHorizontal: SlidersHorizontalIcon,\n Smartphone: SmartPhone01Icon,\n Sort: Sorting01Icon,\n Spinner: DashedLineCircleIcon,\n Square: SquareIcon,\n Star: StarIcon,\n Target: Target02Icon,\n Terminal: ComputerTerminal01Icon,\n TextWrap: TextWrapIcon,\n TimeSchedule: TimeScheduleIcon,\n Toolbox: ToolboxIcon,\n Trash2: Delete02Icon,\n UserRound: UserIcon,\n UserRoundPlus: UserAdd01Icon,\n Workflow: WorkflowCircle03Icon,\n X: Cancel01Icon,\n Zap: ZapIcon,\n ZoomIn: ZoomInAreaIcon,\n ZoomOut: ZoomOutAreaIcon,\n} as const satisfies Record;\n\nexport type IconName = keyof typeof ICON_MAP;\n\nexport const ICON_NAMES = Object.keys(ICON_MAP) as readonly IconName[];\n\nexport interface IconProps {\n name: IconName;\n className?: string;\n \"aria-hidden\"?: boolean | \"true\" | \"false\";\n \"aria-label\"?: string;\n}\n\nexport function Icon({\n name,\n className,\n \"aria-hidden\": ariaHidden,\n \"aria-label\": ariaLabel,\n}: IconProps) {\n return (\n \n );\n}\n" + "content": "import { HugeiconsIcon, type IconSvgElement } from \"@hugeicons/react\";\nimport {\n AiContentGenerator01Icon,\n Alert02Icon,\n AlertCircleIcon,\n Archive03Icon,\n ArrowDown01Icon,\n ArrowDown02Icon,\n ArrowDownDoubleIcon,\n ArrowLeft01Icon,\n ArrowMoveDownLeftIcon,\n ArrowMoveDownRightIcon,\n ArrowRight01Icon,\n ArrowRight02Icon,\n ArrowReloadHorizontalIcon,\n ArrowUp01Icon,\n ArrowUp02Icon,\n ArrowUpDoubleIcon,\n ArrowUpDownIcon,\n ArrowTurnBackwardIcon,\n ArrowTurnForwardIcon,\n ArrowUpRight01Icon,\n AttachmentIcon,\n AudioWaveIcon,\n BrowserIcon,\n BubbleChatAddIcon,\n BubbleChatIcon,\n BubbleChatQuestionIcon,\n Calendar03Icon,\n CalendarCheckOut02Icon,\n CalendarSyncIcon,\n Cancel01Icon,\n CancelCircleIcon,\n ChatFeedback01Icon,\n Bug01Icon,\n ChartColumnIcon,\n CheckListIcon,\n CheckmarkCircle02Icon,\n CircleArrowShrink01Icon,\n CircleIcon,\n Clock01Icon,\n CloudIcon,\n CollapseIcon,\n Book02Icon,\n BrainIcon,\n ComputerTerminal01Icon,\n Copy01Icon,\n DashedLine02Icon,\n DashedLineCircleIcon,\n DateTimeIcon,\n Delete02Icon,\n Download01Icon,\n DiscordIcon,\n GithubIcon,\n DragDropHorizontalIcon,\n DragDropVerticalIcon,\n Edit02Icon,\n Edit04Icon,\n ElectricPlugsIcon,\n ExpandIcon,\n ViewIcon,\n ViewOffIcon,\n File01Icon,\n FileAttachmentIcon,\n FileEmpty02Icon,\n FileQuestionMarkIcon,\n FileXIcon,\n Folder02Icon,\n FolderAddIcon,\n FolderGitTwoIcon,\n FolderIcon,\n FolderRemoveIcon,\n GitBranchIcon,\n GitForkIcon,\n GitMergeIcon,\n GitPullRequestArrow,\n GitPullRequestClosedIcon,\n GitPullRequestDraftIcon,\n GitPullRequestIcon,\n GridViewIcon,\n HelpCircleIcon,\n InformationCircleIcon,\n InternetIcon,\n LaptopIcon,\n Layers01Icon,\n ListViewIcon,\n LockIcon,\n Loading03Icon,\n LayoutThreeRowIcon,\n LayoutTwoColumnIcon,\n LayoutTwoRowIcon,\n LinkSquare02Icon,\n Mail02Icon,\n MailOpen01Icon,\n Menu02Icon,\n MessageAdd02Icon,\n MessageQuestionIcon,\n Mic02Icon,\n MoreHorizontalIcon,\n PackageReceiveIcon,\n PauseIcon,\n PinIcon,\n PinOffIcon,\n PlayIcon,\n PlusMinusSquare01Icon,\n PlusSignIcon,\n PuzzleIcon,\n Refresh01Icon,\n RepeatIcon,\n Search01Icon,\n SentIcon,\n Settings01Icon,\n SidebarBottomIcon,\n SidebarLeftIcon,\n SidebarRightIcon,\n SlidersHorizontalIcon,\n SmartPhone01Icon,\n Sorting01Icon,\n SourceCodeIcon,\n SquareIcon,\n StarIcon,\n Target02Icon,\n TestTube01Icon,\n TextWrapIcon,\n Tick02Icon,\n TimeScheduleIcon,\n ToolboxIcon,\n Unarchive03Icon,\n UserAdd01Icon,\n UserIcon,\n WorkflowCircle03Icon,\n ZapIcon,\n ZoomInAreaIcon,\n ZoomOutAreaIcon,\n} from \"@hugeicons/core-free-icons\";\nimport { cn } from \"../../lib/utils\";\n\n// The free hugeicons set ships no artist-palette glyph (its `Palette` export\n// is a pen nib), so this inlines the stroke-rounded palette artwork in the\n// same element format the set uses.\nconst PaletteStrokeRoundedIcon: IconSvgElement = [\n [\n \"path\",\n {\n d: \"M21.8205 10.4127C22.062 11.8519 22.1827 12.5715 21.2423 13.9326C21.1459 14.0722 20.8966 14.3713 20.777 14.4911C19.6103 15.6586 18.4308 15.6586 16.0716 15.6586H14.1392C13.5085 15.6586 13.1931 15.6586 12.9639 15.7142C11.9586 15.9581 11.3031 16.9391 11.453 17.9755C11.4872 18.2118 11.6043 18.5085 11.8386 19.102C11.9345 19.3449 11.9824 19.4664 12.0136 19.7304C12.1292 20.7084 11.0869 21.9508 10.1158 21.9926C9.85358 22.0039 9.83681 22.0002 9.80326 21.9926C7.66174 21.51 5.66204 20.3123 4.18389 18.4421C0.736789 14.0808 1.43146 7.71364 5.73548 4.22064C10.0395 0.727643 16.323 1.43156 19.7701 5.79289C20.868 7.1819 21.5457 8.77438 21.8205 10.4127Z\",\n fill: \"none\",\n fillRule: \"evenodd\",\n clipRule: \"evenodd\",\n stroke: \"currentColor\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"0\",\n },\n ],\n [\n \"path\",\n {\n d: \"M7.36719 7.74976H7.24219M7.49219 7.74976C7.49219 7.88783 7.38026 7.99976 7.24219 7.99976C7.10412 7.99976 6.99219 7.88783 6.99219 7.74976C6.99219 7.61169 7.10412 7.49976 7.24219 7.49976C7.38026 7.49976 7.49219 7.61169 7.49219 7.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"1\",\n },\n ],\n [\n \"path\",\n {\n d: \"M7.36719 15.7498H7.24219M7.49219 15.7498C7.49219 15.8878 7.38026 15.9998 7.24219 15.9998C7.10412 15.9998 6.99219 15.8878 6.99219 15.7498C6.99219 15.6117 7.10412 15.4998 7.24219 15.4998C7.38026 15.4998 7.49219 15.6117 7.49219 15.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"2\",\n },\n ],\n [\n \"path\",\n {\n d: \"M11.8672 5.74976H11.7422M11.9922 5.74976C11.9922 5.88783 11.8803 5.99976 11.7422 5.99976C11.6041 5.99976 11.4922 5.88783 11.4922 5.74976C11.4922 5.61169 11.6041 5.49976 11.7422 5.49976C11.8803 5.49976 11.9922 5.61169 11.9922 5.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"3\",\n },\n ],\n [\n \"path\",\n {\n d: \"M16.3672 7.74976H16.2422M16.4922 7.74976C16.4922 7.88783 16.3803 7.99976 16.2422 7.99976C16.1041 7.99976 15.9922 7.88783 15.9922 7.74976C15.9922 7.61169 16.1041 7.49976 16.2422 7.49976C16.3803 7.49976 16.4922 7.61169 16.4922 7.74976Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"4\",\n },\n ],\n [\n \"path\",\n {\n d: \"M18.3672 11.7498H18.2422M18.4922 11.7498C18.4922 11.8878 18.3803 11.9998 18.2422 11.9998C18.1041 11.9998 17.9922 11.8878 17.9922 11.7498C17.9922 11.6117 18.1041 11.4998 18.2422 11.4998C18.3803 11.4998 18.4922 11.6117 18.4922 11.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"5\",\n },\n ],\n [\n \"path\",\n {\n d: \"M5.86719 11.7498H5.74219M5.99219 11.7498C5.99219 11.8878 5.88026 11.9998 5.74219 11.9998C5.60412 11.9998 5.49219 11.8878 5.49219 11.7498C5.49219 11.6117 5.60412 11.4998 5.74219 11.4998C5.88026 11.4998 5.99219 11.6117 5.99219 11.7498Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n strokeWidth: \"1.5\",\n key: \"6\",\n },\n ],\n];\n\n// Custom \"new section\" glyph: the set's ListView rows with the middle and\n// bottom rows shortened so the plus owns the lower-right quadrant, matching\n// FolderAdd's non-overlapping plus placement (same plus geometry). Hugeicons\n// has no list-with-plus variant that keeps the ListView row shape, so this\n// inlines the artwork in the same element format the set uses.\nconst SectionAddStrokeRoundedIcon: IconSvgElement = [\n [\n \"path\",\n {\n d: \"M2 3.4C2 2.24173 2.24173 2 3.4 2H20.6C21.7583 2 22 2.24173 22 3.4V4.6C22 5.75827 21.7583 6 20.6 6H3.4C2.24173 6 2 5.75827 2 4.6V3.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"0\",\n },\n ],\n [\n \"path\",\n {\n d: \"M2 11.4C2 10.2417 2.24173 10 3.4 10H10.6C11.7583 10 12 10.2417 12 11.4V12.6C12 13.7583 11.7583 14 10.6 14H3.4C2.24173 14 2 13.7583 2 12.6V11.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"1\",\n },\n ],\n [\n \"path\",\n {\n d: \"M2 19.4C2 18.2417 2.24173 18 3.4 18H10.6C11.7583 18 12 18.2417 12 19.4V20.6C12 21.7583 11.7583 22 10.6 22H3.4C2.24173 22 2 21.7583 2 20.6V19.4Z\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"2\",\n },\n ],\n [\n \"path\",\n {\n d: \"M18 13V21M22 17H14\",\n stroke: \"currentColor\",\n strokeLinecap: \"round\",\n strokeWidth: \"1.5\",\n key: \"3\",\n },\n ],\n];\n\nconst ICON_MAP = {\n AiContentGenerator01: AiContentGenerator01Icon,\n AlertCircle: AlertCircleIcon,\n AlertTriangle: Alert02Icon,\n AlignLeft: Menu02Icon,\n AppWindow: BrowserIcon,\n Archive: Archive03Icon,\n ArchiveRestore: Unarchive03Icon,\n ArrowDown: ArrowDown02Icon,\n ArrowRight: ArrowRight02Icon,\n ArrowReloadHorizontal: ArrowReloadHorizontalIcon,\n ArrowUp: ArrowUp02Icon,\n ArrowUpDown: ArrowUpDownIcon,\n ArrowTurnBackward: ArrowTurnBackwardIcon,\n ArrowTurnForward: ArrowTurnForwardIcon,\n ArrowUpRight: ArrowUpRight01Icon,\n AudioLines: AudioWaveIcon,\n Beaker: TestTube01Icon,\n BubbleChatQuestion: BubbleChatQuestionIcon,\n Browser: BrowserIcon,\n Brain: BrainIcon,\n Bug: Bug01Icon,\n Calendar: Calendar03Icon,\n CalendarCheckOut02: CalendarCheckOut02Icon,\n CalendarSync: CalendarSyncIcon,\n ChatFeedback: ChatFeedback01Icon,\n ChartColumn: ChartColumnIcon,\n Check: Tick02Icon,\n ChevronDown: ArrowDown01Icon,\n ChevronLeft: ArrowLeft01Icon,\n ChevronRight: ArrowRight01Icon,\n ChevronUp: ArrowUp01Icon,\n ChevronsDown: ArrowDownDoubleIcon,\n ChevronsUp: ArrowUpDoubleIcon,\n Circle: CircleIcon,\n CircleArrowShrink: CircleArrowShrink01Icon,\n CircleCheck: CheckmarkCircle02Icon,\n CircleDashed: DashedLineCircleIcon,\n CircleQuestion: HelpCircleIcon,\n CircleX: CancelCircleIcon,\n Clock: Clock01Icon,\n Code: SourceCodeIcon,\n ComputerTerminal01: ComputerTerminal01Icon,\n Columns2: LayoutTwoColumnIcon,\n Container: CloudIcon,\n Copy: Copy01Icon,\n CornerDownLeft: ArrowMoveDownLeftIcon,\n CornerDownRight: ArrowMoveDownRightIcon,\n Discord: DiscordIcon,\n DateTime: DateTimeIcon,\n Github: GithubIcon,\n DragDropHorizontal: DragDropHorizontalIcon,\n DragDropVertical: DragDropVerticalIcon,\n Download: Download01Icon,\n Edit: Edit02Icon,\n EditFile: Edit04Icon,\n ElectricPlugs: ElectricPlugsIcon,\n Eye: ViewIcon,\n EyeOff: ViewOffIcon,\n Explore: Book02Icon,\n ExternalLink: LinkSquare02Icon,\n FileDiff: PlusMinusSquare01Icon,\n File: FileEmpty02Icon,\n FileAttachment: FileAttachmentIcon,\n FileQuestion: FileQuestionMarkIcon,\n FileText: File01Icon,\n FileX2: FileXIcon,\n Folder: FolderIcon,\n FolderGit: FolderGitTwoIcon,\n FolderOpen: Folder02Icon,\n FolderMinus: FolderRemoveIcon,\n FolderPlus: FolderAddIcon,\n Fork: GitForkIcon,\n GitBranch: GitBranchIcon,\n GitMerge: GitMergeIcon,\n GitPullRequest: GitPullRequestIcon,\n GitPullRequestArrow: GitPullRequestArrow,\n GitPullRequestClosed: GitPullRequestClosedIcon,\n GitPullRequestDraft: GitPullRequestDraftIcon,\n Globe: InternetIcon,\n GridView: GridViewIcon,\n Info: InformationCircleIcon,\n Laptop: LaptopIcon,\n Layers: Layers01Icon,\n ListView: ListViewIcon,\n SectionAdd: SectionAddStrokeRoundedIcon,\n ListTodo: CheckListIcon,\n Loading: Loading03Icon,\n Lock: LockIcon,\n Mail: Mail02Icon,\n MailOpen: MailOpen01Icon,\n Maximize2: ExpandIcon,\n MessageQuestion: MessageQuestionIcon,\n MessageCirclePlus: BubbleChatAddIcon,\n MessageSquarePlus: BubbleChatAddIcon,\n MessageSquare: BubbleChatIcon,\n Mic: Mic02Icon,\n Minimize2: CollapseIcon,\n MoreHorizontal: MoreHorizontalIcon,\n NewTab: DashedLine02Icon,\n PackageReceive: PackageReceiveIcon,\n Palette: PaletteStrokeRoundedIcon,\n PanelBottom: SidebarBottomIcon,\n PanelLeft: SidebarLeftIcon,\n PanelRight: SidebarRightIcon,\n Paperclip: AttachmentIcon,\n Pause: PauseIcon,\n Pin: PinIcon,\n PinOff: PinOffIcon,\n Play: PlayIcon,\n Plus: PlusSignIcon,\n Puzzle: PuzzleIcon,\n Repeat: RepeatIcon,\n RotateCcw: Refresh01Icon,\n Rows2: LayoutTwoRowIcon,\n Rows3: LayoutThreeRowIcon,\n Search: Search01Icon,\n Sent: SentIcon,\n Settings: Settings01Icon,\n SideChat: MessageAdd02Icon,\n ClosePluginPane: Cancel01Icon,\n CloseThreadPane: Cancel01Icon,\n SlidersHorizontal: SlidersHorizontalIcon,\n Smartphone: SmartPhone01Icon,\n Sort: Sorting01Icon,\n Spinner: DashedLineCircleIcon,\n Square: SquareIcon,\n Star: StarIcon,\n Target: Target02Icon,\n Terminal: ComputerTerminal01Icon,\n TextWrap: TextWrapIcon,\n TimeSchedule: TimeScheduleIcon,\n Toolbox: ToolboxIcon,\n Trash2: Delete02Icon,\n UserRound: UserIcon,\n UserRoundPlus: UserAdd01Icon,\n Workflow: WorkflowCircle03Icon,\n X: Cancel01Icon,\n Zap: ZapIcon,\n ZoomIn: ZoomInAreaIcon,\n ZoomOut: ZoomOutAreaIcon,\n} as const satisfies Record;\n\nexport type IconName = keyof typeof ICON_MAP;\n\nexport const ICON_NAMES = Object.keys(ICON_MAP) as readonly IconName[];\n\nexport interface IconProps {\n name: IconName;\n className?: string;\n \"aria-hidden\"?: boolean | \"true\" | \"false\";\n \"aria-label\"?: string;\n}\n\nexport function Icon({\n name,\n className,\n \"aria-hidden\": ariaHidden,\n \"aria-label\": ariaLabel,\n}: IconProps) {\n return (\n \n );\n}\n" }, { "target": "components/ui/input.tsx",