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 { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements'; import type { HTMLButtonAttributes } from 'svelte/elements';
import type { import type {
ButtonLayout,
ButtonSize, ButtonSize,
ButtonVariant, ButtonVariant,
IconPosition, IconPosition,
@@ -23,6 +24,14 @@ interface Props extends HTMLButtonAttributes {
* @default 'md' * @default 'md'
*/ */
size?: ButtonSize; 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 * Icon snippet
*/ */
@@ -56,6 +65,7 @@ interface Props extends HTMLButtonAttributes {
let { let {
variant = 'secondary', variant = 'secondary',
size = 'md', size = 'md',
layout = 'inline',
icon, icon,
iconPosition = 'left', iconPosition = 'left',
active = false, active = false,
@@ -76,10 +86,10 @@ const variantStyles: Record<ButtonVariant, string> = {
'hover:bg-swiss-red/90', 'hover:bg-swiss-red/90',
'active:bg-swiss-red/80', 'active:bg-swiss-red/80',
'border border-swiss-red', 'border border-swiss-red',
'shadow-[0.125rem_0.125rem_0_0_rgba(0,0,0,0.1)]', 'shadow-stamp-rest',
'hover:shadow-[0.1875rem_0.1875rem_0_0_rgba(0,0,0,0.15)]', 'hover:shadow-stamp-hover',
'active:shadow-[0.0625rem_0.0625rem_0_0_rgba(0,0,0,0.08)]', 'active:shadow-stamp-pressed',
'active:translate-x-[0.0625rem] active:translate-y-[0.0625rem]', 'active:translate-x-px active:translate-y-px',
'disabled:bg-neutral-300 dark:disabled:bg-neutral-700', 'disabled:bg-neutral-300 dark:disabled:bg-neutral-700',
'disabled:text-neutral-500 dark:disabled:text-neutral-500', 'disabled:text-neutral-500 dark:disabled:text-neutral-500',
'disabled:border-neutral-300 dark:disabled:border-neutral-700', 'disabled:border-neutral-300 dark:disabled:border-neutral-700',
@@ -111,7 +121,7 @@ const variantStyles: Record<ButtonVariant, string> = {
), ),
ghost: cn( ghost: cn(
'bg-transparent', 'bg-transparent',
'text-secondary', 'text-subtle',
'border border-transparent', 'border border-transparent',
'hover:bg-transparent dark:hover:bg-transparent', 'hover:bg-transparent dark:hover:bg-transparent',
'hover:text-brand dark:hover:text-brand', 'hover:text-brand dark:hover:text-brand',
@@ -120,8 +130,8 @@ const variantStyles: Record<ButtonVariant, string> = {
'disabled:cursor-not-allowed', 'disabled:cursor-not-allowed',
), ),
icon: cn( icon: cn(
'bg-surface dark:bg-dark-bg', 'surface-canvas',
'text-secondary', 'text-subtle',
'border border-transparent', 'border border-transparent',
'hover:bg-paper dark:hover:bg-paper', 'hover:bg-paper dark:hover:bg-paper',
'hover:text-brand', 'hover:text-brand',
@@ -174,12 +184,19 @@ const activeStyles: Partial<Record<ButtonVariant, string>> = {
icon: 'bg-paper dark:bg-paper text-brand border-subtle', 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( const classes = $derived(cn(
// Base // Base
'inline-flex items-center justify-center', 'inline-flex items-center justify-center',
'font-primary font-bold tracking-tight uppercase', 'text-label-mono',
'rounded-none', 'rounded-none',
'transition-all duration-200', 'transition-all duration-normal',
'select-none', 'select-none',
'outline-none', 'outline-none',
'cursor-pointer', 'cursor-pointer',
@@ -190,6 +207,8 @@ const classes = $derived(cn(
variantStyles[variant], variantStyles[variant],
// Size (square when icon-only) // Size (square when icon-only)
isIconOnly ? iconSizeStyles[size] : sizeStyles[size], isIconOnly ? iconSizeStyles[size] : sizeStyles[size],
// Layout
layoutStyles[layout],
// Animate (CSS tap scale — excluded for primary which uses translate instead) // Animate (CSS tap scale — excluded for primary which uses translate instead)
animate && !disabled && variant !== 'primary' && 'active:scale-[0.97]', animate && !disabled && variant !== 'primary' && 'active:scale-[0.97]',
// Active override // Active override
+1
View File
@@ -1,3 +1,4 @@
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'ghost' | 'outline' | 'icon'; export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'ghost' | 'outline' | 'icon';
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
export type ButtonLayout = 'inline' | 'block-list-row';
export type IconPosition = 'left' | 'right'; export type IconPosition = 'left' | 'right';