feat(ui): add size system (sm/md/lg) + compact mode + remove collapsed dot
Build and Deploy / build-and-push (push) Successful in 53s
Build and Deploy / build-and-push (push) Successful in 53s
This commit is contained in:
@@ -6,9 +6,11 @@ export type BadgeTone = 'default' | 'success' | 'warn' | 'danger' | 'info'
|
||||
export function Badge({
|
||||
children,
|
||||
tone = 'default',
|
||||
size = 'md',
|
||||
}: {
|
||||
children: ReactNode
|
||||
tone?: BadgeTone
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
}) {
|
||||
const tones: Record<BadgeTone, string> = {
|
||||
default: 'border-[var(--border)] bg-[var(--bg-tertiary)] text-[var(--text-secondary)]',
|
||||
@@ -21,8 +23,13 @@ export function Badge({
|
||||
info:
|
||||
'border-[color-mix(in_srgb,var(--info)_25%,transparent)] bg-[color-mix(in_srgb,var(--info)_12%,transparent)] text-[var(--info)]',
|
||||
}
|
||||
const badgeSizes = {
|
||||
sm: 'px-1.5 py-px text-[10px]',
|
||||
md: 'px-2 py-0.5 text-[11px]',
|
||||
lg: 'px-2.5 py-1 text-[12px]',
|
||||
}
|
||||
return (
|
||||
<span className={cn('inline-flex items-center rounded-md border px-2 py-0.5 text-[11px] font-medium', tones[tone])}>
|
||||
<span className={cn('inline-flex items-center rounded-md border font-medium', badgeSizes[size], tones[tone])}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ButtonHTMLAttributes } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export type ButtonVariant = 'default' | 'secondary' | 'ghost' | 'danger' | 'outline'
|
||||
export type ButtonSize = 'sm' | 'md' | 'icon'
|
||||
export type ButtonSize = 'sm' | 'md' | 'lg' | 'icon' | 'icon-sm' | 'icon-lg'
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: ButtonVariant
|
||||
|
||||
@@ -10,6 +10,7 @@ export { Badge } from './badge'
|
||||
export type { BadgeTone } from './badge'
|
||||
export { Modal } from './modal'
|
||||
export { Table, Th, Td } from './table'
|
||||
export type { TableSize } from './table'
|
||||
export { Field } from './field'
|
||||
export { PageHeader } from './page-header'
|
||||
export { Empty } from './empty'
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { InputHTMLAttributes } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function Input({ className, ...props }: InputHTMLAttributes<HTMLInputElement>) {
|
||||
return <input {...props} className={cn('ui-control h-9 px-3', className)} />
|
||||
const heights: Record<string, string> = {
|
||||
sm: 'h-[var(--ui-height-sm)] text-[length:var(--ui-font-sm)] px-[var(--ui-px-sm)]',
|
||||
md: 'h-[var(--ui-height-md)] text-[length:var(--ui-font-md)] px-[var(--ui-px-md)]',
|
||||
lg: 'h-[var(--ui-height-lg)] text-[length:var(--ui-font-lg)] px-[var(--ui-px-lg)]',
|
||||
}
|
||||
|
||||
export function Input({ className, size = 'md', ...props }: InputHTMLAttributes<HTMLInputElement> & { size?: 'sm' | 'md' | 'lg' }) {
|
||||
return <input {...props} className={cn('ui-control', heights[size], className)} />
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ export function Select({
|
||||
disabled={disabled}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
className={cn('ui-control ui-select-trigger h-9 px-3 text-left', open && 'ui-control-open')}
|
||||
className={cn('ui-control ui-select-trigger h-[var(--ui-height-md)] px-[var(--ui-px-md)] text-left text-[length:var(--ui-font-md)]', open && 'ui-control-open')}
|
||||
onClick={() => {
|
||||
if (disabled) return
|
||||
setOpen((v) => !v)
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const sizes = {
|
||||
sm: { track: 'h-4 w-7', thumb: 'h-2.5 w-2.5', on: 'translate-x-[14px]', off: 'translate-x-[2px]' },
|
||||
md: { track: 'h-5 w-9', thumb: 'h-3.5 w-3.5', on: 'translate-x-[18px]', off: 'translate-x-[3px]' },
|
||||
lg: { track: 'h-6 w-11', thumb: 'h-4 w-4', on: 'translate-x-[22px]', off: 'translate-x-[3px]' },
|
||||
}
|
||||
|
||||
export function Switch({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
disabled,
|
||||
className,
|
||||
size = 'md',
|
||||
}: {
|
||||
checked: boolean
|
||||
onCheckedChange: (v: boolean) => void
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
}) {
|
||||
const s = sizes[size]
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -18,7 +27,8 @@ export function Switch({
|
||||
aria-checked={checked}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border transition',
|
||||
'relative inline-flex shrink-0 items-center rounded-full border transition',
|
||||
s.track,
|
||||
checked ? 'border-transparent bg-[var(--bg-accent)]' : 'border-[var(--border)] bg-[var(--bg-tertiary)]',
|
||||
disabled && 'opacity-55 cursor-not-allowed',
|
||||
className,
|
||||
@@ -27,8 +37,9 @@ export function Switch({
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block h-3.5 w-3.5 rounded-full bg-white shadow transition-transform',
|
||||
checked ? 'translate-x-[18px]' : 'translate-x-[3px]',
|
||||
'inline-block rounded-full bg-white shadow transition-transform',
|
||||
s.thumb,
|
||||
checked ? s.on : s.off,
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export type TableSize = 'sm' | 'md' | 'lg'
|
||||
|
||||
const thSizes: Record<TableSize, string> = {
|
||||
sm: 'px-2 py-[var(--ui-table-py-sm)] text-[10px]',
|
||||
md: 'px-3 py-[var(--ui-table-py-md)] text-[11px]',
|
||||
lg: 'px-4 py-[var(--ui-table-py-lg)] text-[12px]',
|
||||
}
|
||||
|
||||
const tdSizes: Record<TableSize, string> = {
|
||||
sm: 'px-2 py-[var(--ui-table-py-sm)] text-[length:var(--ui-font-sm)]',
|
||||
md: 'px-3 py-[var(--ui-table-py-md)] text-[length:var(--ui-font-md)]',
|
||||
lg: 'px-4 py-[var(--ui-table-py-lg)] text-[length:var(--ui-font-lg)]',
|
||||
}
|
||||
|
||||
export function Table({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
@@ -9,11 +23,12 @@ export function Table({ children }: { children: ReactNode }) {
|
||||
)
|
||||
}
|
||||
|
||||
export function Th({ children, className }: { children?: ReactNode; className?: string }) {
|
||||
export function Th({ children, className, size = 'md' }: { children?: ReactNode; className?: string; size?: TableSize }) {
|
||||
return (
|
||||
<th
|
||||
className={cn(
|
||||
'border-b border-[var(--border)] bg-[var(--bg-secondary)] px-3 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--text-muted)]',
|
||||
'border-b border-[var(--border)] bg-[var(--bg-secondary)] font-medium uppercase tracking-wide text-[var(--text-muted)]',
|
||||
thSizes[size],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -22,9 +37,9 @@ export function Th({ children, className }: { children?: ReactNode; className?:
|
||||
)
|
||||
}
|
||||
|
||||
export function Td({ children, className }: { children?: ReactNode; className?: string }) {
|
||||
export function Td({ children, className, size = 'md' }: { children?: ReactNode; className?: string; size?: TableSize }) {
|
||||
return (
|
||||
<td className={cn('border-b border-[var(--border)] px-3 py-2.5 align-middle text-[var(--text-primary)]', className)}>
|
||||
<td className={cn('border-b border-[var(--border)] align-middle text-[var(--text-primary)]', tdSizes[size], className)}>
|
||||
{children}
|
||||
</td>
|
||||
)
|
||||
|
||||
@@ -5,12 +5,19 @@ export function Tabs({
|
||||
onValueChange,
|
||||
items,
|
||||
className,
|
||||
size = 'md',
|
||||
}: {
|
||||
value: string
|
||||
onValueChange: (v: string) => void
|
||||
items: { value: string; label: string }[]
|
||||
className?: string
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
}) {
|
||||
const tabSizes = {
|
||||
sm: 'px-2.5 py-1 text-[12px]',
|
||||
md: 'px-3.5 py-1.5 text-[13px]',
|
||||
lg: 'px-4 py-2 text-[14px]',
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -29,7 +36,8 @@ export function Tabs({
|
||||
role="tab"
|
||||
aria-selected={active}
|
||||
className={cn(
|
||||
'relative shrink-0 rounded-md px-3.5 py-1.5 text-[13px] font-medium whitespace-nowrap transition-colors',
|
||||
'relative shrink-0 rounded-md font-medium whitespace-nowrap transition-colors',
|
||||
tabSizes[size],
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring)]',
|
||||
active
|
||||
? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-[var(--shadow-sm)] ring-1 ring-[var(--border)]'
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { TextareaHTMLAttributes } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function Textarea({ className, ...props }: TextareaHTMLAttributes<HTMLTextAreaElement>) {
|
||||
return <textarea {...props} className={cn('ui-control min-h-24 resize-y px-3 py-2', className)} />
|
||||
const sizes: Record<string, string> = {
|
||||
sm: 'text-[length:var(--ui-font-sm)] px-[var(--ui-px-sm)] py-1.5 min-h-16',
|
||||
md: 'text-[length:var(--ui-font-md)] px-[var(--ui-px-md)] py-2 min-h-24',
|
||||
lg: 'text-[length:var(--ui-font-lg)] px-[var(--ui-px-lg)] py-2.5 min-h-32',
|
||||
}
|
||||
|
||||
export function Textarea({ className, size = 'md', ...props }: TextareaHTMLAttributes<HTMLTextAreaElement> & { size?: 'sm' | 'md' | 'lg' }) {
|
||||
return <textarea {...props} className={cn('ui-control resize-y', sizes[size], className)} />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user