b94912789f
Заменяет blanket override из MR !45 на типизированную type scale per design_handoff_ordinis_mdm/README.md "Scale" section. Семь semantic @utility в styles.css: text-title-xl 22/600 — modal title, section header text-title-lg 17/600 — page title в editor text-title-md 15/600 — dictionary card title text-body 13/400 — workhorse: body, buttons, tabs, inputs text-cell 12.5/500 — table cell text text-mono 11/500 — Mono: IDs, dates, FK refs (font-mono baked in) text-cap 10.5/600 — Tektur UPPERCASE caption (font/uppercase baked in) Audit phases: P1: добавил 7 утилит, font/uppercase/tracking baked где надо P2: 43 deterministic codemods (font-mono+text-2xs/[11px] → text-mono, [15/17/22]px+font-semibold → text-title-*, [12.5]px → text-cell, [10.5]px+uppercase+tracking → text-cap) P3: 64 text-sm → text-body (handoff workhorse), 84 text-2xs context-aware (TableCell → text-cell, bare → text-cell, плюс cleanup caps в backticks template literals который Phase 2 пропустил), 25 text-xs (6 → text-mono когда с font-mono, 19 → text-cell), 8 titles text-base/lg → text-title-* P4: убрал --text-2xs override (no users), оставил --text-sm: 13px scoped к @nstart/ui passthrough (см. comment в styles.css — убирается в Stage 3.9) Stats: text-body: 69 | text-cell: 99 | text-mono: 50 | text-cap: 42 text-title-xl: 5 | text-title-lg: 5 | text-title-md: 7 text-sm/text-2xs/text-xs в src/: 0 (только в styles.css комментариях) Поведение всех 277 typography usages теперь явно соответствует handoff — каждое место осознанно выбрано под роль, не плажирующий override.
110 lines
3.3 KiB
TypeScript
110 lines
3.3 KiB
TypeScript
import * as React from 'react'
|
||
import { Slot } from '@radix-ui/react-slot'
|
||
import { cva, type VariantProps } from 'class-variance-authority'
|
||
import { cn } from '@/lib/utils'
|
||
|
||
/**
|
||
* Button — shadcn-стиль, адаптирован под наш token palette.
|
||
*
|
||
* Variants per design handoff:
|
||
* - primary — navy bg + on-accent text (deep coffee → cream).
|
||
* - secondary — surface bg + ink text + line border (default outlined).
|
||
* - ghost — transparent + hover:surface-2.
|
||
* - danger — pink bg + on-accent text (destructive actions).
|
||
* - link — accent text + underline-offset, no bg.
|
||
*
|
||
* Sizes: sm (28px), md (36px default), lg (40px), icon-sm (24px square),
|
||
* icon (32px square).
|
||
*
|
||
* Use {@code asChild} (Radix Slot pattern) для рендера кастомного root
|
||
* элемента — например {@code <Button asChild><Link to="/">…</Link></Button>}.
|
||
*/
|
||
const buttonVariants = cva(
|
||
[
|
||
'inline-flex items-center justify-center gap-2 whitespace-nowrap',
|
||
'rounded-md text-body font-medium transition-colors',
|
||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg',
|
||
'disabled:pointer-events-none disabled:opacity-50',
|
||
'[&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||
].join(' '),
|
||
{
|
||
variants: {
|
||
variant: {
|
||
primary:
|
||
'bg-navy text-on-accent hover:opacity-90 active:opacity-95',
|
||
secondary:
|
||
'border border-line bg-surface text-ink hover:bg-surface-2',
|
||
ghost:
|
||
'text-ink hover:bg-surface-2',
|
||
danger:
|
||
'bg-pink text-on-accent hover:opacity-90 active:opacity-95',
|
||
link:
|
||
'text-accent underline-offset-4 hover:underline',
|
||
},
|
||
size: {
|
||
sm: 'h-7 px-2.5 text-cell',
|
||
md: 'h-9 px-3.5',
|
||
lg: 'h-10 px-4',
|
||
'icon-sm': 'h-6 w-6',
|
||
icon: 'h-8 w-8',
|
||
},
|
||
},
|
||
defaultVariants: {
|
||
variant: 'primary',
|
||
size: 'md',
|
||
},
|
||
},
|
||
)
|
||
|
||
export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
|
||
VariantProps<typeof buttonVariants> & {
|
||
asChild?: boolean
|
||
leftIcon?: React.ReactNode
|
||
rightIcon?: React.ReactNode
|
||
/** Loading state — disables button + replaces leftIcon с inline spinner. */
|
||
loading?: boolean
|
||
}
|
||
|
||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||
function Button(
|
||
{
|
||
className,
|
||
variant,
|
||
size,
|
||
asChild,
|
||
leftIcon,
|
||
rightIcon,
|
||
loading,
|
||
disabled,
|
||
children,
|
||
...props
|
||
},
|
||
ref,
|
||
) {
|
||
const Comp = asChild ? Slot : 'button'
|
||
const renderedLeftIcon = loading ? (
|
||
<svg className="animate-spin size-3.5" viewBox="0 0 24 24" aria-hidden>
|
||
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeOpacity="0.25" strokeWidth="4" />
|
||
<path d="M4 12a8 8 0 0 1 8-8" fill="none" stroke="currentColor" strokeWidth="4" strokeLinecap="round" />
|
||
</svg>
|
||
) : (
|
||
leftIcon
|
||
)
|
||
return (
|
||
<Comp
|
||
ref={ref}
|
||
className={cn(buttonVariants({ variant, size }), className)}
|
||
disabled={disabled || loading}
|
||
aria-busy={loading || undefined}
|
||
{...props}
|
||
>
|
||
{renderedLeftIcon}
|
||
{children}
|
||
{rightIcon}
|
||
</Comp>
|
||
)
|
||
},
|
||
)
|
||
|
||
export { buttonVariants }
|