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:
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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)} />
|
||||
}
|
||||
|
||||
+68
-11
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user