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:
parent
953730ff51
commit
a058419828
91
dashboards/admin-web/.pnpmfile.cjs
Normal file
91
dashboards/admin-web/.pnpmfile.cjs
Normal 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;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -36,6 +36,7 @@
|
|||||||
"@bytelyst/logger": "workspace:*",
|
"@bytelyst/logger": "workspace:*",
|
||||||
"@bytelyst/react-auth": "workspace:*",
|
"@bytelyst/react-auth": "workspace:*",
|
||||||
"@bytelyst/telemetry-client": "workspace:*",
|
"@bytelyst/telemetry-client": "workspace:*",
|
||||||
|
"@bytelyst/ui": "workspace:*",
|
||||||
"@radix-ui/react-slider": "^1.3.6",
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
@import "shadcn/tailwind.css";
|
@import "shadcn/tailwind.css";
|
||||||
|
@import "@bytelyst/design-tokens/css";
|
||||||
@plugin "@tailwindcss/typography";
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|||||||
245
dashboards/admin-web/src/components/ui/Primitives.tsx
Normal file
245
dashboards/admin-web/src/components/ui/Primitives.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user