refactor(ui): reconcile common ui adapter

This commit is contained in:
root 2026-05-07 05:13:38 +00:00
parent 799bbf36af
commit 35fbe873e4
2 changed files with 168 additions and 173 deletions

View File

@ -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<CommonButtonProps['variant']> | 'link';
type ProductButtonSize = NonNullable<CommonButtonProps['size']> | 'icon';
type ProductFieldVariant = 'surface' | 'muted';
type ProductFieldSize = 'sm' | 'md';
type ProductBadgeVariant = NonNullable<CommonBadgeProps['variant']> | 'danger';
type ProductStatusTone = 'success' | 'warning' | 'error' | 'info' | 'neutral';
export type { StatusTone };
export interface ButtonProps extends Omit<CommonButtonProps, 'variant' | 'size'> {
variant?: ProductButtonVariant;
size?: ProductButtonSize;
}
export interface IconButtonProps extends Omit<ButtonProps, 'children'> {
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<CommonBadgeProps, 'variant'> {
variant?: ProductBadgeVariant;
}
export type ProductStatus =
| 'active'
@ -190,18 +75,35 @@ export type ProductStatus =
| 'synced'
| 'warning';
const productStatusTone: Record<ProductStatus, StatusTone> = {
const buttonSizeClass: Record<ProductButtonSize, string> = {
sm: '',
md: '',
lg: '',
icon: 'h-10 w-10 px-0',
};
const fieldSizeClass: Record<ProductFieldSize, string> = {
sm: 'min-h-8 px-2 py-1 text-xs',
md: '',
};
const fieldVariantClass: Record<ProductFieldVariant, string> = {
surface: 'bg-[var(--card)] border-[var(--border)] text-[var(--foreground)]',
muted: 'bg-[var(--muted)] border-[var(--border)] text-[var(--foreground)]',
};
const productStatusTone: Record<ProductStatus, ProductStatusTone> = {
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<ProductStatus, StatusTone> = {
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<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', size = 'md', className, ...props }, ref) => (
<CommonButton
ref={ref}
variant={buttonVariantFor(variant)}
size={size === 'icon' ? 'sm' : size}
className={cn(
buttonSizeClass[size],
variant === 'link' && 'h-auto px-0 py-0 underline underline-offset-4 hover:bg-transparent',
className,
)}
{...props}
/>
),
);
Button.displayName = 'Button';
export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
({ icon, label, variant = 'ghost', size = 'icon', className, ...props }, ref) => (
<Button
ref={ref}
type="button"
aria-label={label}
title={props.title ?? label}
variant={variant}
size={size}
className={cn('shrink-0', className)}
{...props}
>
{icon}
</Button>
),
);
IconButton.displayName = 'IconButton';
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ controlSize = 'md', variant = 'surface', className, ...props }, ref) => (
<CommonInput
ref={ref}
className={cn(fieldSizeClass[controlSize], fieldVariantClass[variant], className)}
{...props}
/>
),
);
Input.displayName = 'Input';
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ controlSize = 'md', variant = 'surface', className, ...props }, ref) => (
<CommonSelect
ref={ref}
className={cn(fieldSizeClass[controlSize], fieldVariantClass[variant], className)}
{...props}
/>
),
);
Select.displayName = 'Select';
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ controlSize = 'md', variant = 'surface', className, ...props }, ref) => (
<CommonTextarea
ref={ref}
className={cn(fieldSizeClass[controlSize], fieldVariantClass[variant], className)}
{...props}
/>
),
);
Textarea.displayName = 'Textarea';
export function Badge({ variant = 'neutral', ...props }: BadgeProps) {
return <CommonBadge variant={badgeVariantFor(variant)} {...props} />;
}
export function ProductStatusBadge({
status,
children,
}: {
status: ProductStatus | string | null | undefined;
children?: ReactNode;
children?: React.ReactNode;
}) {
return (
<StatusBadge tone={statusToneFor(status)} dot>
<Badge variant={statusToneFor(status)} dot>
{children ?? status ?? 'Unknown'}
</StatusBadge>
</Badge>
);
}

View File

@ -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