From 29ad325514d360aa7fb283a2f0c8f656e71aca55 Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Fri, 8 May 2026 20:56:05 -0700 Subject: [PATCH] Improve shared UI primitives --- .npmrc | 3 - packages/design-tokens/generated/tokens.css | 38 +++++++++ packages/design-tokens/scripts/generate.ts | 51 ++++++++++++ packages/ui/package.json | 4 + packages/ui/src/components/Button.tsx | 18 ++-- packages/ui/src/components/Card.tsx | 25 ++++-- packages/ui/src/components/DataTable.tsx | 13 +-- packages/ui/src/components/EmptyState.tsx | 8 +- packages/ui/src/components/Field.tsx | 91 +++++++++++++++++++++ packages/ui/src/components/Input.tsx | 13 +-- packages/ui/src/components/Panel.tsx | 24 ++++-- packages/ui/src/components/Select.tsx | 15 ++-- packages/ui/src/components/StatCard.tsx | 8 +- packages/ui/src/components/Textarea.tsx | 9 +- packages/ui/src/index.ts | 16 ++++ 15 files changed, 281 insertions(+), 55 deletions(-) create mode 100644 packages/ui/src/components/Field.tsx diff --git a/.npmrc b/.npmrc index 4f51e760..6c2b9be4 100644 --- a/.npmrc +++ b/.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 diff --git a/packages/design-tokens/generated/tokens.css b/packages/design-tokens/generated/tokens.css index 27d5710b..ea763539 100644 --- a/packages/design-tokens/generated/tokens.css +++ b/packages/design-tokens/generated/tokens.css @@ -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; diff --git a/packages/design-tokens/scripts/generate.ts b/packages/design-tokens/scripts/generate.ts index 3675d12d..dabc7dd8 100644 --- a/packages/design-tokens/scripts/generate.ts +++ b/packages/design-tokens/scripts/generate.ts @@ -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 diff --git a/packages/ui/package.json b/packages/ui/package.json index 29be7b8d..8c085da7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -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" diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/Button.tsx index 73f69cbd..032d1f3d 100644 --- a/packages/ui/src/components/Button.tsx +++ b/packages/ui/src/components/Button.tsx @@ -18,28 +18,28 @@ export const Button = React.forwardRef( 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 = { 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 = { 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 ( diff --git a/packages/ui/src/components/Card.tsx b/packages/ui/src/components/Card.tsx index c6cc3e80..b4b05ae7 100644 --- a/packages/ui/src/components/Card.tsx +++ b/packages/ui/src/components/Card.tsx @@ -9,8 +9,8 @@ export interface CardProps extends React.HTMLAttributes { const paddings: Record = { 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, 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 {} export function CardHeader({ className, children, ...props }: CardHeaderProps) { return ( -
+
{children}
); @@ -60,7 +63,10 @@ export type CardTitleProps = React.ComponentPropsWithoutRef<'h3'>; export function CardTitle({ className, children, ...props }: CardTitleProps) { return (

{children} @@ -72,7 +78,10 @@ export type CardDescriptionProps = React.ComponentPropsWithoutRef<'p'>; export function CardDescription({ className, children, ...props }: CardDescriptionProps) { return ( -

+

{children}

); diff --git a/packages/ui/src/components/DataTable.tsx b/packages/ui/src/components/DataTable.tsx index 78f3273f..5b86076c 100644 --- a/packages/ui/src/components/DataTable.tsx +++ b/packages/ui/src/components/DataTable.tsx @@ -5,7 +5,7 @@ export type DataTableProps = React.TableHTMLAttributes; export function DataTable({ className, children, ...props }: DataTableProps) { return ( -
+
; export function DataTableHeader({ className, ...props }: DataTableHeaderProps) { - return ; + return ; } export type DataTableBodyProps = React.HTMLAttributes; @@ -36,7 +36,7 @@ export type DataTableRowProps = React.HTMLAttributes; export function DataTableRow({ className, ...props }: DataTableRowProps) { return ( ); @@ -47,7 +47,10 @@ export type DataTableHeadProps = React.ThHTMLAttributes; export function DataTableHead({ className, ...props }: DataTableHeadProps) { return (
); @@ -56,5 +59,5 @@ export function DataTableHead({ className, ...props }: DataTableHeadProps) { export type DataTableCellProps = React.TdHTMLAttributes; export function DataTableCell({ className, ...props }: DataTableCellProps) { - return ; + return ; } diff --git a/packages/ui/src/components/EmptyState.tsx b/packages/ui/src/components/EmptyState.tsx index a82d4b15..d27161bf 100644 --- a/packages/ui/src/components/EmptyState.tsx +++ b/packages/ui/src/components/EmptyState.tsx @@ -23,16 +23,16 @@ export function EmptyState({ return (
-
+
{icon ?? }
-

{title}

+

{title}

{description && ( -

+

{description}

)} diff --git a/packages/ui/src/components/Field.tsx b/packages/ui/src/components/Field.tsx new file mode 100644 index 00000000..83d9a0f7 --- /dev/null +++ b/packages/ui/src/components/Field.tsx @@ -0,0 +1,91 @@ +import * as React from 'react'; +import { clsx } from 'clsx'; +import { Label } from './Label.js'; + +export interface FieldProps extends React.HTMLAttributes { + orientation?: 'vertical' | 'horizontal'; + invalid?: boolean; +} + +export function Field({ orientation = 'vertical', invalid, className, ...props }: FieldProps) { + return ( +
+ ); +} + +export type FieldGroupProps = React.HTMLAttributes; + +export function FieldGroup({ className, ...props }: FieldGroupProps) { + return
; +} + +export type FieldContentProps = React.HTMLAttributes; + +export function FieldContent({ className, ...props }: FieldContentProps) { + return
; +} + +export interface FieldLabelProps extends React.ComponentPropsWithoutRef {} + +export function FieldLabel({ className, ...props }: FieldLabelProps) { + return ( +