Improve shared UI primitives
This commit is contained in:
parent
3398574155
commit
29ad325514
3
.npmrc
3
.npmrc
@ -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
|
||||
prefer-workspace-packages=true
|
||||
|
||||
@ -19,6 +19,36 @@
|
||||
--ml-focus-ring: rgba(90,140,255,0.45);
|
||||
--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-body: "DM Sans", "SF Pro Text", sans-serif;
|
||||
--ml-font-mono: "IBM Plex Mono", "SF Mono", monospace;
|
||||
@ -50,10 +80,18 @@
|
||||
--ml-radius-lg: 20px;
|
||||
--ml-radius-xl: 24px;
|
||||
--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-md: 0 12px 28px rgba(0,0,0,0.18);
|
||||
--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-base: 220ms;
|
||||
|
||||
@ -75,6 +75,49 @@ function generateCSS(): string {
|
||||
}
|
||||
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
|
||||
for (const [key, value] of Object.entries(tokens.typography.fontFamily)) {
|
||||
// Swap single quotes → double quotes for CSS
|
||||
@ -99,6 +142,11 @@ function generateCSS(): string {
|
||||
for (const [key, value] of Object.entries(tokens.radius)) {
|
||||
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('');
|
||||
|
||||
// Elevation (--ml-elevation-* to match existing)
|
||||
@ -106,6 +154,9 @@ function generateCSS(): string {
|
||||
if (key === 'none') continue;
|
||||
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('');
|
||||
|
||||
// Motion
|
||||
|
||||
@ -52,6 +52,10 @@
|
||||
"types": "./dist/components/Input.d.ts",
|
||||
"import": "./dist/components/Input.js"
|
||||
},
|
||||
"./field": {
|
||||
"types": "./dist/components/Field.d.ts",
|
||||
"import": "./dist/components/Field.js"
|
||||
},
|
||||
"./textarea": {
|
||||
"types": "./dist/components/Textarea.d.ts",
|
||||
"import": "./dist/components/Textarea.js"
|
||||
|
||||
@ -18,28 +18,28 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
|
||||
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> = {
|
||||
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:
|
||||
'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:
|
||||
'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:
|
||||
'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:
|
||||
'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:
|
||||
'bg-[var(--bl-surface-muted,#252540)] text-[var(--bl-text-primary,#fff)] hover:bg-[var(--bl-surface-card,#1a1a2e)]',
|
||||
link: 'h-auto rounded-none p-0 text-[var(--bl-accent,#5A8CFF)] underline-offset-4 hover:underline',
|
||||
'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-md border border-transparent p-0 text-[var(--bl-accent,#5A8CFF)] underline-offset-4 hover:underline',
|
||||
};
|
||||
|
||||
const sizes: Record<string, string> = {
|
||||
sm: 'h-8 px-3 text-xs gap-1.5',
|
||||
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 (
|
||||
|
||||
@ -9,8 +9,8 @@ export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
|
||||
const paddings: Record<string, string> = {
|
||||
none: '',
|
||||
sm: 'p-3',
|
||||
md: 'p-4',
|
||||
sm: 'p-4',
|
||||
md: 'p-5',
|
||||
lg: 'p-6',
|
||||
};
|
||||
|
||||
@ -23,9 +23,11 @@ export function Card({
|
||||
...props
|
||||
}: CardProps) {
|
||||
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)]',
|
||||
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)]',
|
||||
};
|
||||
|
||||
@ -34,7 +36,8 @@ export function Card({
|
||||
className={clsx(
|
||||
'rounded-xl border',
|
||||
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],
|
||||
className
|
||||
)}
|
||||
@ -49,7 +52,7 @@ export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export function CardHeader({ className, children, ...props }: CardHeaderProps) {
|
||||
return (
|
||||
<div className={clsx('mb-3', className)} {...props}>
|
||||
<div className={clsx('mb-4 flex flex-col gap-1', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@ -60,7 +63,10 @@ export type CardTitleProps = React.ComponentPropsWithoutRef<'h3'>;
|
||||
export function CardTitle({ className, children, ...props }: CardTitleProps) {
|
||||
return (
|
||||
<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}
|
||||
>
|
||||
{children}
|
||||
@ -72,7 +78,10 @@ export type CardDescriptionProps = React.ComponentPropsWithoutRef<'p'>;
|
||||
|
||||
export function CardDescription({ className, children, ...props }: CardDescriptionProps) {
|
||||
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}
|
||||
</p>
|
||||
);
|
||||
|
||||
@ -5,7 +5,7 @@ export type DataTableProps = React.TableHTMLAttributes<HTMLElement>;
|
||||
|
||||
export function DataTable({ className, children, ...props }: DataTableProps) {
|
||||
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
|
||||
className={clsx(
|
||||
'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 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>;
|
||||
@ -36,7 +36,7 @@ export type DataTableRowProps = React.HTMLAttributes<HTMLElement>;
|
||||
export function DataTableRow({ className, ...props }: DataTableRowProps) {
|
||||
return (
|
||||
<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}
|
||||
/>
|
||||
);
|
||||
@ -47,7 +47,10 @@ export type DataTableHeadProps = React.ThHTMLAttributes<HTMLElement>;
|
||||
export function DataTableHead({ className, ...props }: DataTableHeadProps) {
|
||||
return (
|
||||
<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}
|
||||
/>
|
||||
);
|
||||
@ -56,5 +59,5 @@ export function DataTableHead({ className, ...props }: DataTableHeadProps) {
|
||||
export type DataTableCellProps = React.TdHTMLAttributes<HTMLElement>;
|
||||
|
||||
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} />;
|
||||
}
|
||||
|
||||
@ -23,16 +23,16 @@ export function EmptyState({
|
||||
return (
|
||||
<div
|
||||
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
|
||||
)}
|
||||
>
|
||||
<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" />}
|
||||
</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 && (
|
||||
<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}
|
||||
</p>
|
||||
)}
|
||||
|
||||
91
packages/ui/src/components/Field.tsx
Normal file
91
packages/ui/src/components/Field.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -19,20 +19,20 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
const sizes: Record<NonNullable<InputProps['controlSize']>, string> = {
|
||||
sm: 'h-8 px-2.5 text-xs',
|
||||
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> = {
|
||||
surface: 'bg-[var(--bl-surface-card,#1a1a2e)]',
|
||||
surface: 'bg-[var(--bl-input,var(--bl-surface-card,#1a1a2e))]',
|
||||
muted: 'bg-[var(--bl-surface-muted,#252540)]',
|
||||
ghost: 'bg-transparent',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="grid gap-1.5">
|
||||
{label && (
|
||||
<label
|
||||
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>
|
||||
@ -41,12 +41,13 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
ref={ref}
|
||||
id={inputId}
|
||||
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],
|
||||
sizes[controlSize],
|
||||
'text-[var(--bl-text-primary,#fff)]',
|
||||
'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)]',
|
||||
className
|
||||
)}
|
||||
|
||||
@ -7,7 +7,7 @@ export interface PanelProps extends React.HTMLAttributes<HTMLElement> {
|
||||
}
|
||||
|
||||
const panelPadding: Record<NonNullable<PanelProps['density']>, string> = {
|
||||
compact: 'p-3',
|
||||
compact: 'p-4',
|
||||
normal: 'p-5',
|
||||
spacious: 'p-6',
|
||||
};
|
||||
@ -22,7 +22,7 @@ export function Panel({
|
||||
return (
|
||||
<Comp
|
||||
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],
|
||||
className
|
||||
)}
|
||||
@ -37,7 +37,13 @@ export type PanelHeaderProps = React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export function PanelHeader({ className, children, ...props }: PanelHeaderProps) {
|
||||
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}
|
||||
</div>
|
||||
);
|
||||
@ -47,7 +53,7 @@ export type PanelBodyProps = React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export function PanelBody({ className, children, ...props }: PanelBodyProps) {
|
||||
return (
|
||||
<div className={clsx('grid gap-3', className)} {...props}>
|
||||
<div className={clsx('grid gap-4', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@ -58,7 +64,10 @@ export type PanelTitleProps = React.ComponentPropsWithoutRef<'h2'>;
|
||||
export function PanelTitle({ className, children, ...props }: PanelTitleProps) {
|
||||
return (
|
||||
<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}
|
||||
>
|
||||
{children}
|
||||
@ -70,7 +79,10 @@ export type PanelDescriptionProps = React.ComponentPropsWithoutRef<'p'>;
|
||||
|
||||
export function PanelDescription({ className, children, ...props }: PanelDescriptionProps) {
|
||||
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}
|
||||
</p>
|
||||
);
|
||||
|
||||
@ -31,20 +31,20 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
const sizes: Record<NonNullable<SelectProps['controlSize']>, string> = {
|
||||
sm: 'h-8 px-2.5 pr-8 text-xs',
|
||||
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> = {
|
||||
surface: 'bg-[var(--bl-surface-card,#1a1a2e)]',
|
||||
surface: 'bg-[var(--bl-input,var(--bl-surface-card,#1a1a2e))]',
|
||||
muted: 'bg-[var(--bl-surface-muted,#252540)]',
|
||||
ghost: 'bg-transparent',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="grid gap-1.5">
|
||||
{label && (
|
||||
<label
|
||||
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>
|
||||
@ -54,11 +54,12 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
ref={ref}
|
||||
id={selectId}
|
||||
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],
|
||||
sizes[controlSize],
|
||||
'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)]',
|
||||
className
|
||||
)}
|
||||
@ -78,7 +79,7 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
))}
|
||||
</select>
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -17,17 +17,19 @@ export function StatCard({ label, value, trend, trendValue, icon, className }: S
|
||||
return (
|
||||
<div
|
||||
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)]',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<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}
|
||||
</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>
|
||||
{icon && (
|
||||
<div className="rounded-lg p-2 bg-[var(--bl-surface-muted,#252540)] text-[var(--bl-text-secondary,#a0a0b0)]">
|
||||
|
||||
@ -28,11 +28,11 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="grid gap-1.5">
|
||||
{label && (
|
||||
<label
|
||||
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>
|
||||
@ -41,12 +41,13 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
ref={ref}
|
||||
id={textareaId}
|
||||
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],
|
||||
sizes[controlSize],
|
||||
'text-[var(--bl-text-primary,#fff)]',
|
||||
'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)]',
|
||||
className
|
||||
)}
|
||||
|
||||
@ -39,6 +39,22 @@ export {
|
||||
} from './components/StatusBadge.js';
|
||||
export { EmptyState, type EmptyStateProps } from './components/EmptyState.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 { Card, CardHeader, CardTitle, CardDescription, type CardProps } from './components/Card.js';
|
||||
export {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user