feat(ui): add size system (sm/md/lg) + compact mode + remove collapsed dot
Build and Deploy / build-and-push (push) Successful in 53s

This commit is contained in:
2026-07-26 20:14:42 +08:00
parent 29319ee2d3
commit a9024ff6f8
11 changed files with 137 additions and 29 deletions
-3
View File
@@ -211,9 +211,6 @@ export default function Layout() {
{active ? <span className="ml-auto h-1.5 w-1.5 rounded-full bg-[var(--bg-accent)]" /> : null}
</>
)}
{sidebarCollapsed && active ? (
<span className="absolute right-1 top-1/2 h-1.5 w-1.5 -translate-y-1/2 rounded-full bg-[var(--bg-accent)]" />
) : null}
</button>
)
})}
+8 -1
View File
@@ -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>
)
+1 -1
View File
@@ -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
+1
View File
@@ -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'
+8 -2
View File
@@ -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)} />
}
+1 -1
View File
@@ -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)
+14 -3
View File
@@ -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>
+19 -4
View File
@@ -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>
)
+9 -1
View File
@@ -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)]'
+8 -2
View File
@@ -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)} />
}
+68 -11
View File
@@ -78,6 +78,44 @@
--info: #2563eb;
}
/* ---- 尺寸系统:默认 md ---- */
:root {
--ui-height-sm: 1.75rem; /* 28px */
--ui-height-md: 2.25rem; /* 36px */
--ui-height-lg: 2.75rem; /* 44px */
--ui-font-sm: 12px;
--ui-font-md: 14px;
--ui-font-lg: 15px;
--ui-px-sm: 0.5rem;
--ui-px-md: 0.75rem;
--ui-px-lg: 1rem;
--ui-gap-sm: 0.25rem;
--ui-gap-md: 0.375rem;
--ui-gap-lg: 0.5rem;
--ui-table-py-sm: 0.375rem;
--ui-table-py-md: 0.625rem;
--ui-table-py-lg: 0.75rem;
}
/* 紧凑模式 */
[data-density='compact'] {
--ui-height-sm: 1.5rem; /* 24px */
--ui-height-md: 1.875rem; /* 30px */
--ui-height-lg: 2.25rem; /* 36px */
--ui-font-sm: 11px;
--ui-font-md: 13px;
--ui-font-lg: 14px;
--ui-px-sm: 0.375rem;
--ui-px-md: 0.5rem;
--ui-px-lg: 0.75rem;
--ui-gap-sm: 0.125rem;
--ui-gap-md: 0.25rem;
--ui-gap-lg: 0.375rem;
--ui-table-py-sm: 0.25rem;
--ui-table-py-md: 0.375rem;
--ui-table-py-lg: 0.5rem;
}
*,
*::before,
*::after {
@@ -234,10 +272,10 @@ pre,
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
gap: var(--ui-gap-md);
border: 1px solid transparent;
border-radius: var(--radius-md);
font-size: 14px;
font-size: var(--ui-font-md);
font-weight: 500;
line-height: 1.2;
transition:
@@ -310,19 +348,38 @@ pre,
}
.ui-btn-sm {
height: 2rem;
padding: 0 0.625rem;
font-size: 12px;
height: var(--ui-height-sm);
padding: 0 var(--ui-px-sm);
font-size: var(--ui-font-sm);
}
.ui-btn-md {
height: 2.25rem;
padding: 0 0.75rem;
height: var(--ui-height-md);
padding: 0 var(--ui-px-md);
font-size: var(--ui-font-md);
}
.ui-btn-lg {
height: var(--ui-height-lg);
padding: 0 var(--ui-px-lg);
font-size: var(--ui-font-lg);
}
.ui-btn-icon {
height: 2.25rem;
width: 2.25rem;
height: var(--ui-height-md);
width: var(--ui-height-md);
padding: 0;
}
.ui-btn-icon-sm {
height: var(--ui-height-sm);
width: var(--ui-height-sm);
padding: 0;
}
.ui-btn-icon-lg {
height: var(--ui-height-lg);
width: var(--ui-height-lg);
padding: 0;
}
@@ -357,9 +414,9 @@ pre,
background: transparent;
color: var(--text-primary);
cursor: pointer;
font-size: 13px;
font-size: var(--ui-font-sm);
line-height: 1.35;
min-height: 2rem;
min-height: var(--ui-height-sm);
padding: 0.5rem 0.75rem;
text-align: left;
}