Improve shared UI primitives

This commit is contained in:
Saravana Achu Mac 2026-05-08 20:56:05 -07:00
parent 3398574155
commit 29ad325514
15 changed files with 281 additions and 55 deletions

3
.npmrc
View File

@ -1,5 +1,2 @@
@bytelyst:registry=http://${GITEA_NPM_HOST:-localhost}:3300/api/packages/ByteLyst/npm/
//localhost:3300/api/packages/ByteLyst/npm/:_authToken=${GITEA_NPM_TOKEN}
strict-ssl=false
link-workspace-packages=true link-workspace-packages=true
prefer-workspace-packages=true prefer-workspace-packages=true

View File

@ -19,6 +19,36 @@
--ml-focus-ring: rgba(90,140,255,0.45); --ml-focus-ring: rgba(90,140,255,0.45);
--ml-overlay-scrim: rgba(5,8,18,0.72); --ml-overlay-scrim: rgba(5,8,18,0.72);
--bl-bg-canvas: var(--ml-bg-canvas);
--bl-bg-elevated: var(--ml-bg-elevated);
--bl-surface-card: var(--ml-surface-card);
--bl-surface-muted: var(--ml-surface-muted);
--bl-surface-highlight: color-mix(in oklab, var(--ml-surface-muted) 82%, white);
--bl-surface-overlay: color-mix(in oklab, var(--ml-bg-canvas) 88%, transparent);
--bl-input: color-mix(in oklab, var(--ml-surface-muted) 76%, var(--ml-bg-canvas));
--bl-border: var(--ml-border-default);
--bl-border-strong: var(--ml-border-strong);
--bl-border-subtle: color-mix(in oklab, var(--ml-border-default) 62%, transparent);
--bl-text-primary: var(--ml-text-primary);
--bl-text-secondary: var(--ml-text-secondary);
--bl-text-tertiary: var(--ml-text-tertiary);
--bl-text-quiet: color-mix(in oklab, var(--ml-text-secondary) 78%, var(--ml-bg-canvas));
--bl-accent: var(--ml-accent-primary);
--bl-accent-foreground: var(--ml-bg-canvas);
--bl-accent-muted: color-mix(in oklab, var(--ml-accent-primary) 16%, transparent);
--bl-info: var(--ml-accent-primary);
--bl-info-muted: color-mix(in oklab, var(--ml-accent-primary) 14%, transparent);
--bl-success: var(--ml-success);
--bl-success-muted: color-mix(in oklab, var(--ml-success) 14%, transparent);
--bl-warning: var(--ml-warning);
--bl-warning-muted: color-mix(in oklab, var(--ml-warning) 14%, transparent);
--bl-danger: var(--ml-danger);
--bl-danger-muted: color-mix(in oklab, var(--ml-danger) 14%, transparent);
--bl-danger-foreground: var(--ml-bg-canvas);
--bl-focus-ring: var(--ml-focus-ring);
--bl-focus-ring-muted: color-mix(in oklab, var(--ml-accent-primary) 18%, transparent);
--bl-overlay-scrim: var(--ml-overlay-scrim);
--ml-font-display: "Space Grotesk", "SF Pro Display", sans-serif; --ml-font-display: "Space Grotesk", "SF Pro Display", sans-serif;
--ml-font-body: "DM Sans", "SF Pro Text", sans-serif; --ml-font-body: "DM Sans", "SF Pro Text", sans-serif;
--ml-font-mono: "IBM Plex Mono", "SF Mono", monospace; --ml-font-mono: "IBM Plex Mono", "SF Mono", monospace;
@ -50,10 +80,18 @@
--ml-radius-lg: 20px; --ml-radius-lg: 20px;
--ml-radius-xl: 24px; --ml-radius-xl: 24px;
--ml-radius-pill: 999px; --ml-radius-pill: 999px;
--bl-radius-control: var(--ml-radius-xs);
--bl-radius-surface: var(--ml-radius-sm);
--bl-radius-card: var(--ml-radius-md);
--bl-radius-panel: var(--ml-radius-lg);
--bl-radius-pill: var(--ml-radius-pill);
--ml-elevation-sm: 0 4px 12px rgba(0,0,0,0.12); --ml-elevation-sm: 0 4px 12px rgba(0,0,0,0.12);
--ml-elevation-md: 0 12px 28px rgba(0,0,0,0.18); --ml-elevation-md: 0 12px 28px rgba(0,0,0,0.18);
--ml-elevation-lg: 0 20px 48px rgba(0,0,0,0.24); --ml-elevation-lg: 0 20px 48px rgba(0,0,0,0.24);
--bl-shadow-sm: var(--ml-elevation-sm);
--bl-shadow-md: var(--ml-elevation-md);
--bl-shadow-lg: var(--ml-elevation-lg);
--ml-motion-fast: 140ms; --ml-motion-fast: 140ms;
--ml-motion-base: 220ms; --ml-motion-base: 220ms;

View File

@ -75,6 +75,49 @@ function generateCSS(): string {
} }
lines.push(''); lines.push('');
// ByteLyst semantic aliases. These are the shared UI contract and map the
// historical MindLyst token names into a product-neutral design system.
lines.push(' --bl-bg-canvas: var(--ml-bg-canvas);');
lines.push(' --bl-bg-elevated: var(--ml-bg-elevated);');
lines.push(' --bl-surface-card: var(--ml-surface-card);');
lines.push(' --bl-surface-muted: var(--ml-surface-muted);');
lines.push(' --bl-surface-highlight: color-mix(in oklab, var(--ml-surface-muted) 82%, white);');
lines.push(' --bl-surface-overlay: color-mix(in oklab, var(--ml-bg-canvas) 88%, transparent);');
lines.push(
' --bl-input: color-mix(in oklab, var(--ml-surface-muted) 76%, var(--ml-bg-canvas));'
);
lines.push(' --bl-border: var(--ml-border-default);');
lines.push(' --bl-border-strong: var(--ml-border-strong);');
lines.push(
' --bl-border-subtle: color-mix(in oklab, var(--ml-border-default) 62%, transparent);'
);
lines.push(' --bl-text-primary: var(--ml-text-primary);');
lines.push(' --bl-text-secondary: var(--ml-text-secondary);');
lines.push(' --bl-text-tertiary: var(--ml-text-tertiary);');
lines.push(
' --bl-text-quiet: color-mix(in oklab, var(--ml-text-secondary) 78%, var(--ml-bg-canvas));'
);
lines.push(' --bl-accent: var(--ml-accent-primary);');
lines.push(' --bl-accent-foreground: var(--ml-bg-canvas);');
lines.push(
' --bl-accent-muted: color-mix(in oklab, var(--ml-accent-primary) 16%, transparent);'
);
lines.push(' --bl-info: var(--ml-accent-primary);');
lines.push(' --bl-info-muted: color-mix(in oklab, var(--ml-accent-primary) 14%, transparent);');
lines.push(' --bl-success: var(--ml-success);');
lines.push(' --bl-success-muted: color-mix(in oklab, var(--ml-success) 14%, transparent);');
lines.push(' --bl-warning: var(--ml-warning);');
lines.push(' --bl-warning-muted: color-mix(in oklab, var(--ml-warning) 14%, transparent);');
lines.push(' --bl-danger: var(--ml-danger);');
lines.push(' --bl-danger-muted: color-mix(in oklab, var(--ml-danger) 14%, transparent);');
lines.push(' --bl-danger-foreground: var(--ml-bg-canvas);');
lines.push(' --bl-focus-ring: var(--ml-focus-ring);');
lines.push(
' --bl-focus-ring-muted: color-mix(in oklab, var(--ml-accent-primary) 18%, transparent);'
);
lines.push(' --bl-overlay-scrim: var(--ml-overlay-scrim);');
lines.push('');
// Typography // Typography
for (const [key, value] of Object.entries(tokens.typography.fontFamily)) { for (const [key, value] of Object.entries(tokens.typography.fontFamily)) {
// Swap single quotes → double quotes for CSS // Swap single quotes → double quotes for CSS
@ -99,6 +142,11 @@ function generateCSS(): string {
for (const [key, value] of Object.entries(tokens.radius)) { for (const [key, value] of Object.entries(tokens.radius)) {
lines.push(` --ml-radius-${key}: ${value}px;`); lines.push(` --ml-radius-${key}: ${value}px;`);
} }
lines.push(' --bl-radius-control: var(--ml-radius-xs);');
lines.push(' --bl-radius-surface: var(--ml-radius-sm);');
lines.push(' --bl-radius-card: var(--ml-radius-md);');
lines.push(' --bl-radius-panel: var(--ml-radius-lg);');
lines.push(' --bl-radius-pill: var(--ml-radius-pill);');
lines.push(''); lines.push('');
// Elevation (--ml-elevation-* to match existing) // Elevation (--ml-elevation-* to match existing)
@ -106,6 +154,9 @@ function generateCSS(): string {
if (key === 'none') continue; if (key === 'none') continue;
lines.push(` --ml-elevation-${key}: ${value};`); lines.push(` --ml-elevation-${key}: ${value};`);
} }
lines.push(' --bl-shadow-sm: var(--ml-elevation-sm);');
lines.push(' --bl-shadow-md: var(--ml-elevation-md);');
lines.push(' --bl-shadow-lg: var(--ml-elevation-lg);');
lines.push(''); lines.push('');
// Motion // Motion

View File

@ -52,6 +52,10 @@
"types": "./dist/components/Input.d.ts", "types": "./dist/components/Input.d.ts",
"import": "./dist/components/Input.js" "import": "./dist/components/Input.js"
}, },
"./field": {
"types": "./dist/components/Field.d.ts",
"import": "./dist/components/Field.js"
},
"./textarea": { "./textarea": {
"types": "./dist/components/Textarea.d.ts", "types": "./dist/components/Textarea.d.ts",
"import": "./dist/components/Textarea.js" "import": "./dist/components/Textarea.js"

View File

@ -18,28 +18,28 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
const Comp = asChild ? Slot : 'button'; const Comp = asChild ? Slot : 'button';
const baseStyles = const baseStyles =
'inline-flex items-center justify-center font-medium rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50'; 'inline-flex shrink-0 items-center justify-center whitespace-nowrap rounded-lg font-semibold tracking-normal transition duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bl-focus-ring,var(--bl-accent,#5A8CFF))] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bl-bg-canvas,#0b0f17)] disabled:pointer-events-none disabled:opacity-50';
const variants: Record<string, string> = { const variants: Record<string, string> = {
primary: primary:
'bg-[var(--bl-accent,#5A8CFF)] text-[var(--bl-accent-foreground,var(--bl-bg-canvas,#0b0f17))] hover:opacity-90', 'border border-transparent bg-[var(--bl-accent,#5A8CFF)] text-[var(--bl-accent-foreground,var(--bl-bg-canvas,#0b0f17))] shadow-sm shadow-black/10 hover:brightness-105 active:brightness-95',
secondary: secondary:
'bg-[var(--bl-surface-card,#1a1a2e)] text-[var(--bl-text-primary,#fff)] border border-[var(--bl-border,#2a2a4a)] hover:bg-[var(--bl-surface-muted,#252540)]', 'border border-[var(--bl-border,#2a2a4a)] bg-[var(--bl-surface-card,#1a1a2e)] text-[var(--bl-text-primary,#fff)] shadow-sm shadow-black/5 hover:border-[var(--bl-border-strong,var(--bl-border,#2a2a4a))] hover:bg-[var(--bl-surface-highlight,var(--bl-surface-muted,#252540))]',
ghost: ghost:
'text-[var(--bl-text-secondary,#a0a0b0)] hover:bg-[var(--bl-surface-muted,#252540)] hover:text-[var(--bl-text-primary,#fff)]', 'border border-transparent text-[var(--bl-text-secondary,#a0a0b0)] hover:bg-[var(--bl-surface-muted,#252540)] hover:text-[var(--bl-text-primary,#fff)]',
destructive: destructive:
'bg-[var(--bl-danger)] text-[var(--bl-danger-foreground,var(--bl-bg-canvas,#0b0f17))] hover:opacity-90', 'border border-transparent bg-[var(--bl-danger)] text-[var(--bl-danger-foreground,#fff)] shadow-sm shadow-black/10 hover:brightness-105 active:brightness-95',
outline: outline:
'border border-[var(--bl-border,#2a2a4a)] text-[var(--bl-text-primary,#fff)] hover:bg-[var(--bl-surface-muted,#252540)]', 'border border-[var(--bl-border,#2a2a4a)] bg-transparent text-[var(--bl-text-primary,#fff)] hover:border-[var(--bl-accent,#5A8CFF)] hover:bg-[var(--bl-accent-muted,var(--bl-surface-muted,#252540))]',
subtle: subtle:
'bg-[var(--bl-surface-muted,#252540)] text-[var(--bl-text-primary,#fff)] hover:bg-[var(--bl-surface-card,#1a1a2e)]', 'border border-transparent bg-[var(--bl-surface-muted,#252540)] text-[var(--bl-text-primary,#fff)] hover:bg-[var(--bl-surface-highlight,var(--bl-surface-card,#1a1a2e))]',
link: 'h-auto rounded-none p-0 text-[var(--bl-accent,#5A8CFF)] underline-offset-4 hover:underline', link: 'h-auto rounded-md border border-transparent p-0 text-[var(--bl-accent,#5A8CFF)] underline-offset-4 hover:underline',
}; };
const sizes: Record<string, string> = { const sizes: Record<string, string> = {
sm: 'h-8 px-3 text-xs gap-1.5', sm: 'h-8 px-3 text-xs gap-1.5',
md: 'h-10 px-4 text-sm gap-2', md: 'h-10 px-4 text-sm gap-2',
lg: 'h-12 px-6 text-base gap-2.5', lg: 'h-11 px-5 text-sm gap-2.5',
}; };
return ( return (

View File

@ -9,8 +9,8 @@ export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
const paddings: Record<string, string> = { const paddings: Record<string, string> = {
none: '', none: '',
sm: 'p-3', sm: 'p-4',
md: 'p-4', md: 'p-5',
lg: 'p-6', lg: 'p-6',
}; };
@ -23,9 +23,11 @@ export function Card({
...props ...props
}: CardProps) { }: CardProps) {
const variants: Record<NonNullable<CardProps['variant']>, string> = { const variants: Record<NonNullable<CardProps['variant']>, string> = {
default: 'bg-[var(--bl-surface-card,#1a1a2e)] border-[var(--bl-border,#2a2a4a)]', default:
'bg-[var(--bl-surface-card,#1a1a2e)] border-[var(--bl-border,#2a2a4a)] shadow-sm shadow-black/[0.04]',
muted: 'bg-[var(--bl-surface-muted,#252540)] border-[var(--bl-border,#2a2a4a)]', muted: 'bg-[var(--bl-surface-muted,#252540)] border-[var(--bl-border,#2a2a4a)]',
elevated: 'bg-[var(--bl-bg-elevated,#12151c)] border-[var(--bl-border,#2a2a4a)] shadow-sm', elevated:
'bg-[var(--bl-bg-elevated,#12151c)] border-[var(--bl-border,#2a2a4a)] shadow-lg shadow-black/10',
outline: 'bg-transparent border-[var(--bl-border,#2a2a4a)]', outline: 'bg-transparent border-[var(--bl-border,#2a2a4a)]',
}; };
@ -34,7 +36,8 @@ export function Card({
className={clsx( className={clsx(
'rounded-xl border', 'rounded-xl border',
variants[variant], variants[variant],
hover && 'transition-colors hover:border-[var(--bl-accent,#5A8CFF)]/40', hover &&
'transition duration-150 hover:-translate-y-0.5 hover:border-[var(--bl-accent,#5A8CFF)] hover:shadow-lg hover:shadow-black/10',
paddings[padding], paddings[padding],
className className
)} )}
@ -49,7 +52,7 @@ export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
export function CardHeader({ className, children, ...props }: CardHeaderProps) { export function CardHeader({ className, children, ...props }: CardHeaderProps) {
return ( return (
<div className={clsx('mb-3', className)} {...props}> <div className={clsx('mb-4 flex flex-col gap-1', className)} {...props}>
{children} {children}
</div> </div>
); );
@ -60,7 +63,10 @@ export type CardTitleProps = React.ComponentPropsWithoutRef<'h3'>;
export function CardTitle({ className, children, ...props }: CardTitleProps) { export function CardTitle({ className, children, ...props }: CardTitleProps) {
return ( return (
<h3 <h3
className={clsx('text-lg font-semibold text-[var(--bl-text-primary,#fff)]', className)} className={clsx(
'm-0 text-base font-semibold leading-6 text-[var(--bl-text-primary,#fff)]',
className
)}
{...props} {...props}
> >
{children} {children}
@ -72,7 +78,10 @@ export type CardDescriptionProps = React.ComponentPropsWithoutRef<'p'>;
export function CardDescription({ className, children, ...props }: CardDescriptionProps) { export function CardDescription({ className, children, ...props }: CardDescriptionProps) {
return ( return (
<p className={clsx('text-sm text-[var(--bl-text-secondary,#a0a0b0)]', className)} {...props}> <p
className={clsx('m-0 text-sm leading-6 text-[var(--bl-text-secondary,#a0a0b0)]', className)}
{...props}
>
{children} {children}
</p> </p>
); );

View File

@ -5,7 +5,7 @@ export type DataTableProps = React.TableHTMLAttributes<HTMLElement>;
export function DataTable({ className, children, ...props }: DataTableProps) { export function DataTable({ className, children, ...props }: DataTableProps) {
return ( return (
<div className="w-full overflow-x-auto rounded-lg border border-[var(--bl-border)]"> <div className="w-full overflow-x-auto rounded-xl border border-[var(--bl-border)] bg-[var(--bl-surface-card)] shadow-sm shadow-black/[0.04]">
<table <table
className={clsx( className={clsx(
'w-full border-collapse text-left text-sm text-[var(--bl-text-primary)]', 'w-full border-collapse text-left text-sm text-[var(--bl-text-primary)]',
@ -22,7 +22,7 @@ export function DataTable({ className, children, ...props }: DataTableProps) {
export type DataTableHeaderProps = React.HTMLAttributes<HTMLElement>; export type DataTableHeaderProps = React.HTMLAttributes<HTMLElement>;
export function DataTableHeader({ className, ...props }: DataTableHeaderProps) { export function DataTableHeader({ className, ...props }: DataTableHeaderProps) {
return <thead className={clsx('bg-[var(--bl-surface-muted)]', className)} {...props} />; return <thead className={clsx('bg-[var(--bl-surface-muted)]/80', className)} {...props} />;
} }
export type DataTableBodyProps = React.HTMLAttributes<HTMLElement>; export type DataTableBodyProps = React.HTMLAttributes<HTMLElement>;
@ -36,7 +36,7 @@ export type DataTableRowProps = React.HTMLAttributes<HTMLElement>;
export function DataTableRow({ className, ...props }: DataTableRowProps) { export function DataTableRow({ className, ...props }: DataTableRowProps) {
return ( return (
<tr <tr
className={clsx('transition-colors hover:bg-[var(--bl-surface-muted)]', className)} className={clsx('transition-colors hover:bg-[var(--bl-surface-muted)]/70', className)}
{...props} {...props}
/> />
); );
@ -47,7 +47,10 @@ export type DataTableHeadProps = React.ThHTMLAttributes<HTMLElement>;
export function DataTableHead({ className, ...props }: DataTableHeadProps) { export function DataTableHead({ className, ...props }: DataTableHeadProps) {
return ( return (
<th <th
className={clsx('px-3 py-2 text-xs font-medium text-[var(--bl-text-secondary)]', className)} className={clsx(
'px-4 py-3 text-xs font-semibold uppercase tracking-[0.08em] text-[var(--bl-text-secondary)]',
className
)}
{...props} {...props}
/> />
); );
@ -56,5 +59,5 @@ export function DataTableHead({ className, ...props }: DataTableHeadProps) {
export type DataTableCellProps = React.TdHTMLAttributes<HTMLElement>; export type DataTableCellProps = React.TdHTMLAttributes<HTMLElement>;
export function DataTableCell({ className, ...props }: DataTableCellProps) { export function DataTableCell({ className, ...props }: DataTableCellProps) {
return <td className={clsx('px-3 py-2 align-middle', className)} {...props} />; return <td className={clsx('px-4 py-3 align-middle', className)} {...props} />;
} }

View File

@ -23,16 +23,16 @@ export function EmptyState({
return ( return (
<div <div
className={clsx( className={clsx(
'flex flex-col items-center justify-center py-16 px-4 text-center', 'flex flex-col items-center justify-center rounded-xl border border-dashed border-[var(--bl-border)] bg-[var(--bl-surface-muted)]/35 px-6 py-14 text-center',
className className
)} )}
> >
<div className="mb-4 text-[var(--bl-text-tertiary,#555)]"> <div className="mb-4 rounded-xl border border-[var(--bl-border)] bg-[var(--bl-surface-card)] p-3 text-[var(--bl-text-tertiary,#555)] shadow-sm shadow-black/[0.04]">
{icon ?? <Inbox className="h-12 w-12" />} {icon ?? <Inbox className="h-12 w-12" />}
</div> </div>
<h3 className="text-lg font-medium text-[var(--bl-text-primary,#fff)]">{title}</h3> <h3 className="m-0 text-base font-semibold text-[var(--bl-text-primary,#fff)]">{title}</h3>
{description && ( {description && (
<p className="mt-2 max-w-sm text-sm text-[var(--bl-text-secondary,#a0a0b0)]"> <p className="mt-2 max-w-sm text-sm leading-6 text-[var(--bl-text-secondary,#a0a0b0)]">
{description} {description}
</p> </p>
)} )}

View File

@ -0,0 +1,91 @@
import * as React from 'react';
import { clsx } from 'clsx';
import { Label } from './Label.js';
export interface FieldProps extends React.HTMLAttributes<HTMLDivElement> {
orientation?: 'vertical' | 'horizontal';
invalid?: boolean;
}
export function Field({ orientation = 'vertical', invalid, className, ...props }: FieldProps) {
return (
<div
data-invalid={invalid ? 'true' : undefined}
data-orientation={orientation}
className={clsx(
'grid gap-2 text-[var(--bl-text-primary)]',
orientation === 'horizontal' && 'items-start sm:grid-cols-[minmax(11rem,16rem)_1fr]',
invalid && 'text-[var(--bl-danger)]',
className
)}
role="group"
{...props}
/>
);
}
export type FieldGroupProps = React.HTMLAttributes<HTMLDivElement>;
export function FieldGroup({ className, ...props }: FieldGroupProps) {
return <div className={clsx('grid gap-5', className)} {...props} />;
}
export type FieldContentProps = React.HTMLAttributes<HTMLDivElement>;
export function FieldContent({ className, ...props }: FieldContentProps) {
return <div className={clsx('grid gap-1.5', className)} {...props} />;
}
export interface FieldLabelProps extends React.ComponentPropsWithoutRef<typeof Label> {}
export function FieldLabel({ className, ...props }: FieldLabelProps) {
return (
<Label
className={clsx(
'text-xs font-semibold uppercase tracking-[0.08em] text-[var(--bl-text-secondary)]',
className
)}
{...props}
/>
);
}
export type FieldTitleProps = React.HTMLAttributes<HTMLDivElement>;
export function FieldTitle({ className, ...props }: FieldTitleProps) {
return (
<div
className={clsx('text-sm font-semibold leading-5 text-[var(--bl-text-primary)]', className)}
{...props}
/>
);
}
export type FieldDescriptionProps = React.ComponentPropsWithoutRef<'p'>;
export function FieldDescription({ className, ...props }: FieldDescriptionProps) {
return (
<p
className={clsx('m-0 text-sm leading-6 text-[var(--bl-text-secondary)]', className)}
{...props}
/>
);
}
export interface FieldErrorProps extends React.ComponentPropsWithoutRef<'p'> {
children?: React.ReactNode;
}
export function FieldError({ className, children, ...props }: FieldErrorProps) {
if (!children) return null;
return (
<p
className={clsx('m-0 text-sm font-medium leading-5 text-[var(--bl-danger)]', className)}
role="alert"
{...props}
>
{children}
</p>
);
}

View File

@ -19,20 +19,20 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
const sizes: Record<NonNullable<InputProps['controlSize']>, string> = { const sizes: Record<NonNullable<InputProps['controlSize']>, string> = {
sm: 'h-8 px-2.5 text-xs', sm: 'h-8 px-2.5 text-xs',
md: 'h-10 px-3 text-sm', md: 'h-10 px-3 text-sm',
lg: 'h-12 px-4 text-base', lg: 'h-11 px-4 text-sm',
}; };
const variants: Record<NonNullable<InputProps['variant']>, string> = { const variants: Record<NonNullable<InputProps['variant']>, string> = {
surface: 'bg-[var(--bl-surface-card,#1a1a2e)]', surface: 'bg-[var(--bl-input,var(--bl-surface-card,#1a1a2e))]',
muted: 'bg-[var(--bl-surface-muted,#252540)]', muted: 'bg-[var(--bl-surface-muted,#252540)]',
ghost: 'bg-transparent', ghost: 'bg-transparent',
}; };
return ( return (
<div className="space-y-1"> <div className="grid gap-1.5">
{label && ( {label && (
<label <label
htmlFor={inputId} htmlFor={inputId}
className="block text-sm font-medium text-[var(--bl-text-secondary,#a0a0b0)]" className="block text-xs font-semibold uppercase tracking-[0.08em] text-[var(--bl-text-secondary,#a0a0b0)]"
> >
{label} {label}
</label> </label>
@ -41,12 +41,13 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
ref={ref} ref={ref}
id={inputId} id={inputId}
className={clsx( className={clsx(
'w-full rounded-md border outline-none transition-colors', 'w-full rounded-lg border shadow-sm shadow-black/[0.03] outline-none transition duration-150',
variants[variant], variants[variant],
sizes[controlSize], sizes[controlSize],
'text-[var(--bl-text-primary,#fff)]', 'text-[var(--bl-text-primary,#fff)]',
'placeholder:text-[var(--bl-text-tertiary,#555)]', 'placeholder:text-[var(--bl-text-tertiary,#555)]',
'focus:ring-2 focus:ring-[var(--bl-accent,#5A8CFF)] focus:ring-offset-0', 'disabled:cursor-not-allowed disabled:opacity-60',
'focus:border-[var(--bl-focus-ring,var(--bl-accent,#5A8CFF))] focus:ring-2 focus:ring-[var(--bl-focus-ring-muted,var(--bl-accent-muted,rgba(90,140,255,0.2)))] focus:ring-offset-0',
error ? 'border-[var(--bl-danger)]' : 'border-[var(--bl-border,#2a2a4a)]', error ? 'border-[var(--bl-danger)]' : 'border-[var(--bl-border,#2a2a4a)]',
className className
)} )}

View File

@ -7,7 +7,7 @@ export interface PanelProps extends React.HTMLAttributes<HTMLElement> {
} }
const panelPadding: Record<NonNullable<PanelProps['density']>, string> = { const panelPadding: Record<NonNullable<PanelProps['density']>, string> = {
compact: 'p-3', compact: 'p-4',
normal: 'p-5', normal: 'p-5',
spacious: 'p-6', spacious: 'p-6',
}; };
@ -22,7 +22,7 @@ export function Panel({
return ( return (
<Comp <Comp
className={clsx( className={clsx(
'rounded-lg border bg-[var(--bl-surface-card)] border-[var(--bl-border)] shadow-sm', 'rounded-xl border border-[var(--bl-border)] bg-[var(--bl-surface-card)] shadow-sm shadow-black/[0.04]',
panelPadding[density], panelPadding[density],
className className
)} )}
@ -37,7 +37,13 @@ export type PanelHeaderProps = React.HTMLAttributes<HTMLDivElement>;
export function PanelHeader({ className, children, ...props }: PanelHeaderProps) { export function PanelHeader({ className, children, ...props }: PanelHeaderProps) {
return ( return (
<div className={clsx('flex items-center justify-between gap-3', className)} {...props}> <div
className={clsx(
'flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-3',
className
)}
{...props}
>
{children} {children}
</div> </div>
); );
@ -47,7 +53,7 @@ export type PanelBodyProps = React.HTMLAttributes<HTMLDivElement>;
export function PanelBody({ className, children, ...props }: PanelBodyProps) { export function PanelBody({ className, children, ...props }: PanelBodyProps) {
return ( return (
<div className={clsx('grid gap-3', className)} {...props}> <div className={clsx('grid gap-4', className)} {...props}>
{children} {children}
</div> </div>
); );
@ -58,7 +64,10 @@ export type PanelTitleProps = React.ComponentPropsWithoutRef<'h2'>;
export function PanelTitle({ className, children, ...props }: PanelTitleProps) { export function PanelTitle({ className, children, ...props }: PanelTitleProps) {
return ( return (
<h2 <h2
className={clsx('m-0 text-base font-semibold text-[var(--bl-text-primary)]', className)} className={clsx(
'm-0 text-base font-semibold leading-6 text-[var(--bl-text-primary)]',
className
)}
{...props} {...props}
> >
{children} {children}
@ -70,7 +79,10 @@ export type PanelDescriptionProps = React.ComponentPropsWithoutRef<'p'>;
export function PanelDescription({ className, children, ...props }: PanelDescriptionProps) { export function PanelDescription({ className, children, ...props }: PanelDescriptionProps) {
return ( return (
<p className={clsx('m-0 text-sm text-[var(--bl-text-secondary)]', className)} {...props}> <p
className={clsx('m-0 text-sm leading-6 text-[var(--bl-text-secondary)]', className)}
{...props}
>
{children} {children}
</p> </p>
); );

View File

@ -31,20 +31,20 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
const sizes: Record<NonNullable<SelectProps['controlSize']>, string> = { const sizes: Record<NonNullable<SelectProps['controlSize']>, string> = {
sm: 'h-8 px-2.5 pr-8 text-xs', sm: 'h-8 px-2.5 pr-8 text-xs',
md: 'h-10 px-3 pr-8 text-sm', md: 'h-10 px-3 pr-8 text-sm',
lg: 'h-12 px-4 pr-9 text-base', lg: 'h-11 px-4 pr-9 text-sm',
}; };
const variants: Record<NonNullable<SelectProps['variant']>, string> = { const variants: Record<NonNullable<SelectProps['variant']>, string> = {
surface: 'bg-[var(--bl-surface-card,#1a1a2e)]', surface: 'bg-[var(--bl-input,var(--bl-surface-card,#1a1a2e))]',
muted: 'bg-[var(--bl-surface-muted,#252540)]', muted: 'bg-[var(--bl-surface-muted,#252540)]',
ghost: 'bg-transparent', ghost: 'bg-transparent',
}; };
return ( return (
<div className="space-y-1"> <div className="grid gap-1.5">
{label && ( {label && (
<label <label
htmlFor={selectId} htmlFor={selectId}
className="block text-sm font-medium text-[var(--bl-text-secondary,#a0a0b0)]" className="block text-xs font-semibold uppercase tracking-[0.08em] text-[var(--bl-text-secondary,#a0a0b0)]"
> >
{label} {label}
</label> </label>
@ -54,11 +54,12 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
ref={ref} ref={ref}
id={selectId} id={selectId}
className={clsx( className={clsx(
'w-full appearance-none rounded-md border outline-none transition-colors', 'w-full appearance-none rounded-lg border shadow-sm shadow-black/[0.03] outline-none transition duration-150',
variants[variant], variants[variant],
sizes[controlSize], sizes[controlSize],
'text-[var(--bl-text-primary,#fff)]', 'text-[var(--bl-text-primary,#fff)]',
'focus:ring-2 focus:ring-[var(--bl-accent,#5A8CFF)] focus:ring-offset-0', 'disabled:cursor-not-allowed disabled:opacity-60',
'focus:border-[var(--bl-focus-ring,var(--bl-accent,#5A8CFF))] focus:ring-2 focus:ring-[var(--bl-focus-ring-muted,var(--bl-accent-muted,rgba(90,140,255,0.2)))] focus:ring-offset-0',
error ? 'border-[var(--bl-danger)]' : 'border-[var(--bl-border,#2a2a4a)]', error ? 'border-[var(--bl-danger)]' : 'border-[var(--bl-border,#2a2a4a)]',
className className
)} )}
@ -78,7 +79,7 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
))} ))}
</select> </select>
<ChevronDown <ChevronDown
className="pointer-events-none absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--bl-text-tertiary,#555)]" className="pointer-events-none absolute right-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--bl-text-tertiary,#555)]"
aria-hidden="true" aria-hidden="true"
/> />
</div> </div>

View File

@ -17,17 +17,19 @@ export function StatCard({ label, value, trend, trendValue, icon, className }: S
return ( return (
<div <div
className={clsx( className={clsx(
'rounded-xl border p-5', 'rounded-xl border p-5 shadow-sm shadow-black/[0.04]',
'bg-[var(--bl-surface-card,#1a1a2e)] border-[var(--bl-border,#2a2a4a)]', 'bg-[var(--bl-surface-card,#1a1a2e)] border-[var(--bl-border,#2a2a4a)]',
className className
)} )}
> >
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div> <div>
<p className="text-xs font-medium text-[var(--bl-text-secondary,#a0a0b0)] mb-1"> <p className="mb-1 text-xs font-semibold uppercase tracking-[0.08em] text-[var(--bl-text-secondary,#a0a0b0)]">
{label} {label}
</p> </p>
<p className="text-2xl font-bold text-[var(--bl-text-primary,#fff)]">{value}</p> <p className="text-2xl font-semibold tracking-tight text-[var(--bl-text-primary,#fff)]">
{value}
</p>
</div> </div>
{icon && ( {icon && (
<div className="rounded-lg p-2 bg-[var(--bl-surface-muted,#252540)] text-[var(--bl-text-secondary,#a0a0b0)]"> <div className="rounded-lg p-2 bg-[var(--bl-surface-muted,#252540)] text-[var(--bl-text-secondary,#a0a0b0)]">

View File

@ -28,11 +28,11 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
}; };
return ( return (
<div className="space-y-1"> <div className="grid gap-1.5">
{label && ( {label && (
<label <label
htmlFor={textareaId} htmlFor={textareaId}
className="block text-sm font-medium text-[var(--bl-text-secondary,#a0a0b0)]" className="block text-xs font-semibold uppercase tracking-[0.08em] text-[var(--bl-text-secondary,#a0a0b0)]"
> >
{label} {label}
</label> </label>
@ -41,12 +41,13 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
ref={ref} ref={ref}
id={textareaId} id={textareaId}
className={clsx( className={clsx(
'w-full resize-y rounded-md border outline-none transition-colors', 'w-full resize-y rounded-lg border shadow-sm shadow-black/[0.03] outline-none transition duration-150',
variants[variant], variants[variant],
sizes[controlSize], sizes[controlSize],
'text-[var(--bl-text-primary,#fff)]', 'text-[var(--bl-text-primary,#fff)]',
'placeholder:text-[var(--bl-text-tertiary,#555)]', 'placeholder:text-[var(--bl-text-tertiary,#555)]',
'focus:ring-2 focus:ring-[var(--bl-accent,#5A8CFF)] focus:ring-offset-0', 'disabled:cursor-not-allowed disabled:opacity-60',
'focus:border-[var(--bl-focus-ring,var(--bl-accent,#5A8CFF))] focus:ring-2 focus:ring-[var(--bl-focus-ring-muted,var(--bl-accent-muted,rgba(90,140,255,0.2)))] focus:ring-offset-0',
error ? 'border-[var(--bl-danger)]' : 'border-[var(--bl-border,#2a2a4a)]', error ? 'border-[var(--bl-danger)]' : 'border-[var(--bl-border,#2a2a4a)]',
className className
)} )}

View File

@ -39,6 +39,22 @@ export {
} from './components/StatusBadge.js'; } from './components/StatusBadge.js';
export { EmptyState, type EmptyStateProps } from './components/EmptyState.js'; export { EmptyState, type EmptyStateProps } from './components/EmptyState.js';
export { Input, type InputProps } from './components/Input.js'; export { Input, type InputProps } from './components/Input.js';
export {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldTitle,
type FieldContentProps,
type FieldDescriptionProps,
type FieldErrorProps,
type FieldGroupProps,
type FieldLabelProps,
type FieldProps,
type FieldTitleProps,
} from './components/Field.js';
export { Textarea, type TextareaProps } from './components/Textarea.js'; export { Textarea, type TextareaProps } from './components/Textarea.js';
export { Card, CardHeader, CardTitle, CardDescription, type CardProps } from './components/Card.js'; export { Card, CardHeader, CardTitle, CardDescription, type CardProps } from './components/Card.js';
export { export {