refactor(ui): update shared components and add ControlGroup, SidebarContainer

This commit is contained in:
Ilia Mashkov
2026-03-02 22:19:35 +03:00
parent 13818d5844
commit 0dd08874bc
33 changed files with 927 additions and 203 deletions
+29 -7
View File
@@ -13,18 +13,43 @@ import type {
} from './types';
interface Props extends HTMLButtonAttributes {
/**
* Visual style variant
* @default 'secondary'
*/
variant?: ButtonVariant;
/**
* Button size
* @default 'md'
*/
size?: ButtonSize;
/** Svelte snippet rendered as the icon. */
/**
* Icon snippet
*/
icon?: Snippet;
/**
* Icon placement
* @default 'left'
*/
iconPosition?: IconPosition;
/**
* Active toggle state
* @default false
*/
active?: boolean;
/**
* When true (default), adds `active:scale-[0.97]` on tap via CSS.
* Primary variant is excluded from scale — it shifts via translate instead.
* Tap animation
* Primary uses translate, others use scale
* @default true
*/
animate?: boolean;
/**
* Content snippet
*/
children?: Snippet;
/**
* CSS classes
*/
class?: string;
}
@@ -45,7 +70,6 @@ let {
// Square sizing when icon is present but there is no text label
const isIconOnly = $derived(!!icon && !children);
// ── Variant base styles ──────────────────────────────────────────────────────
const variantStyles: Record<ButtonVariant, string> = {
primary: cn(
'bg-swiss-red text-white',
@@ -125,7 +149,6 @@ const variantStyles: Record<ButtonVariant, string> = {
),
};
// ── Size styles ───────────────────────────────────────────────────────────────
const sizeStyles: Record<ButtonSize, string> = {
xs: 'h-6 px-2 text-[9px] gap-1',
sm: 'h-8 px-3 text-[10px] gap-1.5',
@@ -143,12 +166,11 @@ const iconSizeStyles: Record<ButtonSize, string> = {
xl: 'h-14 w-14 p-3',
};
// ── Active state overrides (per variant) ─────────────────────────────────────
const activeStyles: Partial<Record<ButtonVariant, string>> = {
secondary: 'bg-paper dark:bg-paper shadow-sm border-black/20 dark:border-white/20',
tertiary:
'bg-paper dark:bg-[#1e1e1e] border-black/10 dark:border-white/10 shadow-sm text-neutral-900 dark:text-neutral-100',
ghost: 'bg-transparent dark:bg-transparent text-brnad dark:text-brand',
ghost: 'bg-transparent dark:bg-transparent text-brand dark:text-brand',
outline: 'bg-surface dark:bg-paper border-brand',
icon: 'bg-paper dark:bg-paper text-brand border-black/5 dark:border-white/10',
};
@@ -0,0 +1,91 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import ButtonGroup from './ButtonGroup.svelte';
const { Story } = defineMeta({
title: 'Shared/ButtonGroup',
component: ButtonGroup,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'Wraps buttons in a warm-surface pill with a 1px gap and subtle border. Use for segmented controls, view toggles, or any mutually exclusive button set.',
},
story: { inline: false },
},
layout: 'centered',
},
argTypes: {
class: {
control: 'text',
description: 'Additional CSS classes',
},
},
});
</script>
<script lang="ts">
import { Button } from '$shared/ui/Button';
</script>
<Story name="Default">
{#snippet template(args)}
<ButtonGroup {...args}>
<Button variant="tertiary">Option 1</Button>
<Button variant="tertiary">Option 2</Button>
<Button variant="tertiary">Option 3</Button>
</ButtonGroup>
{/snippet}
</Story>
<Story name="Horizontal">
{#snippet template(args)}
<ButtonGroup {...args}>
<Button variant="tertiary">Day</Button>
<Button variant="tertiary" active>Week</Button>
<Button variant="tertiary">Month</Button>
<Button variant="tertiary">Year</Button>
</ButtonGroup>
{/snippet}
</Story>
<Story name="Vertical">
{#snippet template(args)}
<ButtonGroup {...args} class="flex-col">
<Button variant="tertiary">Top</Button>
<Button variant="tertiary" active>Middle</Button>
<Button variant="tertiary">Bottom</Button>
</ButtonGroup>
{/snippet}
</Story>
<Story name="With Icons">
{#snippet template(args)}
<ButtonGroup {...args}>
<Button variant="tertiary">Grid</Button>
<Button variant="tertiary" active>List</Button>
<Button variant="tertiary">Map</Button>
</ButtonGroup>
{/snippet}
</Story>
<Story
name="Dark Mode"
parameters={{
backgrounds: {
default: 'dark',
},
}}
>
{#snippet template(args)}
<div class="p-8 bg-background text-foreground">
<h3 class="mb-4 text-sm font-medium">Dark Mode</h3>
<ButtonGroup {...args}>
<Button variant="tertiary">Option A</Button>
<Button variant="tertiary" active>Option B</Button>
<Button variant="tertiary">Option C</Button>
</ButtonGroup>
</div>
{/snippet}
</Story>
+6
View File
@@ -9,7 +9,13 @@ import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
interface Props extends HTMLAttributes<HTMLDivElement> {
/**
* Content snippet
*/
children?: Snippet;
/**
* CSS classes
*/
class?: string;
}
@@ -0,0 +1,148 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import IconButton from './IconButton.svelte';
const { Story } = defineMeta({
title: 'Shared/IconButton',
component: IconButton,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'Icon-only button variant. Convenience wrapper that defaults variant to "icon" and enforces icon-only usage.',
},
story: { inline: false },
},
layout: 'centered',
},
argTypes: {
variant: {
control: 'select',
options: ['icon', 'ghost', 'secondary'],
defaultValue: 'icon',
},
size: {
control: 'select',
options: ['xs', 'sm', 'md', 'lg', 'xl'],
defaultValue: 'md',
},
active: {
control: 'boolean',
defaultValue: false,
},
animate: {
control: 'boolean',
defaultValue: true,
},
},
});
</script>
<script lang="ts">
import MoonIcon from '@lucide/svelte/icons/moon';
import SearchIcon from '@lucide/svelte/icons/search';
import TrashIcon from '@lucide/svelte/icons/trash-2';
</script>
<Story
name="Default"
args={{ variant: 'icon', size: 'md', active: false, animate: true }}
>
{#snippet template(args)}
<div class="flex items-center gap-4">
<IconButton {...args}>
{#snippet icon()}
<SearchIcon />
{/snippet}
</IconButton>
<IconButton {...args} size="sm">
{#snippet icon()}
<SearchIcon />
{/snippet}
</IconButton>
<IconButton {...args} size="lg">
{#snippet icon()}
<SearchIcon />
{/snippet}
</IconButton>
</div>
{/snippet}
</Story>
<Story
name="Variants"
args={{ size: 'md', active: false, animate: true }}
>
{#snippet template(args)}
<div class="flex items-center gap-4">
<IconButton {...args} variant="icon">
{#snippet icon()}
<SearchIcon />
{/snippet}
</IconButton>
<IconButton {...args} variant="ghost">
{#snippet icon()}
<MoonIcon />
{/snippet}
</IconButton>
<IconButton {...args} variant="secondary">
{#snippet icon()}
<SearchIcon />
{/snippet}
</IconButton>
</div>
{/snippet}
</Story>
<Story
name="Active State"
args={{ size: 'md', animate: true }}
>
{#snippet template(args)}
<div class="flex items-center gap-4">
<IconButton {...args} active={false} variant="icon">
{#snippet icon()}
<TrashIcon />
{/snippet}
</IconButton>
<IconButton {...args} active={true} variant="icon">
{#snippet icon()}
<TrashIcon />
{/snippet}
</IconButton>
</div>
{/snippet}
</Story>
<Story
name="Dark Mode"
parameters={{
backgrounds: {
default: 'dark',
},
}}
>
{#snippet template(args)}
<div class="p-8 bg-background text-foreground">
<h3 class="mb-4 text-sm font-medium">Dark Mode</h3>
<div class="flex items-center gap-4">
<IconButton {...args} variant="icon">
{#snippet icon()}
<SearchIcon />
{/snippet}
</IconButton>
<IconButton {...args} variant="ghost">
{#snippet icon()}
<MoonIcon />
{/snippet}
</IconButton>
<IconButton {...args} variant="secondary">
{#snippet icon()}
<SearchIcon />
{/snippet}
</IconButton>
</div>
</div>
{/snippet}
</Story>
+4
View File
@@ -12,6 +12,10 @@ import type { ButtonVariant } from './types';
type BaseProps = Exclude<ComponentProps<typeof Button>, 'children' | 'iconPosition'>;
interface Props extends BaseProps {
/**
* Visual variant
* @default 'icon'
*/
variant?: Extract<ButtonVariant, 'icon' | 'ghost' | 'secondary'>;
}
@@ -0,0 +1,138 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import ToggleButton from './ToggleButton.svelte';
const { Story } = defineMeta({
title: 'Shared/ToggleButton',
component: ToggleButton,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'Toggle button with selected/active states. Accepts `selected` prop as alias for `active`, matching common toggle patterns.',
},
story: { inline: false },
},
layout: 'centered',
},
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'tertiary', 'outline', 'ghost'],
defaultValue: 'tertiary',
},
size: {
control: 'select',
options: ['xs', 'sm', 'md', 'lg', 'xl'],
defaultValue: 'md',
},
selected: {
control: 'boolean',
description: 'Selected state (alias for active)',
},
active: {
control: 'boolean',
defaultValue: false,
},
animate: {
control: 'boolean',
defaultValue: true,
},
},
});
</script>
<script lang="ts">
let selected = $state(false);
</script>
<Story
name="Default"
args={{ variant: 'tertiary', size: 'md', selected: false, animate: true }}
>
{#snippet template(args)}
<ToggleButton {...args}>Toggle Me</ToggleButton>
{/snippet}
</Story>
<Story
name="Selected/Unselected"
args={{ variant: 'tertiary', size: 'md', animate: true }}
>
{#snippet template(args)}
<div class="flex items-center gap-4">
<ToggleButton {...args} selected={false}>
Unselected
</ToggleButton>
<ToggleButton {...args} selected={true}>
Selected
</ToggleButton>
</div>
{/snippet}
</Story>
<Story
name="Variants"
args={{ size: 'md', selected: true, animate: true }}
>
{#snippet template(args)}
<div class="flex items-center gap-4">
<ToggleButton {...args} variant="primary">
Primary
</ToggleButton>
<ToggleButton {...args} variant="secondary">
Secondary
</ToggleButton>
<ToggleButton {...args} variant="tertiary">
Tertiary
</ToggleButton>
<ToggleButton {...args} variant="outline">
Outline
</ToggleButton>
<ToggleButton {...args} variant="ghost">
Ghost
</ToggleButton>
</div>
{/snippet}
</Story>
<Story
name="Interactive"
args={{ variant: 'tertiary', size: 'md', animate: true }}
>
{#snippet template(args)}
<div class="flex items-center gap-4">
<ToggleButton {...args} selected={selected} onclick={() => selected = !selected}>
Click to toggle
</ToggleButton>
<span class="text-sm text-muted-foreground">Currently: {selected ? 'selected' : 'unselected'}</span>
</div>
{/snippet}
</Story>
<Story
name="Dark Mode"
parameters={{
backgrounds: {
default: 'dark',
},
}}
>
{#snippet template(args)}
<div class="p-8 bg-background text-foreground">
<h3 class="mb-4 text-sm font-medium">Dark Mode</h3>
<div class="flex items-center gap-4">
<ToggleButton {...args} variant="primary" selected={true}>
Primary
</ToggleButton>
<ToggleButton {...args} variant="secondary" selected={false}>
Secondary
</ToggleButton>
<ToggleButton {...args} variant="tertiary" selected={false}>
Tertiary
</ToggleButton>
</div>
</div>
{/snippet}
</Story>
+4 -2
View File
@@ -10,12 +10,14 @@ import Button from './Button.svelte';
type BaseProps = ComponentProps<typeof Button>;
interface Props extends BaseProps {
/** Alias for `active`. Takes precedence if both are provided. */
/**
* Selected state alias for active
*/
selected?: boolean;
}
let {
variant = 'secondary',
variant = 'tertiary',
size = 'md',
icon,
iconPosition = 'left',