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
|
link-workspace-packages=true
|
||||||
prefer-workspace-packages=true
|
prefer-workspace-packages=true
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
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> = {
|
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
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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)]">
|
||||||
|
|||||||
@ -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
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user