Files
mdm-ordinis/ordinis-admin-ui/src/ui/components/button.tsx
T
Zimin A.N. b94912789f fix(fonts): semantic typography utilities per handoff (7 roles)
Заменяет 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.
2026-05-11 14:31:35 +03:00

110 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 }