refactor(ui): update shared components and add ControlGroup, SidebarContainer
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user