diff --git a/web/package-lock.json b/web/package-lock.json index a78c46342..b8fe979dc 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -19,6 +19,7 @@ "@monaco-editor/react": "^4.7.0", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-select": "^2.1.6", @@ -2426,6 +2427,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -5683,6 +5685,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.3.tgz", + "integrity": "sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", diff --git a/web/package.json b/web/package.json index 117e04a3a..9f66fb675 100644 --- a/web/package.json +++ b/web/package.json @@ -28,6 +28,7 @@ "@monaco-editor/react": "^4.7.0", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-select": "^2.1.6", diff --git a/web/src/components/ui/collapsible/CollapsibleBase.tsx b/web/src/components/ui/collapsible/CollapsibleBase.tsx new file mode 100644 index 000000000..86ab87d88 --- /dev/null +++ b/web/src/components/ui/collapsible/CollapsibleBase.tsx @@ -0,0 +1,11 @@ +'use client'; + +import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; + +const Collapsible = CollapsiblePrimitive.Root; + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/web/src/components/ui/sidebar/SidebarCollapsible.tsx b/web/src/components/ui/sidebar/SidebarCollapsible.tsx new file mode 100644 index 000000000..3d0c4c909 --- /dev/null +++ b/web/src/components/ui/sidebar/SidebarCollapsible.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from '../collapsible/CollapsibleBase'; +import { type ISidebarGroup } from './interfaces'; + +export const SidebarGroup: React.FC = React.memo(({ label, items }) => { + return <>; +}); diff --git a/web/src/components/ui/sidebar/SidebarItem.stories.tsx b/web/src/components/ui/sidebar/SidebarItem.stories.tsx new file mode 100644 index 000000000..c4a8b7230 --- /dev/null +++ b/web/src/components/ui/sidebar/SidebarItem.stories.tsx @@ -0,0 +1,84 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { SidebarItem } from './SidebarItem'; +import { HouseModern } from '@/components/ui/icons'; +import { BusterRoutes } from '@/routes'; + +const meta: Meta = { + title: 'UI/Sidebar/SidebarItem', + component: SidebarItem, + parameters: { + layout: 'centered' + }, + argTypes: { + variant: { + control: 'select', + options: ['default', 'emphasized'] + }, + active: { + control: 'boolean' + }, + disabled: { + control: 'boolean' + } + }, + tags: ['autodocs'], + decorators: [ + (Story) => ( +
+ +
+ ) + ] +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'Home', + icon: , + route: BusterRoutes.APP_ROOT, + id: 'home' + } +}; + +export const Emphasized: Story = { + args: { + ...Default.args, + variant: 'emphasized', + id: 'home-emphasized' + } +}; + +export const Active: Story = { + args: { + ...Default.args, + id: 'home-active', + active: true + } +}; + +export const Disabled: Story = { + args: { + ...Default.args, + disabled: true, + id: 'home-disabled' + } +}; + +export const LongText: Story = { + args: { + ...Default.args, + label: + 'This is a very long sidebar item label that should demonstrate text truncation behavior in the component', + id: 'long-text' + }, + decorators: [ + (Story) => ( +
+ +
+ ) + ] +}; diff --git a/web/src/components/ui/sidebar/SidebarItem.tsx b/web/src/components/ui/sidebar/SidebarItem.tsx new file mode 100644 index 000000000..b47f10836 --- /dev/null +++ b/web/src/components/ui/sidebar/SidebarItem.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import Link from 'next/link'; +import { cn } from '@/lib/classMerge'; +import { type ISidebarItem } from './interfaces'; +import { cva, VariantProps } from 'class-variance-authority'; + +const itemVariants = cva( + 'flex items-center gap-2.5 rounded px-1.5 py-1.5 text-sm transition-colors', + { + variants: { + variant: { + default: 'hover:bg-item-hover text-text-default', + emphasized: 'shadow bg-background border border-border text-text-default' + }, + active: { + true: '', + false: '' + }, + disabled: { + true: 'cursor-not-allowed', + false: '' + } + }, + compoundVariants: [ + { + active: true, + disabled: false, + variant: 'default', + className: 'bg-nav-item-select hover:bg-nav-item-select' + }, + { + active: true, + disabled: false, + variant: 'emphasized', + className: 'bg-nav-item-select hover:bg-nav-item-select' + }, + { + active: false, + disabled: true, + variant: 'emphasized', + className: 'bg-nav-item-select hover:bg-nav-item-select' + }, + { + active: false, + disabled: false, + variant: 'emphasized', + className: 'hover:bg-item-hover ' + } + ] + } +); + +export const SidebarItem: React.FC> = React.memo( + ({ label, icon, route, id, disabled = false, active = false, variant = 'default' }) => { + const ItemNode = disabled ? 'div' : Link; + + return ( + + {icon} + {label} + + ); + } +); diff --git a/web/src/components/ui/sidebar/interfaces.ts b/web/src/components/ui/sidebar/interfaces.ts new file mode 100644 index 000000000..f3c491cf0 --- /dev/null +++ b/web/src/components/ui/sidebar/interfaces.ts @@ -0,0 +1,29 @@ +import { BusterRoutes } from '@/routes'; + +export interface ISidebarItem { + label: string; + icon: React.ReactNode; + route: BusterRoutes; + id: string; + disabled?: boolean; + active?: boolean; + onRemove?: () => void; +} + +export interface ISidebarGroup { + label: string; + items: ISidebarItem[]; +} + +export interface ISidebarList { + items: ISidebarItem[]; +} + +type SidebarContent = ISidebarGroup | ISidebarList; + +export interface SidebarProps { + header: React.ReactNode; + content: SidebarContent[]; + footer?: React.ReactNode; + activeItem: string; +} diff --git a/web/src/context/BusterReactQuery/BusterReactQueryAndApi.tsx b/web/src/context/BusterReactQuery/BusterReactQueryAndApi.tsx index 1e82976cb..14093f271 100644 --- a/web/src/context/BusterReactQuery/BusterReactQueryAndApi.tsx +++ b/web/src/context/BusterReactQuery/BusterReactQueryAndApi.tsx @@ -24,7 +24,7 @@ export const BusterReactQueryProvider = ({ children }: { children: React.ReactEl return ( {children} - {/* */} + ); };