feat(admin-web): add UX foundation - local package resolution and design tokens

Phase 1 of UX compliance implementation:
- Add .pnpmfile.cjs for local package resolution from common platform
- Install @bytelyst/ui for shared UI components
- Create Primitives.tsx product adapter for type-safe component extensions
- Integrate @bytelyst/design-tokens CSS variables
- Enable design token usage via CSS custom properties

This establishes the foundation for component normalization and
consistent styling across ByteLyst products, following the UX
implementation guide patterns.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
root 2026-05-11 01:51:47 +00:00
parent 953730ff51
commit a058419828
4 changed files with 338 additions and 0 deletions

View File

@ -0,0 +1,91 @@
const fs = require('node:fs');
const path = require('node:path');
const PACKAGE_SCOPE = '@bytelyst/';
const PACKAGE_SOURCE = process.env.BYTELYST_PACKAGE_SOURCE || 'common-plat';
const COMMON_PLAT_ROOT = process.env.BYTELYST_COMMON_PLAT_ROOT || path.resolve(__dirname, '../../..');
const COMMON_PLAT_PACKAGES_ROOT = path.join(COMMON_PLAT_ROOT, 'packages');
const VERSION_CACHE = new Map();
let loggedSource = false;
function packageDirFor(name) {
return name.startsWith(PACKAGE_SCOPE) ? name.slice(PACKAGE_SCOPE.length) : null;
}
function pathIfPackageExists(rootDir, name) {
const packageDir = packageDirFor(name);
if (!packageDir) return null;
const candidate = path.join(rootDir, packageDir);
return fs.existsSync(path.join(candidate, 'package.json')) ? candidate : null;
}
function readPackageVersion(packagePath) {
if (VERSION_CACHE.has(packagePath)) {
return VERSION_CACHE.get(packagePath);
}
try {
const packageJson = JSON.parse(fs.readFileSync(path.join(packagePath, 'package.json'), 'utf8'));
const version = packageJson.version || null;
VERSION_CACHE.set(packagePath, version);
return version;
} catch {
VERSION_CACHE.set(packagePath, null);
return null;
}
}
function resolveSpecifier(name) {
if (!name.startsWith(PACKAGE_SCOPE)) {
return null;
}
const commonPlatPath = pathIfPackageExists(COMMON_PLAT_PACKAGES_ROOT, name);
if (PACKAGE_SOURCE === 'common-plat') {
return commonPlatPath ? 'workspace:*' : null;
}
if (PACKAGE_SOURCE === 'gitea') {
return commonPlatPath ? readPackageVersion(commonPlatPath) : null;
}
return commonPlatPath ? 'workspace:*' : null;
}
function rewriteDependencySet(dependencies = {}) {
for (const dependencyName of Object.keys(dependencies)) {
const rewrittenSpecifier = resolveSpecifier(dependencyName);
if (rewrittenSpecifier) {
dependencies[dependencyName] = rewrittenSpecifier;
}
}
}
function logSourceOnce() {
if (loggedSource) {
return;
}
loggedSource = true;
process.stderr.write(
`[bytelyst] pnpm package source=${PACKAGE_SOURCE} commonPlatRoot=${COMMON_PLAT_ROOT}\n`,
);
}
module.exports = {
hooks: {
readPackage(packageJson) {
logSourceOnce();
if (packageJson.name?.startsWith(PACKAGE_SCOPE)) {
return packageJson;
}
rewriteDependencySet(packageJson.dependencies);
rewriteDependencySet(packageJson.devDependencies);
rewriteDependencySet(packageJson.optionalDependencies);
return packageJson;
},
},
};

View File

@ -36,6 +36,7 @@
"@bytelyst/logger": "workspace:*",
"@bytelyst/react-auth": "workspace:*",
"@bytelyst/telemetry-client": "workspace:*",
"@bytelyst/ui": "workspace:*",
"@radix-ui/react-slider": "^1.3.6",
"@tailwindcss/typography": "^0.5.19",
"bcryptjs": "^3.0.3",

View File

@ -1,6 +1,7 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@bytelyst/design-tokens/css";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:is(.dark *));

View File

@ -0,0 +1,245 @@
import * as React from 'react';
import {
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';
// Re-export all shared primitives from @bytelyst/ui
export {
ActionMenu,
AlertBanner,
DataList,
DataTable,
Drawer,
EmptyState,
EntityCard,
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldTitle,
FilterBar,
FormSection,
MetricCard,
Modal,
PageHeader,
Panel,
PanelBody,
PanelDescription,
PanelHeader,
PanelTitle,
Skeleton,
Timeline,
Toolbar,
// Add other @bytelyst/ui components as needed
} from '@bytelyst/ui';
// Define product-specific variants
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';
// Extend interfaces with product-specific props
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;
}
// Product status mapping for badges
export type ProductStatus =
| 'active'
| 'approved'
| 'blocked'
| 'cancelled'
| 'connected'
| 'danger'
| 'degraded'
| 'disabled'
| 'error'
| 'failed'
| 'idle'
| 'info'
| 'live'
| 'neutral'
| 'off'
| 'ok'
| 'paper'
| 'pending'
| 'rejected'
| 'success'
| 'synced'
| 'warning';
const productStatusTone: Record<ProductStatus, ProductStatusTone> = {
active: 'success',
approved: 'success',
blocked: 'error',
cancelled: 'neutral',
connected: 'success',
danger: 'error',
degraded: 'warning',
disabled: 'neutral',
error: 'error',
failed: 'error',
idle: 'neutral',
info: 'info',
live: 'warning',
neutral: 'neutral',
off: 'neutral',
ok: 'success',
paper: 'info',
pending: 'warning',
rejected: 'error',
success: 'success',
synced: 'success',
warning: 'warning',
};
// Helper function to map product status to tone
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';
}
// Product-specific component implementations
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', size = 'md', className, ...props }, ref) => (
<CommonButton
ref={ref}
variant={variant === 'link' ? 'ghost' : variant}
size={size === 'icon' ? 'sm' : size}
className={cn('product-button', 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}
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(
controlSize === 'sm' ? 'min-h-9 px-3 py-2 text-xs' : 'min-h-11 px-3.5 py-2.5 text-sm',
variant === 'surface' ? 'bg-[var(--bl-input)]' : 'bg-[var(--bl-surface-muted)]',
className
)}
{...props}
/>
)
);
Input.displayName = 'Input';
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ controlSize = 'md', variant = 'surface', className, ...props }, ref) => (
<CommonSelect
ref={ref}
className={cn(
controlSize === 'sm' ? 'min-h-9 px-3 py-2 text-xs' : 'min-h-11 px-3.5 py-2.5 text-sm',
variant === 'surface' ? 'bg-[var(--bl-input)]' : 'bg-[var(--bl-surface-muted)]',
className
)}
{...props}
/>
)
);
Select.displayName = 'Select';
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ controlSize = 'md', variant = 'surface', className, ...props }, ref) => (
<CommonTextarea
ref={ref}
className={cn(
controlSize === 'sm' ? 'min-h-9 px-3 py-2 text-xs' : 'min-h-11 px-3.5 py-2.5 text-sm',
variant === 'surface' ? 'bg-[var(--bl-input)]' : 'bg-[var(--bl-surface-muted)]',
className
)}
{...props}
/>
)
);
Textarea.displayName = 'Textarea';
export function Badge({ variant = 'neutral', ...props }: BadgeProps) {
return <CommonBadge variant={variant === 'danger' ? 'error' : variant} {...props} />;
}
export function ProductStatusBadge({
status,
children,
}: {
status: ProductStatus | string | null | undefined;
children?: React.ReactNode;
}) {
return (
<Badge variant={statusToneFor(status)} dot>
{children ?? status ?? 'Unknown'}
</Badge>
);
}