refactor(Button): add block-list-row layout variant + adopt design-system tokens

- New layout prop with values 'inline' (default) and 'block-list-row'.
  The block-list-row variant bakes in full-width, left-aligned content
  with trailing icon and text-sm, replacing the ~10-class override
  duplicated across FilterGroup, FontList, and similar list-row sites.
- primary variant's three hard-offset shadows now reference the
  shadow-stamp-{rest,hover,pressed} tokens; the 0.0625rem translate
  becomes translate-{x,y}-px.
- Base classes use text-label-mono and duration-normal utilities
  instead of inline 'font-primary font-bold tracking-tight uppercase'
  and 'duration-200'.
- The icon variant's background uses surface-canvas (semantic naming;
  picks up dark-mode automatically via --color-surface).
- text-secondary → text-subtle (avoids collision with the @theme
  --color-secondary token; see earlier styles commit).

New exported type: ButtonLayout.
This commit is contained in:
Ilia Mashkov
2026-05-25 10:19:56 +03:00
parent 4e7f76ecb1
commit 15bb961ccc
2 changed files with 29 additions and 9 deletions
+28 -9
View File
@@ -7,6 +7,7 @@ import { cn } from '$shared/lib';
import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements';
import type {
ButtonLayout,
ButtonSize,
ButtonVariant,
IconPosition,
@@ -23,6 +24,14 @@ interface Props extends HTMLButtonAttributes {
* @default 'md'
*/
size?: ButtonSize;
/**
* Layout shape
* - `inline`: default — content-sized, centered.
* - `block-list-row`: full-width row with the content left-aligned and any
* trailing icon pushed to the right (used for filter-group rows, etc).
* @default 'inline'
*/
layout?: ButtonLayout;
/**
* Icon snippet
*/
@@ -56,6 +65,7 @@ interface Props extends HTMLButtonAttributes {
let {
variant = 'secondary',
size = 'md',
layout = 'inline',
icon,
iconPosition = 'left',
active = false,
@@ -76,10 +86,10 @@ const variantStyles: Record<ButtonVariant, string> = {
'hover:bg-swiss-red/90',
'active:bg-swiss-red/80',
'border border-swiss-red',
'shadow-[0.125rem_0.125rem_0_0_rgba(0,0,0,0.1)]',
'hover:shadow-[0.1875rem_0.1875rem_0_0_rgba(0,0,0,0.15)]',
'active:shadow-[0.0625rem_0.0625rem_0_0_rgba(0,0,0,0.08)]',
'active:translate-x-[0.0625rem] active:translate-y-[0.0625rem]',
'shadow-stamp-rest',
'hover:shadow-stamp-hover',
'active:shadow-stamp-pressed',
'active:translate-x-px active:translate-y-px',
'disabled:bg-neutral-300 dark:disabled:bg-neutral-700',
'disabled:text-neutral-500 dark:disabled:text-neutral-500',
'disabled:border-neutral-300 dark:disabled:border-neutral-700',
@@ -111,7 +121,7 @@ const variantStyles: Record<ButtonVariant, string> = {
),
ghost: cn(
'bg-transparent',
'text-secondary',
'text-subtle',
'border border-transparent',
'hover:bg-transparent dark:hover:bg-transparent',
'hover:text-brand dark:hover:text-brand',
@@ -120,8 +130,8 @@ const variantStyles: Record<ButtonVariant, string> = {
'disabled:cursor-not-allowed',
),
icon: cn(
'bg-surface dark:bg-dark-bg',
'text-secondary',
'surface-canvas',
'text-subtle',
'border border-transparent',
'hover:bg-paper dark:hover:bg-paper',
'hover:text-brand',
@@ -174,12 +184,19 @@ const activeStyles: Partial<Record<ButtonVariant, string>> = {
icon: 'bg-paper dark:bg-paper text-brand border-subtle',
};
const layoutStyles: Record<ButtonLayout, string> = {
inline: '',
/* List-row buttons act as content labels rather than action buttons,
so they bump to `text-sm` regardless of the size prop's default. */
'block-list-row': 'w-full justify-between text-left text-sm',
};
const classes = $derived(cn(
// Base
'inline-flex items-center justify-center',
'font-primary font-bold tracking-tight uppercase',
'text-label-mono',
'rounded-none',
'transition-all duration-200',
'transition-all duration-normal',
'select-none',
'outline-none',
'cursor-pointer',
@@ -190,6 +207,8 @@ const classes = $derived(cn(
variantStyles[variant],
// Size (square when icon-only)
isIconOnly ? iconSizeStyles[size] : sizeStyles[size],
// Layout
layoutStyles[layout],
// Animate (CSS tap scale — excluded for primary which uses translate instead)
animate && !disabled && variant !== 'primary' && 'active:scale-[0.97]',
// Active override
+1
View File
@@ -1,3 +1,4 @@
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'ghost' | 'outline' | 'icon';
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
export type ButtonLayout = 'inline' | 'block-list-row';
export type IconPosition = 'left' | 'right';