From a058419828338e3c33410d7635504bc2b493c31a Mon Sep 17 00:00:00 2001 From: root Date: Mon, 11 May 2026 01:51:47 +0000 Subject: [PATCH] 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> --- dashboards/admin-web/.pnpmfile.cjs | 91 +++++++ dashboards/admin-web/package.json | 1 + dashboards/admin-web/src/app/globals.css | 1 + .../src/components/ui/Primitives.tsx | 245 ++++++++++++++++++ 4 files changed, 338 insertions(+) create mode 100644 dashboards/admin-web/.pnpmfile.cjs create mode 100644 dashboards/admin-web/src/components/ui/Primitives.tsx diff --git a/dashboards/admin-web/.pnpmfile.cjs b/dashboards/admin-web/.pnpmfile.cjs new file mode 100644 index 00000000..23815d43 --- /dev/null +++ b/dashboards/admin-web/.pnpmfile.cjs @@ -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; + }, + }, +}; \ No newline at end of file diff --git a/dashboards/admin-web/package.json b/dashboards/admin-web/package.json index 1fc5acdd..c9a72a1c 100644 --- a/dashboards/admin-web/package.json +++ b/dashboards/admin-web/package.json @@ -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", diff --git a/dashboards/admin-web/src/app/globals.css b/dashboards/admin-web/src/app/globals.css index c0d41cdf..7e117b75 100644 --- a/dashboards/admin-web/src/app/globals.css +++ b/dashboards/admin-web/src/app/globals.css @@ -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 *)); diff --git a/dashboards/admin-web/src/components/ui/Primitives.tsx b/dashboards/admin-web/src/components/ui/Primitives.tsx new file mode 100644 index 00000000..c1fa9876 --- /dev/null +++ b/dashboards/admin-web/src/components/ui/Primitives.tsx @@ -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 | 'link'; +type ProductButtonSize = NonNullable | 'icon'; +type ProductFieldVariant = 'surface' | 'muted'; +type ProductFieldSize = 'sm' | 'md'; +type ProductBadgeVariant = NonNullable | 'danger'; +type ProductStatusTone = 'success' | 'warning' | 'error' | 'info' | 'neutral'; + +// Extend interfaces with product-specific props +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; +} + +// 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 = { + 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( + ({ 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?: React.ReactNode; +}) { + return ( + + {children ?? status ?? 'Unknown'} + + ); +}