feat(shared/ui): add Badge component with brutalist variants and Storybook story

This commit is contained in:
Ilia Mashkov
2026-03-06 23:13:43 +03:00
parent 53b9a8bf7a
commit ac2eb6ba0b
5 changed files with 115 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import Badge from './Badge.svelte';
const { Story } = defineMeta({
title: 'Shared/Badge',
component: Badge,
tags: ['autodocs'],
parameters: {
layout: 'centered',
docs: { description: { component: 'Brutalist inline label. Uppercase, 3px border, zero radius.' } },
},
argTypes: {
variant: {
control: 'select',
options: ['default', 'primary', 'secondary', 'outline'],
},
},
});
</script>
<Story name="Default">
{#snippet template(args)}
<Badge {...args}>Tag</Badge>
{/snippet}
</Story>
<Story name="Primary" args={{ variant: 'primary' }}>
{#snippet template(args)}
<Badge {...args}>Primary</Badge>
{/snippet}
</Story>
<Story name="Secondary" args={{ variant: 'secondary' }}>
{#snippet template(args)}
<Badge {...args}>Secondary</Badge>
{/snippet}
</Story>
<Story name="Outline" args={{ variant: 'outline' }}>
{#snippet template(args)}
<Badge {...args}>Outline</Badge>
{/snippet}
</Story>
<Story name="All variants">
{#snippet template()}
<div class="flex gap-3 flex-wrap">
<Badge variant="default">Default</Badge>
<Badge variant="primary">Primary</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="outline">Outline</Badge>
</div>
{/snippet}
</Story>

View File

@@ -0,0 +1,37 @@
<!--
Component: Badge
Inline label for tags and status indicators. Brutalist border, uppercase, no radius.
-->
<script lang="ts">
import { cn } from '$shared/lib/cn';
import type { Snippet } from 'svelte';
import type { BadgeVariant } from './types';
interface Props {
/** Visual style variant @default 'default' */
variant?: BadgeVariant;
children?: Snippet;
class?: string;
}
let { variant = 'default', children, class: className }: Props = $props();
const variantStyles: Record<BadgeVariant, string> = {
default: 'brutal-border bg-carbon-black text-ochre-clay',
primary: 'brutal-border bg-burnt-oxide text-ochre-clay',
secondary: 'brutal-border bg-slate-indigo text-ochre-clay',
outline: 'brutal-border bg-transparent text-carbon-black',
};
const classes = $derived(cn(
'inline-block px-3 py-1 text-xs uppercase tracking-wider',
variantStyles[variant],
className,
));
</script>
<span class={classes}>
{#if children}
{@render children()}
{/if}
</span>

View File

@@ -0,0 +1,20 @@
import { render } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';
import Badge from './Badge.svelte';
describe('Badge', () => {
it('renders children text', () => {
const { getByText } = render(Badge, { props: { children: 'hello' } });
expect(getByText('hello')).toBeTruthy();
});
it('applies default variant class', () => {
const { container } = render(Badge, { props: { variant: 'default' } });
expect(container.querySelector('span')?.className).toContain('bg-carbon-black');
});
it('applies primary variant class', () => {
const { container } = render(Badge, { props: { variant: 'primary' } });
expect(container.querySelector('span')?.className).toContain('bg-burnt-oxide');
});
});

View File

@@ -0,0 +1,2 @@
export { default as Badge } from './Badge.svelte';
export type { BadgeVariant } from './types';

View File

@@ -0,0 +1 @@
export type BadgeVariant = 'default' | 'primary' | 'secondary' | 'outline';