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}
|
{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>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ export type BadgeTone = 'default' | 'success' | 'warn' | 'danger' | 'info'
|
|||||||
export function Badge({
|
export function Badge({
|
||||||
children,
|
children,
|
||||||
tone = 'default',
|
tone = 'default',
|
||||||
|
size = 'md',
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
tone?: BadgeTone
|
tone?: BadgeTone
|
||||||
|
size?: 'sm' | 'md' | 'lg'
|
||||||
}) {
|
}) {
|
||||||
const tones: Record<BadgeTone, string> = {
|
const tones: Record<BadgeTone, string> = {
|
||||||
default: 'border-[var(--border)] bg-[var(--bg-tertiary)] text-[var(--text-secondary)]',
|
default: 'border-[var(--border)] bg-[var(--bg-tertiary)] text-[var(--text-secondary)]',
|
||||||
@@ -21,8 +23,13 @@ export function Badge({
|
|||||||
info:
|
info:
|
||||||
'border-[color-mix(in_srgb,var(--info)_25%,transparent)] bg-[color-mix(in_srgb,var(--info)_12%,transparent)] text-[var(--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 (
|
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}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { ButtonHTMLAttributes } from 'react'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export type ButtonVariant = 'default' | 'secondary' | 'ghost' | 'danger' | 'outline'
|
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> {
|
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
variant?: ButtonVariant
|
variant?: ButtonVariant
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export { Badge } from './badge'
|
|||||||
export type { BadgeTone } from './badge'
|
export type { BadgeTone } from './badge'
|
||||||
export { Modal } from './modal'
|
export { Modal } from './modal'
|
||||||
export { Table, Th, Td } from './table'
|
export { Table, Th, Td } from './table'
|
||||||
|
export type { TableSize } from './table'
|
||||||
export { Field } from './field'
|
export { Field } from './field'
|
||||||
export { PageHeader } from './page-header'
|
export { PageHeader } from './page-header'
|
||||||
export { Empty } from './empty'
|
export { Empty } from './empty'
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import type { InputHTMLAttributes } from 'react'
|
import type { InputHTMLAttributes } from 'react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export function Input({ className, ...props }: InputHTMLAttributes<HTMLInputElement>) {
|
const heights: Record<string, string> = {
|
||||||
return <input {...props} className={cn('ui-control h-9 px-3', className)} />
|
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}
|
disabled={disabled}
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
aria-expanded={open}
|
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={() => {
|
onClick={() => {
|
||||||
if (disabled) return
|
if (disabled) return
|
||||||
setOpen((v) => !v)
|
setOpen((v) => !v)
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
import { cn } from '@/lib/utils'
|
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({
|
export function Switch({
|
||||||
checked,
|
checked,
|
||||||
onCheckedChange,
|
onCheckedChange,
|
||||||
disabled,
|
disabled,
|
||||||
className,
|
className,
|
||||||
|
size = 'md',
|
||||||
}: {
|
}: {
|
||||||
checked: boolean
|
checked: boolean
|
||||||
onCheckedChange: (v: boolean) => void
|
onCheckedChange: (v: boolean) => void
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
size?: 'sm' | 'md' | 'lg'
|
||||||
}) {
|
}) {
|
||||||
|
const s = sizes[size]
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -18,7 +27,8 @@ export function Switch({
|
|||||||
aria-checked={checked}
|
aria-checked={checked}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={cn(
|
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)]',
|
checked ? 'border-transparent bg-[var(--bg-accent)]' : 'border-[var(--border)] bg-[var(--bg-tertiary)]',
|
||||||
disabled && 'opacity-55 cursor-not-allowed',
|
disabled && 'opacity-55 cursor-not-allowed',
|
||||||
className,
|
className,
|
||||||
@@ -27,8 +37,9 @@ export function Switch({
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-block h-3.5 w-3.5 rounded-full bg-white shadow transition-transform',
|
'inline-block rounded-full bg-white shadow transition-transform',
|
||||||
checked ? 'translate-x-[18px]' : 'translate-x-[3px]',
|
s.thumb,
|
||||||
|
checked ? s.on : s.off,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { cn } from '@/lib/utils'
|
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 }) {
|
export function Table({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<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 (
|
return (
|
||||||
<th
|
<th
|
||||||
className={cn(
|
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,
|
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 (
|
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}
|
{children}
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,12 +5,19 @@ export function Tabs({
|
|||||||
onValueChange,
|
onValueChange,
|
||||||
items,
|
items,
|
||||||
className,
|
className,
|
||||||
|
size = 'md',
|
||||||
}: {
|
}: {
|
||||||
value: string
|
value: string
|
||||||
onValueChange: (v: string) => void
|
onValueChange: (v: string) => void
|
||||||
items: { value: string; label: string }[]
|
items: { value: string; label: string }[]
|
||||||
className?: 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -29,7 +36,8 @@ export function Tabs({
|
|||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={active}
|
aria-selected={active}
|
||||||
className={cn(
|
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)]',
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring)]',
|
||||||
active
|
active
|
||||||
? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-[var(--shadow-sm)] ring-1 ring-[var(--border)]'
|
? '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 type { TextareaHTMLAttributes } from 'react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export function Textarea({ className, ...props }: TextareaHTMLAttributes<HTMLTextAreaElement>) {
|
const sizes: Record<string, string> = {
|
||||||
return <textarea {...props} className={cn('ui-control min-h-24 resize-y px-3 py-2', className)} />
|
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;
|
--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,
|
*::before,
|
||||||
*::after {
|
*::after {
|
||||||
@@ -234,10 +272,10 @@ pre,
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.375rem;
|
gap: var(--ui-gap-md);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
font-size: 14px;
|
font-size: var(--ui-font-md);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
transition:
|
transition:
|
||||||
@@ -310,19 +348,38 @@ pre,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ui-btn-sm {
|
.ui-btn-sm {
|
||||||
height: 2rem;
|
height: var(--ui-height-sm);
|
||||||
padding: 0 0.625rem;
|
padding: 0 var(--ui-px-sm);
|
||||||
font-size: 12px;
|
font-size: var(--ui-font-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-btn-md {
|
.ui-btn-md {
|
||||||
height: 2.25rem;
|
height: var(--ui-height-md);
|
||||||
padding: 0 0.75rem;
|
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 {
|
.ui-btn-icon {
|
||||||
height: 2.25rem;
|
height: var(--ui-height-md);
|
||||||
width: 2.25rem;
|
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;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,9 +414,9 @@ pre,
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: var(--ui-font-sm);
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
min-height: 2rem;
|
min-height: var(--ui-height-sm);
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user