feat(shared/ui): add Badge component with brutalist variants and Storybook story
This commit is contained in:
55
src/shared/ui/Badge/Badge.stories.svelte
Normal file
55
src/shared/ui/Badge/Badge.stories.svelte
Normal 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>
|
||||||
37
src/shared/ui/Badge/Badge.svelte
Normal file
37
src/shared/ui/Badge/Badge.svelte
Normal 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>
|
||||||
20
src/shared/ui/Badge/Badge.test.ts
Normal file
20
src/shared/ui/Badge/Badge.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
2
src/shared/ui/Badge/index.ts
Normal file
2
src/shared/ui/Badge/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Badge } from './Badge.svelte';
|
||||||
|
export type { BadgeVariant } from './types';
|
||||||
1
src/shared/ui/Badge/types.ts
Normal file
1
src/shared/ui/Badge/types.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type BadgeVariant = 'default' | 'primary' | 'secondary' | 'outline';
|
||||||
Reference in New Issue
Block a user