diff --git a/web/src/components/ui/Primitives.tsx b/web/src/components/ui/Primitives.tsx index 4d0c97b..47a6788 100644 --- a/web/src/components/ui/Primitives.tsx +++ b/web/src/components/ui/Primitives.tsx @@ -1,168 +1,53 @@ -import type { ReactNode } from 'react'; +import * as React from 'react'; import { - AppShell, - AppShellMain, - AppShellMobileToggle, - AppShellNav, - AppShellNavItem, - AppShellOverlay, - AppShellPageHeader, - AppShellSidebar, - AppShellSkipLink, - Badge, - Button, - Card, - CardDescription, - CardHeader, - CardTitle, - Checkbox, - ConfirmDialog, - DataList, - DataListItem, - DataListMeta, - DataTable, - DataTableBody, - DataTableCell, - DataTableHead, - DataTableHeader, - DataTableRow, - DiffCard, - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuRadioGroup, - DropdownMenuSeparator, - DropdownMenuSub, - DropdownMenuSubTrigger, - DropdownMenuTrigger, - EmptyState, - IconButton, - Input, - Label, - ListItemButton, - LoadingSpinner, - Modal, - Panel, - PanelBody, - PanelDescription, - PanelHeader, - PanelTitle, - RadioGroup, - RadioGroupItem, - SegmentedControl, - Select, - Separator, - Sidebar, - SidebarItem, - StatCard, - StatusBadge, - StatusDot, - Surface, - SurfaceList, - SurfaceListItem, - Switch, - Tabs, - TabsContent, - TabsList, - TabsTrigger, - Textarea, - Timeline, - Toast, - ToastProvider, - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, - dismissToast, - toast, - useToast, - type StatusTone, + Badge as CommonBadge, + Button as CommonButton, + Input as CommonInput, + Select as CommonSelect, + Textarea as CommonTextarea, + type BadgeProps as CommonBadgeProps, + type ButtonProps as CommonButtonProps, + type InputProps as CommonInputProps, + type SelectProps as CommonSelectProps, + type TextareaProps as CommonTextareaProps, } from '@bytelyst/ui'; +import { cn } from '../../lib/utils'; -export { - AppShell, - AppShellMain, - AppShellMobileToggle, - AppShellNav, - AppShellNavItem, - AppShellOverlay, - AppShellPageHeader, - AppShellSidebar, - AppShellSkipLink, - Badge, - Button, - Card, - CardDescription, - CardHeader, - CardTitle, - Checkbox, - ConfirmDialog, - DataList, - DataListItem, - DataListMeta, - DataTable, - DataTableBody, - DataTableCell, - DataTableHead, - DataTableHeader, - DataTableRow, - DiffCard, - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuRadioGroup, - DropdownMenuSeparator, - DropdownMenuSub, - DropdownMenuSubTrigger, - DropdownMenuTrigger, - EmptyState, - IconButton, - Input, - Label, - ListItemButton, - LoadingSpinner, - Modal, - Panel, - PanelBody, - PanelDescription, - PanelHeader, - PanelTitle, - RadioGroup, - RadioGroupItem, - SegmentedControl, - Select, - Separator, - Sidebar, - SidebarItem, - StatCard, - StatusBadge, - StatusDot, - Surface, - SurfaceList, - SurfaceListItem, - Switch, - Tabs, - TabsContent, - TabsList, - TabsTrigger, - Textarea, - Timeline, - Toast, - ToastProvider, - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, - dismissToast, - toast, - useToast, -}; +type ProductButtonVariant = NonNullable | 'link'; +type ProductButtonSize = NonNullable | 'icon'; +type ProductFieldVariant = 'surface' | 'muted'; +type ProductFieldSize = 'sm' | 'md'; +type ProductBadgeVariant = NonNullable | 'danger'; +type ProductStatusTone = 'success' | 'warning' | 'error' | 'info' | 'neutral'; -export type { StatusTone }; +export interface ButtonProps extends Omit { + variant?: ProductButtonVariant; + size?: ProductButtonSize; +} + +export interface IconButtonProps extends Omit { + icon: React.ReactNode; + label: string; +} + +export interface InputProps extends CommonInputProps { + controlSize?: ProductFieldSize; + variant?: ProductFieldVariant; +} + +export interface SelectProps extends CommonSelectProps { + controlSize?: ProductFieldSize; + variant?: ProductFieldVariant; +} + +export interface TextareaProps extends CommonTextareaProps { + controlSize?: ProductFieldSize; + variant?: ProductFieldVariant; +} + +export interface BadgeProps extends Omit { + variant?: ProductBadgeVariant; +} export type ProductStatus = | 'active' @@ -190,18 +75,35 @@ export type ProductStatus = | 'synced' | 'warning'; -const productStatusTone: Record = { +const buttonSizeClass: Record = { + sm: '', + md: '', + lg: '', + icon: 'h-10 w-10 px-0', +}; + +const fieldSizeClass: Record = { + sm: 'min-h-8 px-2 py-1 text-xs', + md: '', +}; + +const fieldVariantClass: Record = { + surface: 'bg-[var(--card)] border-[var(--border)] text-[var(--foreground)]', + muted: 'bg-[var(--muted)] border-[var(--border)] text-[var(--foreground)]', +}; + +const productStatusTone: Record = { active: 'success', approved: 'success', - blocked: 'danger', + blocked: 'error', buy: 'success', cancelled: 'neutral', connected: 'success', - danger: 'danger', + danger: 'error', degraded: 'warning', disabled: 'neutral', - error: 'danger', - failed: 'danger', + error: 'error', + failed: 'error', idle: 'neutral', info: 'info', live: 'warning', @@ -210,30 +112,116 @@ const productStatusTone: Record = { ok: 'success', paper: 'info', pending: 'warning', - rejected: 'danger', - sell: 'danger', + rejected: 'error', + sell: 'error', success: 'success', synced: 'success', warning: 'warning', }; -export function statusToneFor(status: ProductStatus | string | null | undefined): StatusTone { - if (!status) return 'neutral'; +function badgeVariantFor(variant?: ProductBadgeVariant): CommonBadgeProps['variant'] | undefined { + if (variant === 'danger') return 'error'; + return variant; +} +function buttonVariantFor(variant?: ProductButtonVariant): CommonButtonProps['variant'] | undefined { + if (variant === 'link') return 'ghost'; + return variant; +} + +export function statusToneFor(status: ProductStatus | string | null | undefined): ProductStatusTone { + if (!status) return 'neutral'; const normalized = status.trim().toLowerCase().replace(/[\s_]+/g, '-') as ProductStatus; return productStatusTone[normalized] ?? 'neutral'; } +export const Button = React.forwardRef( + ({ variant = 'primary', size = 'md', className, ...props }, ref) => ( + + ), +); + +Button.displayName = 'Button'; + +export const IconButton = React.forwardRef( + ({ icon, label, variant = 'ghost', size = 'icon', className, ...props }, ref) => ( + + ), +); + +IconButton.displayName = 'IconButton'; + +export const Input = React.forwardRef( + ({ controlSize = 'md', variant = 'surface', className, ...props }, ref) => ( + + ), +); + +Input.displayName = 'Input'; + +export const Select = React.forwardRef( + ({ controlSize = 'md', variant = 'surface', className, ...props }, ref) => ( + + ), +); + +Select.displayName = 'Select'; + +export const Textarea = React.forwardRef( + ({ controlSize = 'md', variant = 'surface', className, ...props }, ref) => ( + + ), +); + +Textarea.displayName = 'Textarea'; + +export function Badge({ variant = 'neutral', ...props }: BadgeProps) { + return ; +} + export function ProductStatusBadge({ status, children, }: { status: ProductStatus | string | null | undefined; - children?: ReactNode; + children?: React.ReactNode; }) { return ( - + {children ?? status ?? 'Unknown'} - + ); } diff --git a/web/vite.config.ts b/web/vite.config.ts index 7f698c5..def3dc7 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -8,6 +8,7 @@ const monacoEditorPath = path.resolve( __dirname, '../node_modules/.pnpm/monaco-editor@0.55.1/node_modules/monaco-editor', ); +const commonUiSourcePath = '/opt/bytelyst/learning_ai_common_plat/packages/ui/src/index.ts'; // Resolve a @bytelyst/* package: prefer web/node_modules, fall back to vendor/ function bytelystAlias(pkg: string): string { @@ -34,6 +35,12 @@ export default defineConfig({ // Vendor packages that live only in vendor/ (not in web/node_modules/) { find: '@bytelyst/api-client', replacement: bytelystAlias('api-client') }, { find: '@bytelyst/errors', replacement: bytelystAlias('errors') }, + { + find: '@bytelyst/ui', + replacement: fs.existsSync(commonUiSourcePath) + ? commonUiSourcePath + : path.resolve(__dirname, 'node_modules/@bytelyst/ui'), + }, // Monaco is an explicit web dependency, but this workspace often runs // against pnpm's root store without a web/node_modules symlink when the // private mobile registry is unavailable. Keep local worker imports