Chore/architecture refactoring #42
@@ -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,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';
|
||||||
|
|||||||
Reference in New Issue
Block a user