add storybook setup and initial stories

This commit is contained in:
Justin Edmund 2025-11-30 06:02:41 -08:00
parent 94fdcf1c6b
commit bbec620d00
28 changed files with 3477 additions and 24 deletions

View file

@ -1,20 +1,29 @@
import type { StorybookConfig } from '@storybook/sveltekit'; import type { StorybookConfig } from '@storybook/sveltekit';
import remarkGfm from 'remark-gfm';
const config: StorybookConfig = { const config: StorybookConfig = {
"stories": [ stories: ['../src/stories/**/*.mdx', '../src/stories/**/*.stories.@(js|ts|svelte)'],
"../src/**/*.mdx", addons: [
"../src/**/*.stories.@(js|ts|svelte)" '@storybook/addon-svelte-csf',
], '@chromatic-com/storybook',
"addons": [ {
"@storybook/addon-svelte-csf", name: '@storybook/addon-docs',
"@chromatic-com/storybook", options: {
"@storybook/addon-docs", mdxPluginOptions: {
"@storybook/addon-a11y", mdxCompileOptions: {
"@storybook/addon-vitest" remarkPlugins: [remarkGfm]
], }
"framework": { }
"name": "@storybook/sveltekit", }
"options": {} },
} '@storybook/addon-a11y',
'@storybook/addon-vitest'
],
framework: {
name: '@storybook/sveltekit',
options: {}
},
staticDirs: ['../static']
}; };
export default config; export default config;

View file

@ -0,0 +1,10 @@
<link rel="preload" href="/fonts/gk-variable.woff2" as="font" type="font/woff2" crossorigin />
<style>
@font-face {
font-family: 'Goalking';
src: url('/fonts/gk-variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
</style>

View file

@ -1,14 +1,54 @@
import type { Preview } from '@storybook/sveltekit' import type { Preview } from '@storybook/sveltekit';
import '$src/app.scss';
import './storybook-overrides.css';
const preview: Preview = { const preview: Preview = {
parameters: { parameters: {
controls: { controls: {
matchers: { matchers: {
color: /(background|color)$/i, color: /(background|color)$/i,
date: /Date$/i, date: /Date$/i
}, }
}, },
}, viewport: {
options: {
phone: { name: 'Phone', styles: { width: '375px', height: '667px' } },
tablet: { name: 'Tablet', styles: { width: '768px', height: '1024px' } },
laptop: { name: 'Laptop', styles: { width: '1280px', height: '800px' } },
desktop: { name: 'Desktop', styles: { width: '1920px', height: '1080px' } }
}
},
backgrounds: {
options: {
light: { name: 'light', value: '#f5f5f5' },
dark: { name: 'dark', value: '#191919' },
"card-light": { name: 'card-light', value: '#ffffff' },
"card-dark": { name: 'card-dark', value: '#212121' }
}
},
docs: {
toc: true
}
},
globalTypes: {
theme: {
name: 'Theme',
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
icon: 'circlehollow',
items: ['light', 'dark'],
showName: true
}
}
},
initialGlobals: {
backgrounds: {
value: 'light'
}
}
}; };
export default preview; export default preview;

View file

@ -0,0 +1,16 @@
/* Storybook-specific overrides */
/* Allow scrolling in Storybook (app.scss sets overflow: hidden for custom layout scrolling) */
body {
overflow: auto !important;
height: auto !important;
}
html {
height: auto !important;
}
/* Ensure docs pages can scroll */
.sbdocs-wrapper {
overflow: auto !important;
}

View file

@ -0,0 +1,223 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import CharacterImageCell from '$lib/components/database/cells/CharacterImageCell.svelte';
import WeaponImageCell from '$lib/components/database/cells/WeaponImageCell.svelte';
import SummonImageCell from '$lib/components/database/cells/SummonImageCell.svelte';
import ElementCell from '$lib/components/database/cells/ElementCell.svelte';
import ProficiencyCell from '$lib/components/database/cells/ProficiencyCell.svelte';
import CharacterUncapCell from '$lib/components/database/cells/CharacterUncapCell.svelte';
import WeaponUncapCell from '$lib/components/database/cells/WeaponUncapCell.svelte';
import SummonUncapCell from '$lib/components/database/cells/SummonUncapCell.svelte';
const { Story } = defineMeta({
title: 'Components/Game/Database Cells',
tags: ['autodocs']
});
// Mock row data for cells
const mockCharacterRow = {
granblueId: '3040000000',
name: { en: 'Narmaya', ja: 'ナルメア' },
element: 5, // Dark
proficiency: [1, 2], // Sabre, Dagger
special: false,
uncap: { flb: true, ulb: true, transcendence: true }
};
const mockSpecialCharacterRow = {
granblueId: '3040100000',
name: { en: 'Cagliostro', ja: 'カリオストロ' },
element: 4, // Earth
proficiency: [5], // Staff
special: true,
uncap: { flb: true, ulb: true }
};
const mockWeaponRow = {
granblueId: '1040000000',
name: { en: 'Sword of Bahamut', ja: 'バハムートソード' },
element: 5, // Dark
proficiency: 1, // Sabre
rarity: 3,
uncap: { flb: true, ulb: true, transcendence: true }
};
const mockSummonRow = {
granblueId: '2040001000',
name: { en: 'Bahamut', ja: 'バハムート' },
element: 5, // Dark
rarity: 3,
uncap: { flb: true, ulb: true, transcendence: true }
};
// All elements for comparison
const elements = [
{ id: 1, name: 'Wind' },
{ id: 2, name: 'Fire' },
{ id: 3, name: 'Water' },
{ id: 4, name: 'Earth' },
{ id: 5, name: 'Dark' },
{ id: 6, name: 'Light' }
];
const proficiencies = [
{ id: 1, name: 'Sabre' },
{ id: 2, name: 'Dagger' },
{ id: 3, name: 'Axe' },
{ id: 4, name: 'Spear' },
{ id: 5, name: 'Staff' },
{ id: 6, name: 'Gun' },
{ id: 7, name: 'Melee' },
{ id: 8, name: 'Bow' },
{ id: 9, name: 'Harp' },
{ id: 10, name: 'Katana' }
];
</script>
<!-- Character Image Cell -->
<Story name="Character Image Cell">
<div
style="display: flex; gap: 16px; background: #f5f5f5; padding: 16px; border-radius: 8px; height: 80px;"
>
<CharacterImageCell row={mockCharacterRow} />
</div>
</Story>
<!-- Weapon Image Cell -->
<Story name="Weapon Image Cell">
<div
style="display: flex; gap: 16px; background: #f5f5f5; padding: 16px; border-radius: 8px; height: 80px;"
>
<WeaponImageCell row={mockWeaponRow} />
</div>
</Story>
<!-- Summon Image Cell -->
<Story name="Summon Image Cell">
<div
style="display: flex; gap: 16px; background: #f5f5f5; padding: 16px; border-radius: 8px; height: 80px;"
>
<SummonImageCell row={mockSummonRow} />
</div>
</Story>
<!-- Element Cells -->
<Story name="Element Cell - All Elements">
<div
style="display: flex; gap: 16px; background: #f5f5f5; padding: 16px; border-radius: 8px; height: 60px; align-items: center;"
>
{#each elements as el}
<div style="display: flex; flex-direction: column; align-items: center; gap: 4px;">
<ElementCell row={{ element: el.id }} />
<span style="font-size: 10px; color: #666;">{el.name}</span>
</div>
{/each}
</div>
</Story>
<!-- Proficiency Cell -->
<Story name="Proficiency Cell - All Types">
<div
style="display: flex; flex-wrap: wrap; gap: 12px; background: #f5f5f5; padding: 16px; border-radius: 8px; align-items: center;"
>
{#each proficiencies as prof}
<div style="display: flex; flex-direction: column; align-items: center; gap: 4px;">
<ProficiencyCell row={{ proficiency: [prof.id] }} />
<span style="font-size: 10px; color: #666;">{prof.name}</span>
</div>
{/each}
</div>
</Story>
<!-- Character Uncap Cell -->
<Story name="Character Uncap Cell">
<div style="display: flex; flex-direction: column; gap: 16px;">
<div
style="display: flex; gap: 16px; background: #f5f5f5; padding: 16px; border-radius: 8px; align-items: center;"
>
<span style="width: 100px; font-size: 12px; color: #666;">Regular FLB:</span>
<CharacterUncapCell row={{ uncap: { flb: true }, special: false }} />
</div>
<div
style="display: flex; gap: 16px; background: #f5f5f5; padding: 16px; border-radius: 8px; align-items: center;"
>
<span style="width: 100px; font-size: 12px; color: #666;">Regular ULB:</span>
<CharacterUncapCell row={{ uncap: { flb: true, ulb: true }, special: false }} />
</div>
<div
style="display: flex; gap: 16px; background: #f5f5f5; padding: 16px; border-radius: 8px; align-items: center;"
>
<span style="width: 100px; font-size: 12px; color: #666;">Transcendence:</span>
<CharacterUncapCell
row={{ uncap: { flb: true, ulb: true, transcendence: true }, special: false }}
/>
</div>
<div
style="display: flex; gap: 16px; background: #f5f5f5; padding: 16px; border-radius: 8px; align-items: center;"
>
<span style="width: 100px; font-size: 12px; color: #666;">Special ULB:</span>
<CharacterUncapCell row={{ uncap: { flb: true, ulb: true }, special: true }} />
</div>
</div>
</Story>
<!-- Weapon Uncap Cell -->
<Story name="Weapon Uncap Cell">
<div style="display: flex; flex-direction: column; gap: 16px;">
<div
style="display: flex; gap: 16px; background: #f5f5f5; padding: 16px; border-radius: 8px; align-items: center;"
>
<span style="width: 100px; font-size: 12px; color: #666;">MLB (3★):</span>
<WeaponUncapCell row={{ uncap: {}, rarity: 3 }} />
</div>
<div
style="display: flex; gap: 16px; background: #f5f5f5; padding: 16px; border-radius: 8px; align-items: center;"
>
<span style="width: 100px; font-size: 12px; color: #666;">FLB (4★):</span>
<WeaponUncapCell row={{ uncap: { flb: true }, rarity: 3 }} />
</div>
<div
style="display: flex; gap: 16px; background: #f5f5f5; padding: 16px; border-radius: 8px; align-items: center;"
>
<span style="width: 100px; font-size: 12px; color: #666;">ULB (5★):</span>
<WeaponUncapCell row={{ uncap: { flb: true, ulb: true }, rarity: 3 }} />
</div>
<div
style="display: flex; gap: 16px; background: #f5f5f5; padding: 16px; border-radius: 8px; align-items: center;"
>
<span style="width: 100px; font-size: 12px; color: #666;">Transcendence:</span>
<WeaponUncapCell row={{ uncap: { flb: true, ulb: true, transcendence: true }, rarity: 3 }} />
</div>
</div>
</Story>
<!-- Summon Uncap Cell -->
<Story name="Summon Uncap Cell">
<div style="display: flex; flex-direction: column; gap: 16px;">
<div
style="display: flex; gap: 16px; background: #f5f5f5; padding: 16px; border-radius: 8px; align-items: center;"
>
<span style="width: 100px; font-size: 12px; color: #666;">MLB (3★):</span>
<SummonUncapCell row={{ uncap: {}, rarity: 3 }} />
</div>
<div
style="display: flex; gap: 16px; background: #f5f5f5; padding: 16px; border-radius: 8px; align-items: center;"
>
<span style="width: 100px; font-size: 12px; color: #666;">FLB (4★):</span>
<SummonUncapCell row={{ uncap: { flb: true }, rarity: 3 }} />
</div>
<div
style="display: flex; gap: 16px; background: #f5f5f5; padding: 16px; border-radius: 8px; align-items: center;"
>
<span style="width: 100px; font-size: 12px; color: #666;">ULB (5★):</span>
<SummonUncapCell row={{ uncap: { flb: true, ulb: true }, rarity: 3 }} />
</div>
<div
style="display: flex; gap: 16px; background: #f5f5f5; padding: 16px; border-radius: 8px; align-items: center;"
>
<span style="width: 100px; font-size: 12px; color: #666;">Transcendence:</span>
<SummonUncapCell row={{ uncap: { flb: true, ulb: true, transcendence: true }, rarity: 3 }} />
</div>
</div>
</Story>

View file

@ -0,0 +1,87 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf'
import ElementLabel from '$lib/components/labels/ElementLabel.svelte'
const { Story } = defineMeta({
title: 'Components/Game/ElementLabel',
component: ElementLabel,
tags: ['autodocs'],
argTypes: {
element: {
control: {
type: 'select',
labels: {
0: 'Null',
1: 'Wind',
2: 'Fire',
3: 'Water',
4: 'Earth',
5: 'Dark',
6: 'Light'
}
},
options: [0, 1, 2, 3, 4, 5, 6],
description: 'Element type (0=Null, 1=Wind, 2=Fire, 3=Water, 4=Earth, 5=Dark, 6=Light)'
},
size: {
control: 'select',
options: ['small', 'medium', 'large', 'xlarge', 'natural'],
description: 'Icon size'
}
}
})
const elements = [
{ id: 1, name: 'Wind' },
{ id: 2, name: 'Fire' },
{ id: 3, name: 'Water' },
{ id: 4, name: 'Earth' },
{ id: 5, name: 'Dark' },
{ id: 6, name: 'Light' }
]
</script>
<!-- Default - Interactive single component for autodocs (args-only, no children) -->
<Story name="Default" args={{ element: 1, size: 'large' }} />
<!-- All Elements (custom layout - uses asChild to prevent double render) -->
<Story name="All Elements" asChild>
<div style="display: flex; gap: 16px; align-items: center;">
{#each elements as el}
<div style="display: flex; flex-direction: column; align-items: center; gap: 4px;">
<ElementLabel element={el.id} />
<span style="font-size: 12px; color: #666;">{el.name}</span>
</div>
{/each}
</div>
</Story>
<!-- Size Comparison (custom layout) -->
<Story name="Size Comparison" asChild>
<div style="display: flex; flex-direction: column; gap: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 80px; font-size: 12px; color: #666;">Small:</span>
{#each elements as el}
<ElementLabel element={el.id} size="small" />
{/each}
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 80px; font-size: 12px; color: #666;">Medium:</span>
{#each elements as el}
<ElementLabel element={el.id} size="medium" />
{/each}
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 80px; font-size: 12px; color: #666;">Large:</span>
{#each elements as el}
<ElementLabel element={el.id} size="large" />
{/each}
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 80px; font-size: 12px; color: #666;">X-Large:</span>
{#each elements as el}
<ElementLabel element={el.id} size="xlarge" />
{/each}
</div>
</div>
</Story>

View file

@ -0,0 +1,102 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf'
import JobItem from '$lib/components/job/JobItem.svelte'
import { mockJob, mockJobNoUM, mockJobMultiProf } from '../../mocks/jobs'
import { fn } from 'storybook/test'
const { Story } = defineMeta({
title: 'Components/Game/JobItem',
component: JobItem,
tags: ['autodocs'],
argTypes: {
selected: {
control: 'boolean',
description: 'Whether the job is selected'
}
},
args: {
onClick: fn()
}
})
</script>
<script>
let selectedJob = $state(null)
let interactiveSelected = $state(null)
</script>
<!-- Default - args-only for autodocs -->
<Story name="Default" args={{ job: mockJob }} />
<!-- Selected -->
<Story name="Selected" args={{ job: mockJob, selected: true }} />
<!-- With Ultimate Mastery -->
<Story name="With Ultimate Mastery" asChild>
<div style="max-width: 400px; display: flex; flex-direction: column; gap: 8px;">
<JobItem job={mockJob} />
<span style="font-size: 12px; color: #666;">Has UM badge</span>
</div>
</Story>
<!-- Without Ultimate Mastery -->
<Story name="Without Ultimate Mastery" asChild>
<div style="max-width: 400px; display: flex; flex-direction: column; gap: 8px;">
<JobItem job={mockJobNoUM} />
<span style="font-size: 12px; color: #666;">No UM badge</span>
</div>
</Story>
<!-- Multi Proficiency -->
<Story name="Multi Proficiency" asChild>
<div style="max-width: 400px; display: flex; flex-direction: column; gap: 8px;">
<JobItem job={mockJobMultiProf} />
<span style="font-size: 12px; color: #666;">Shows multiple proficiency icons</span>
</div>
</Story>
<!-- Job List -->
<Story name="Job List" asChild>
<div style="max-width: 400px; display: flex; flex-direction: column; gap: 8px;">
<JobItem
job={mockJob}
selected={selectedJob === mockJob.id}
onClick={() => (selectedJob = mockJob.id)}
/>
<JobItem
job={mockJobNoUM}
selected={selectedJob === mockJobNoUM.id}
onClick={() => (selectedJob = mockJobNoUM.id)}
/>
<JobItem
job={mockJobMultiProf}
selected={selectedJob === mockJobMultiProf.id}
onClick={() => (selectedJob = mockJobMultiProf.id)}
/>
</div>
</Story>
<!-- Interactive Selection -->
<Story name="Interactive Selection" asChild>
<div style="max-width: 400px; display: flex; flex-direction: column; gap: 8px;">
<p style="font-size: 12px; color: #666; margin-bottom: 8px;">Click to select a job</p>
<JobItem
job={mockJob}
selected={interactiveSelected === 'job-1'}
onClick={() => (interactiveSelected = 'job-1')}
/>
<JobItem
job={mockJobNoUM}
selected={interactiveSelected === 'job-2'}
onClick={() => (interactiveSelected = 'job-2')}
/>
<JobItem
job={mockJobMultiProf}
selected={interactiveSelected === 'job-3'}
onClick={() => (interactiveSelected = 'job-3')}
/>
<p style="font-size: 12px; margin-top: 8px;">
Selected: {interactiveSelected ? `Job ${interactiveSelected}` : 'None'}
</p>
</div>
</Story>

View file

@ -0,0 +1,168 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf'
import JobPortrait from '$lib/components/job/JobPortrait.svelte'
import { Gender } from '$lib/utils/jobUtils'
import { mockJob, mockJobNoUM, mockJobMultiProf } from '../../mocks/jobs'
import { fn } from 'storybook/test'
const { Story } = defineMeta({
title: 'Components/Game/JobPortrait',
component: JobPortrait,
tags: ['autodocs'],
argTypes: {
size: {
control: 'select',
options: ['small', 'medium', 'large'],
description: 'Portrait size'
},
gender: {
control: {
type: 'select',
labels: {
[Gender.Gran]: 'Gran',
[Gender.Djeeta]: 'Djeeta'
}
},
options: [Gender.Gran, Gender.Djeeta],
description: 'Character gender'
},
element: {
control: {
type: 'select',
labels: {
1: 'Wind',
2: 'Fire',
3: 'Water',
4: 'Earth',
5: 'Light',
6: 'Dark'
}
},
options: [undefined, 1, 2, 3, 4, 5, 6],
description: 'Element for border color'
},
showPlaceholder: {
control: 'boolean',
description: 'Show placeholder when no job'
},
clickable: {
control: 'boolean',
description: 'Enable click interaction'
}
},
args: {
onclick: fn()
}
})
</script>
<!-- Default - args-only for autodocs -->
<Story name="Default" args={{ job: mockJob, size: 'medium' }} />
<!-- All Sizes -->
<Story name="All Sizes" asChild>
<div style="display: flex; gap: 24px; align-items: flex-end;">
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait job={mockJob} size="small" />
<span style="font-size: 12px; color: #666;">Small</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait job={mockJob} size="medium" />
<span style="font-size: 12px; color: #666;">Medium</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait job={mockJob} size="large" />
<span style="font-size: 12px; color: #666;">Large</span>
</div>
</div>
</Story>
<!-- Gender Options -->
<Story name="Gender Options" asChild>
<div style="display: flex; gap: 24px;">
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait job={mockJob} gender={Gender.Gran} />
<span style="font-size: 12px; color: #666;">Gran</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait job={mockJob} gender={Gender.Djeeta} />
<span style="font-size: 12px; color: #666;">Djeeta</span>
</div>
</div>
</Story>
<!-- Element Borders -->
<Story name="Element Borders" asChild>
<div style="display: flex; flex-wrap: wrap; gap: 16px;">
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait job={mockJob} element={1} />
<span style="font-size: 12px; color: #666;">Wind</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait job={mockJob} element={2} />
<span style="font-size: 12px; color: #666;">Fire</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait job={mockJob} element={3} />
<span style="font-size: 12px; color: #666;">Water</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait job={mockJob} element={4} />
<span style="font-size: 12px; color: #666;">Earth</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait job={mockJob} element={5} />
<span style="font-size: 12px; color: #666;">Light</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait job={mockJob} element={6} />
<span style="font-size: 12px; color: #666;">Dark</span>
</div>
</div>
</Story>
<!-- Empty / Placeholder -->
<Story name="Placeholder" asChild>
<div style="display: flex; gap: 24px;">
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait showPlaceholder={true} />
<span style="font-size: 12px; color: #666;">With Placeholder</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait showPlaceholder={false} />
<span style="font-size: 12px; color: #666;">No Placeholder</span>
</div>
</div>
</Story>
<!-- Clickable -->
<Story name="Clickable" asChild>
<div style="display: flex; gap: 24px;">
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait job={mockJob} clickable onclick={() => alert('Clicked!')} />
<span style="font-size: 12px; color: #666;">Clickable (hover to see effect)</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait job={mockJob} />
<span style="font-size: 12px; color: #666;">Non-clickable</span>
</div>
</div>
</Story>
<!-- Different Jobs -->
<Story name="Different Jobs" asChild>
<div style="display: flex; gap: 24px;">
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait job={mockJob} />
<span style="font-size: 12px; color: #666;">{mockJob.name.en}</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait job={mockJobNoUM} />
<span style="font-size: 12px; color: #666;">{mockJobNoUM.name.en}</span>
</div>
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<JobPortrait job={mockJobMultiProf} />
<span style="font-size: 12px; color: #666;">{mockJobMultiProf.name.en}</span>
</div>
</div>
</Story>

View file

@ -0,0 +1,120 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf'
import ProficiencyLabel from '$lib/components/labels/ProficiencyLabel.svelte'
const { Story } = defineMeta({
title: 'Components/Game/ProficiencyLabel',
component: ProficiencyLabel,
tags: ['autodocs'],
argTypes: {
proficiency: {
control: {
type: 'select',
labels: {
1: 'Sabre',
2: 'Dagger',
3: 'Axe',
4: 'Spear',
5: 'Staff',
6: 'Gun',
7: 'Melee',
8: 'Bow',
9: 'Harp',
10: 'Katana'
}
},
options: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
description:
'Proficiency type (1=Sabre, 2=Dagger, 3=Axe, 4=Spear, 5=Staff, 6=Gun, 7=Melee, 8=Bow, 9=Harp, 10=Katana)'
},
size: {
control: 'select',
options: ['small', 'medium', 'large', 'xlarge', 'natural'],
description: 'Icon size'
}
}
})
const proficiencies = [
{ id: 1, name: 'Sabre' },
{ id: 2, name: 'Dagger' },
{ id: 3, name: 'Axe' },
{ id: 4, name: 'Spear' },
{ id: 5, name: 'Staff' },
{ id: 6, name: 'Gun' },
{ id: 7, name: 'Melee' },
{ id: 8, name: 'Bow' },
{ id: 9, name: 'Harp' },
{ id: 10, name: 'Katana' }
]
</script>
<!-- Default - Interactive single component for autodocs (args-only, no children) -->
<Story name="Default" args={{ proficiency: 1, size: 'large' }} />
<!-- All Proficiencies (custom layout - uses asChild to prevent double render) -->
<Story name="All Proficiencies" asChild>
<div style="display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px;">
{#each proficiencies as prof}
<div style="display: flex; flex-direction: column; align-items: center; gap: 4px;">
<ProficiencyLabel proficiency={prof.id} />
<span style="font-size: 12px; color: #666;">{prof.name}</span>
</div>
{/each}
</div>
</Story>
<!-- Size Comparison (custom layout) -->
<Story name="Size Comparison" asChild>
<div style="display: flex; flex-direction: column; gap: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 80px; font-size: 12px; color: #666;">Small:</span>
{#each proficiencies.slice(0, 5) as prof}
<ProficiencyLabel proficiency={prof.id} size="small" />
{/each}
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 80px; font-size: 12px; color: #666;">Medium:</span>
{#each proficiencies.slice(0, 5) as prof}
<ProficiencyLabel proficiency={prof.id} size="medium" />
{/each}
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 80px; font-size: 12px; color: #666;">Large:</span>
{#each proficiencies.slice(0, 5) as prof}
<ProficiencyLabel proficiency={prof.id} size="large" />
{/each}
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 80px; font-size: 12px; color: #666;">X-Large:</span>
{#each proficiencies.slice(0, 5) as prof}
<ProficiencyLabel proficiency={prof.id} size="xlarge" />
{/each}
</div>
</div>
</Story>
<!-- In Context - Character Proficiencies -->
<Story name="In Context - Character Info" asChild>
<div
style="display: inline-flex; flex-direction: column; gap: 8px; padding: 16px; background: #f5f5f5; border-radius: 8px;"
>
<span style="font-weight: 600; margin-bottom: 4px;">Proficiencies</span>
<div style="display: flex; align-items: center; gap: 8px;">
<ProficiencyLabel proficiency={1} size="medium" />
<span style="font-size: 14px;">Sabre</span>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<ProficiencyLabel proficiency={10} size="medium" />
<span style="font-size: 14px;">Katana</span>
</div>
</div>
</Story>
<!-- Dual Proficiency Display -->
<Story name="Dual Proficiency Display" asChild>
<div style="display: flex; align-items: center; gap: 4px;">
<ProficiencyLabel proficiency={1} size="medium" />
<ProficiencyLabel proficiency={10} size="medium" />
</div>
</Story>

View file

@ -0,0 +1,188 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte';
const { Story } = defineMeta({
title: 'Components/Game/UncapIndicator',
component: UncapIndicator,
tags: ['autodocs'],
argTypes: {
type: {
control: 'select',
options: ['character', 'weapon', 'summon'],
description: 'Type of unit'
},
uncapLevel: {
control: { type: 'range', min: 0, max: 6, step: 1 },
description: 'Current uncap level (number of filled stars)'
},
transcendenceStage: {
control: { type: 'range', min: 0, max: 5, step: 1 },
description: 'Transcendence stage (0-5)',
if: { arg: 'transcendence', eq: true }
},
flb: {
control: 'boolean',
description: 'Has FLB (4th star available)'
},
ulb: {
control: 'boolean',
description: 'Has ULB (5th star available)',
if: { arg: 'flb', eq: true }
},
transcendence: {
control: 'boolean',
description: 'Has transcendence (6th star available)',
if: { arg: 'ulb', eq: true }
},
special: {
control: 'boolean',
description: 'Special character (Story SRs - 3 base stars)',
if: { arg: 'type', eq: 'character' }
},
editable: {
control: 'boolean',
description: 'Allow interactive editing'
}
}
});
</script>
<script>
let editableUncap = $state(2);
let editableTrans = $state(0);
</script>
<!-- Default - args-only for autodocs -->
<Story name="Default" args={{ type: 'character', uncapLevel: 4 }} />
<!-- Character - MLB (4 stars) -->
<Story name="Character - MLB" args={{ type: 'character', uncapLevel: 4 }} />
<!-- Character - FLB (5 stars) -->
<Story name="Character - FLB" args={{ type: 'character', flb: true, uncapLevel: 5 }} />
<!-- Character - With Transcendence -->
<Story name="Character - Transcendence" args={{ type: 'character', flb: true, ulb: true, transcendence: true, uncapLevel: 5, transcendenceStage: 3 }} />
<!-- Character - Special (Story SRs) -->
<Story name="Character - Special (Story SR)" asChild>
<div style="display: flex; flex-direction: column; gap: 16px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 100px; font-size: 12px;">3★ MLB:</span>
<UncapIndicator type="character" special uncapLevel={3} />
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 100px; font-size: 12px;">4★ FLB:</span>
<UncapIndicator type="character" special flb uncapLevel={4} />
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 100px; font-size: 12px;">5★ ULB:</span>
<UncapIndicator type="character" special flb ulb uncapLevel={5} />
</div>
</div>
</Story>
<!-- Weapon - All Stages -->
<Story name="Weapon - All Stages" asChild>
<div style="display: flex; flex-direction: column; gap: 16px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 150px; font-size: 12px;">3★ MLB:</span>
<UncapIndicator type="weapon" uncapLevel={3} />
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 150px; font-size: 12px;">4★ FLB:</span>
<UncapIndicator type="weapon" flb uncapLevel={4} />
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 150px; font-size: 12px;">5★ ULB:</span>
<UncapIndicator type="weapon" flb ulb uncapLevel={5} />
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 150px; font-size: 12px;">6★ Transcendence:</span>
<UncapIndicator type="weapon" flb ulb transcendence uncapLevel={5} transcendenceStage={5} />
</div>
</div>
</Story>
<!-- Summon - All Stages -->
<Story name="Summon - All Stages" asChild>
<div style="display: flex; flex-direction: column; gap: 16px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 150px; font-size: 12px;">3★ MLB:</span>
<UncapIndicator type="summon" uncapLevel={3} />
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 150px; font-size: 12px;">4★ FLB:</span>
<UncapIndicator type="summon" flb uncapLevel={4} />
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 150px; font-size: 12px;">5★ ULB:</span>
<UncapIndicator type="summon" flb ulb uncapLevel={5} />
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 150px; font-size: 12px;">6★ Transcendence:</span>
<UncapIndicator type="summon" flb ulb transcendence uncapLevel={5} transcendenceStage={5} />
</div>
</div>
</Story>
<!-- Partially Uncapped -->
<Story name="Partially Uncapped" asChild>
<div style="display: flex; flex-direction: column; gap: 16px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 100px; font-size: 12px;">0/4:</span>
<UncapIndicator type="character" uncapLevel={0} />
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 100px; font-size: 12px;">1/4:</span>
<UncapIndicator type="character" uncapLevel={1} />
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 100px; font-size: 12px;">2/4:</span>
<UncapIndicator type="character" uncapLevel={2} />
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 100px; font-size: 12px;">3/4:</span>
<UncapIndicator type="character" uncapLevel={3} />
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 100px; font-size: 12px;">4/4:</span>
<UncapIndicator type="character" uncapLevel={4} />
</div>
</div>
</Story>
<!-- Transcendence Stages -->
<Story name="Transcendence Stages" asChild>
<div style="display: flex; flex-direction: column; gap: 16px;">
{#each [0, 1, 2, 3, 4, 5] as stage}
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 100px; font-size: 12px;">Stage {stage}:</span>
<UncapIndicator type="weapon" flb ulb transcendence uncapLevel={5} transcendenceStage={stage} />
</div>
{/each}
</div>
</Story>
<!-- Editable -->
<Story name="Editable" asChild>
<div style="display: flex; flex-direction: column; gap: 16px;">
<p style="font-size: 12px; color: #666;">Click stars to change uncap level</p>
<UncapIndicator
type="weapon"
flb
ulb
transcendence
uncapLevel={editableUncap}
transcendenceStage={editableTrans}
editable
updateUncap={(level) => (editableUncap = level)}
updateTranscendence={(stage) => (editableTrans = stage)}
/>
<p style="font-size: 12px;">
Current: {editableUncap}★ / Transcendence: {editableTrans}
</p>
</div>
</Story>

View file

@ -0,0 +1,186 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf'
import Button from '$lib/components/ui/Button.svelte'
import { fn } from 'storybook/test'
const { Story } = defineMeta({
title: 'Components/UI/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'ghost', 'text', 'destructive', 'notice', 'subtle'],
description: 'Visual style variant'
},
size: {
control: 'select',
options: ['small', 'medium', 'large', 'icon'],
description: 'Button size'
},
shape: {
control: 'select',
options: ['default', 'circular', 'pill'],
description: 'Border radius shape'
},
element: {
control: 'select',
options: [undefined, 'wind', 'fire', 'water', 'earth', 'dark', 'light'],
description: 'Element color theme'
},
elementStyle: {
control: 'boolean',
description: 'Apply element-specific button styling'
},
contained: {
control: 'boolean',
description: 'Contained background style'
},
disabled: {
control: 'boolean',
description: 'Disabled state'
},
fullWidth: {
control: 'boolean',
description: 'Full width button'
},
iconOnly: {
control: 'boolean',
description: 'Icon only mode (no text)'
},
active: {
control: 'boolean',
description: 'Active/pressed state'
}
},
args: {
onclick: fn()
}
})
const variants = ['primary', 'secondary', 'ghost', 'text', 'destructive', 'notice', 'subtle']
const sizes = ['small', 'medium', 'large']
const elements = ['wind', 'fire', 'water', 'earth', 'dark', 'light']
</script>
<!-- Default - args-only for autodocs -->
<Story name="Default" args={{ variant: 'secondary' }}>
{#snippet children()}Button{/snippet}
</Story>
<!-- All Variants -->
<Story name="All Variants" asChild>
<div style="display: flex; flex-wrap: wrap; gap: 12px; align-items: center;">
{#each variants as variant}
<Button {variant}>{variant}</Button>
{/each}
</div>
</Story>
<!-- All Sizes -->
<Story name="All Sizes" asChild>
<div style="display: flex; flex-wrap: wrap; gap: 12px; align-items: center;">
{#each sizes as size}
<Button {size}>{size}</Button>
{/each}
<Button size="icon" icon="settings" iconOnly />
</div>
</Story>
<!-- Disabled States -->
<Story name="Disabled States" asChild>
<div style="display: flex; flex-wrap: wrap; gap: 12px; align-items: center;">
{#each variants as variant}
<Button {variant} disabled>{variant}</Button>
{/each}
</div>
</Story>
<!-- Shapes -->
<Story name="Shapes" asChild>
<div style="display: flex; flex-wrap: wrap; gap: 12px; align-items: center;">
<Button shape="default">Default</Button>
<Button shape="pill">Pill</Button>
<Button shape="circular" icon="plus" iconOnly />
</div>
</Story>
<!-- With Icons -->
<Story name="With Icons" asChild>
<div style="display: flex; flex-wrap: wrap; gap: 12px; align-items: center;">
<Button icon="plus" iconPosition="left">Add Item</Button>
<Button icon="arrow-right" iconPosition="right">Continue</Button>
<Button icon="settings" iconOnly size="icon" />
</div>
</Story>
<!-- Element Colors -->
<Story name="Element Colors" asChild>
<div style="display: flex; flex-direction: column; gap: 16px;">
<div>
<h4 style="margin: 0 0 8px; font-size: 14px; color: #666;">Without elementStyle</h4>
<div style="display: flex; flex-wrap: wrap; gap: 12px;">
{#each elements as element}
<Button {element}>{element}</Button>
{/each}
</div>
</div>
<div>
<h4 style="margin: 0 0 8px; font-size: 14px; color: #666;">With elementStyle</h4>
<div style="display: flex; flex-wrap: wrap; gap: 12px;">
{#each elements as element}
<Button {element} elementStyle>{element}</Button>
{/each}
</div>
</div>
</div>
</Story>
<!-- Contained -->
<Story name="Contained" asChild>
<div style="display: flex; flex-wrap: wrap; gap: 12px; align-items: center;">
<Button contained>Contained</Button>
<Button contained variant="primary">Primary Contained</Button>
</div>
</Story>
<!-- Full Width -->
<Story name="Full Width" asChild>
<div style="display: flex; flex-direction: column; gap: 12px; max-width: 300px;">
<Button fullWidth>Full Width Button</Button>
<Button fullWidth variant="primary">Primary Full Width</Button>
</div>
</Story>
<!-- Size Variant Matrix -->
<Story name="Size Variant Matrix" asChild>
<div style="display: grid; gap: 8px;">
<div
style="display: grid; grid-template-columns: 80px repeat({variants.length}, 1fr); gap: 8px; align-items: center;"
>
<span></span>
{#each variants as variant}
<span style="font-size: 12px; text-align: center; color: #666;">{variant}</span>
{/each}
</div>
{#each sizes as size}
<div
style="display: grid; grid-template-columns: 80px repeat({variants.length}, 1fr); gap: 8px; align-items: center;"
>
<span style="font-size: 12px; color: #666;">{size}</span>
{#each variants as variant}
<Button {variant} {size}>{size}</Button>
{/each}
</div>
{/each}
</div>
</Story>
<!-- Active States -->
<Story name="Active States" asChild>
<div style="display: flex; flex-wrap: wrap; gap: 12px; align-items: center;">
<Button active>Active</Button>
<Button active variant="primary">Active Primary</Button>
<Button active variant="ghost">Active Ghost</Button>
</div>
</Story>

View file

@ -0,0 +1,178 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf'
import Checkbox from '$lib/components/ui/checkbox/Checkbox.svelte'
import { fn } from 'storybook/test'
const { Story } = defineMeta({
title: 'Components/UI/Checkbox',
component: Checkbox,
tags: ['autodocs'],
argTypes: {
checked: {
control: 'boolean',
description: 'Whether the checkbox is checked'
},
indeterminate: {
control: 'boolean',
description: 'Indeterminate state (partial selection)'
},
size: {
control: 'select',
options: ['small', 'medium', 'large'],
description: 'Checkbox size'
},
variant: {
control: 'select',
options: ['default', 'bound'],
description: 'Visual variant'
},
contained: {
control: 'boolean',
description: 'Contained background style (alias for variant=bound)'
},
element: {
control: 'select',
options: [undefined, 'wind', 'fire', 'water', 'earth', 'dark', 'light'],
description: 'Element color theme for checked state'
},
disabled: {
control: 'boolean',
description: 'Disabled state'
},
required: {
control: 'boolean',
description: 'Required field'
}
},
args: {
onCheckedChange: fn()
}
})
</script>
<!-- Default - args-only for autodocs -->
<Story name="Default" args={{ checked: false }} />
<!-- Checked -->
<Story name="Checked" args={{ checked: true }} />
<!-- Indeterminate -->
<Story name="Indeterminate" args={{ indeterminate: true }} />
<!-- All Sizes -->
<Story name="All Sizes" asChild>
<div style="display: flex; flex-direction: column; gap: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 60px; font-size: 12px; color: #666;">Small</span>
<Checkbox size="small" />
<Checkbox size="small" checked />
<Checkbox size="small" indeterminate />
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 60px; font-size: 12px; color: #666;">Medium</span>
<Checkbox size="medium" />
<Checkbox size="medium" checked />
<Checkbox size="medium" indeterminate />
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 60px; font-size: 12px; color: #666;">Large</span>
<Checkbox size="large" />
<Checkbox size="large" checked />
<Checkbox size="large" indeterminate />
</div>
</div>
</Story>
<!-- Variants -->
<Story name="Variants" asChild>
<div style="display: flex; flex-direction: column; gap: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 80px; font-size: 12px; color: #666;">Default</span>
<Checkbox variant="default" />
<Checkbox variant="default" checked />
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 80px; font-size: 12px; color: #666;">Bound</span>
<Checkbox variant="bound" />
<Checkbox variant="bound" checked />
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 80px; font-size: 12px; color: #666;">Contained</span>
<Checkbox contained />
<Checkbox contained checked />
</div>
</div>
</Story>
<!-- Element Colors -->
<Story name="Element Colors" asChild>
<div style="display: flex; flex-direction: column; gap: 16px;">
{#each ['wind', 'fire', 'water', 'earth', 'dark', 'light'] as element}
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 50px; font-size: 12px; color: #666;">{element}</span>
<Checkbox {element} />
<Checkbox {element} checked />
<Checkbox {element} indeterminate />
</div>
{/each}
</div>
</Story>
<!-- Disabled States -->
<Story name="Disabled" asChild>
<div style="display: flex; gap: 12px; align-items: center;">
<Checkbox disabled />
<Checkbox disabled checked />
<Checkbox disabled indeterminate />
</div>
</Story>
<!-- With Labels -->
<Story name="With Labels" asChild>
<div style="display: flex; flex-direction: column; gap: 12px;">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<Checkbox />
<span>Accept terms and conditions</span>
</label>
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<Checkbox checked />
<span>Subscribe to newsletter</span>
</label>
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<Checkbox size="small" />
<span style="font-size: 14px;">Small checkbox with label</span>
</label>
</div>
</Story>
<!-- Checklist Example -->
<Story name="Checklist Example" asChild>
<div
style="display: flex; flex-direction: column; gap: 0; max-width: 250px; background: #f5f5f5; border-radius: 8px; padding: 8px 0;"
>
<label
style="display: flex; align-items: center; gap: 12px; padding: 8px 16px; cursor: pointer;"
>
<Checkbox checked />
<span>Complete profile</span>
</label>
<label
style="display: flex; align-items: center; gap: 12px; padding: 8px 16px; cursor: pointer;"
>
<Checkbox checked />
<span>Verify email</span>
</label>
<label
style="display: flex; align-items: center; gap: 12px; padding: 8px 16px; cursor: pointer;"
>
<Checkbox />
<span>Add payment method</span>
</label>
<label
style="display: flex; align-items: center; gap: 12px; padding: 8px 16px; cursor: pointer;"
>
<Checkbox />
<span>Enable 2FA</span>
</label>
</div>
</Story>

View file

@ -0,0 +1,161 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf'
import Dialog from '$lib/components/ui/Dialog.svelte'
import Button from '$lib/components/ui/Button.svelte'
const { Story } = defineMeta({
title: 'Components/UI/Dialog',
tags: ['autodocs']
})
</script>
<script>
let defaultOpen = $state(false)
let withDescOpen = $state(false)
let withFooterOpen = $state(false)
let longContentOpen = $state(false)
let formOpen = $state(false)
let confirmOpen = $state(false)
</script>
<!-- Default -->
<Story name="Default">
<div>
<Button onclick={() => (defaultOpen = true)}>Open Dialog</Button>
<Dialog bind:open={defaultOpen} title="Dialog Title">
{#snippet children()}
<p>This is the dialog content. You can put any content here.</p>
{/snippet}
</Dialog>
</div>
</Story>
<!-- With Description -->
<Story name="With Description">
<div>
<Button onclick={() => (withDescOpen = true)}>Open Dialog</Button>
<Dialog
bind:open={withDescOpen}
title="Account Settings"
description="Make changes to your account settings here."
>
{#snippet children()}
<p>Your account settings form would go here.</p>
{/snippet}
</Dialog>
</div>
</Story>
<!-- With Footer -->
<Story name="With Footer">
<div>
<Button onclick={() => (withFooterOpen = true)}>Open Dialog</Button>
<Dialog bind:open={withFooterOpen} title="Confirm Action">
{#snippet children()}
<p>Are you sure you want to proceed with this action?</p>
{/snippet}
{#snippet footer()}
<Button variant="secondary" onclick={() => (withFooterOpen = false)}>Cancel</Button>
<Button variant="primary" onclick={() => (withFooterOpen = false)}>Confirm</Button>
{/snippet}
</Dialog>
</div>
</Story>
<!-- Long Content -->
<Story name="Long Content">
<div>
<Button onclick={() => (longContentOpen = true)}>Open Long Dialog</Button>
<Dialog bind:open={longContentOpen} title="Terms and Conditions">
{#snippet children()}
<div style="display: flex; flex-direction: column; gap: 16px;">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris.
</p>
<p>
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
nulla pariatur. Excepteur sint occaecat cupidatat non proident.
</p>
<p>
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque
laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis.
</p>
<p>
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium
voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint.
</p>
<p>
Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id
quod maxime placeat facere possimus.
</p>
</div>
{/snippet}
{#snippet footer()}
<Button variant="secondary" onclick={() => (longContentOpen = false)}>Decline</Button>
<Button variant="primary" onclick={() => (longContentOpen = false)}>Accept</Button>
{/snippet}
</Dialog>
</div>
</Story>
<!-- Form Dialog -->
<Story name="Form Dialog">
<div>
<Button onclick={() => (formOpen = true)}>Edit Profile</Button>
<Dialog bind:open={formOpen} title="Edit Profile" description="Update your profile information.">
{#snippet children()}
<div style="display: flex; flex-direction: column; gap: 16px;">
<div>
<label
style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px;"
for="name">Name</label
>
<input
id="name"
type="text"
placeholder="Enter your name"
style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px;"
/>
</div>
<div>
<label
style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px;"
for="email">Email</label
>
<input
id="email"
type="email"
placeholder="Enter your email"
style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px;"
/>
</div>
</div>
{/snippet}
{#snippet footer()}
<Button variant="secondary" onclick={() => (formOpen = false)}>Cancel</Button>
<Button variant="primary" onclick={() => (formOpen = false)}>Save Changes</Button>
{/snippet}
</Dialog>
</div>
</Story>
<!-- Confirmation Dialog -->
<Story name="Confirmation Dialog">
<div>
<Button variant="destructive" onclick={() => (confirmOpen = true)}>Delete Item</Button>
<Dialog bind:open={confirmOpen} title="Delete Item">
{#snippet children()}
<p>
Are you sure you want to delete this item? This action cannot be undone and all associated
data will be permanently removed.
</p>
{/snippet}
{#snippet footer()}
<Button variant="secondary" onclick={() => (confirmOpen = false)}>Cancel</Button>
<Button variant="destructive" onclick={() => (confirmOpen = false)}>Delete</Button>
{/snippet}
</Dialog>
</div>
</Story>

View file

@ -0,0 +1,133 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf'
import DropdownMenu from '$lib/components/ui/DropdownMenu.svelte'
import { DropdownMenu as DropdownMenuBase } from 'bits-ui'
import Button from '$lib/components/ui/Button.svelte'
const { Story } = defineMeta({
title: 'Components/UI/DropdownMenu',
tags: ['autodocs']
})
</script>
<!-- Default -->
<Story name="Default">
<DropdownMenu>
{#snippet trigger({ props })}
<Button {...props}>Open Menu</Button>
{/snippet}
{#snippet menu()}
<DropdownMenuBase.Item class="dropdown-menu-item">Edit</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">Duplicate</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">Archive</DropdownMenuBase.Item>
{/snippet}
</DropdownMenu>
</Story>
<!-- With Separator -->
<Story name="With Separator">
<DropdownMenu>
{#snippet trigger({ props })}
<Button variant="secondary" {...props}>Actions</Button>
{/snippet}
{#snippet menu()}
<DropdownMenuBase.Item class="dropdown-menu-item">New File</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">New Folder</DropdownMenuBase.Item>
<DropdownMenuBase.Separator class="dropdown-menu-separator" />
<DropdownMenuBase.Item class="dropdown-menu-item">Import</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">Export</DropdownMenuBase.Item>
<DropdownMenuBase.Separator class="dropdown-menu-separator" />
<DropdownMenuBase.Item class="dropdown-menu-item danger">Delete</DropdownMenuBase.Item>
{/snippet}
</DropdownMenu>
</Story>
<!-- With Icons -->
<Story name="With Icons">
<DropdownMenu>
{#snippet trigger({ props })}
<Button {...props}>File</Button>
{/snippet}
{#snippet menu()}
<DropdownMenuBase.Item class="dropdown-menu-item">New Document</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">New Folder</DropdownMenuBase.Item>
<DropdownMenuBase.Separator class="dropdown-menu-separator" />
<DropdownMenuBase.Item class="dropdown-menu-item">Save</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">Save As...</DropdownMenuBase.Item>
{/snippet}
</DropdownMenu>
</Story>
<!-- Danger Actions -->
<Story name="Danger Actions">
<DropdownMenu>
{#snippet trigger({ props })}
<Button variant="secondary" {...props}>Manage</Button>
{/snippet}
{#snippet menu()}
<DropdownMenuBase.Item class="dropdown-menu-item">Edit</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">Move</DropdownMenuBase.Item>
<DropdownMenuBase.Separator class="dropdown-menu-separator" />
<DropdownMenuBase.Item class="dropdown-menu-item danger">Delete</DropdownMenuBase.Item>
{/snippet}
</DropdownMenu>
</Story>
<!-- User Menu Example -->
<Story name="User Menu Example">
<DropdownMenu>
{#snippet trigger({ props })}
<Button variant="subtle" {...props}>
{#snippet leftAccessory()}
<span
style="width: 24px; height: 24px; background: #6366f1; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-weight: 600; font-size: 12px;"
>J</span
>
{/snippet}
John Doe
</Button>
{/snippet}
{#snippet menu()}
<DropdownMenuBase.Item class="dropdown-menu-item">Profile</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">Settings</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">Help</DropdownMenuBase.Item>
<DropdownMenuBase.Separator class="dropdown-menu-separator" />
<DropdownMenuBase.Item class="dropdown-menu-item danger">Sign out</DropdownMenuBase.Item>
{/snippet}
</DropdownMenu>
</Story>
<!-- Icon Button Trigger -->
<Story name="Icon Button Trigger">
<DropdownMenu>
{#snippet trigger({ props })}
<Button size="small" iconOnly icon="ellipsis" {...props} />
{/snippet}
{#snippet menu()}
<DropdownMenuBase.Item class="dropdown-menu-item">View</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">Edit</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">Share</DropdownMenuBase.Item>
<DropdownMenuBase.Separator class="dropdown-menu-separator" />
<DropdownMenuBase.Item class="dropdown-menu-item danger">Delete</DropdownMenuBase.Item>
{/snippet}
</DropdownMenu>
</Story>
<!-- Long Menu -->
<Story name="Long Menu">
<DropdownMenu>
{#snippet trigger({ props })}
<Button {...props}>Select Category</Button>
{/snippet}
{#snippet menu()}
<DropdownMenuBase.Item class="dropdown-menu-item">Characters</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">Weapons</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">Summons</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">Classes</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">Skills</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">Raids</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">Materials</DropdownMenuBase.Item>
<DropdownMenuBase.Item class="dropdown-menu-item">Items</DropdownMenuBase.Item>
{/snippet}
</DropdownMenu>
</Story>

View file

@ -0,0 +1,171 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf'
import Input from '$lib/components/ui/Input.svelte'
const { Story } = defineMeta({
title: 'Components/UI/Input',
component: Input,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['default', 'contained', 'duration', 'number', 'range'],
description: 'Input variant style'
},
contained: {
control: 'boolean',
description: 'Contained background style'
},
label: {
control: 'text',
description: 'Field label'
},
error: {
control: 'text',
description: 'Error message'
},
placeholder: {
control: 'text',
description: 'Placeholder text'
},
leftIcon: {
control: 'text',
description: 'Left icon name'
},
rightIcon: {
control: 'text',
description: 'Right icon name'
},
disabled: {
control: 'boolean',
description: 'Disabled state'
},
required: {
control: 'boolean',
description: 'Required field'
},
readonly: {
control: 'boolean',
description: 'Read-only field'
},
fullWidth: {
control: 'boolean',
description: 'Full width input'
},
maxLength: {
control: 'number',
description: 'Maximum character length'
}
}
})
</script>
<!-- Default - args-only for autodocs -->
<Story name="Default" args={{ placeholder: 'Enter text...' }} />
<!-- With Label -->
<Story name="With Label" args={{ label: 'Username', placeholder: 'Enter username' }} />
<!-- Required Field -->
<Story name="Required Field" args={{ label: 'Email', placeholder: 'Enter email', required: true }} />
<!-- With Error -->
<Story
name="With Error"
args={{
label: 'Password',
type: 'password',
error: 'Password must be at least 8 characters',
value: 'abc'
}}
/>
<!-- Contained Variant -->
<Story name="Contained" args={{ variant: 'contained', placeholder: 'Contained input' }} />
<!-- With Icons -->
<Story name="With Icons" asChild>
<div style="display: flex; flex-direction: column; gap: 12px; max-width: 300px;">
<Input leftIcon="search" placeholder="Search..." />
<Input rightIcon="info" placeholder="With right icon" />
<Input leftIcon="user" rightIcon="check" placeholder="Both icons" />
</div>
</Story>
<!-- With Character Counter -->
<Story name="Character Counter" asChild>
<div style="display: flex; flex-direction: column; gap: 12px; max-width: 300px;">
<Input placeholder="Type something..." counter maxLength={100} label="With max length" />
<Input placeholder="No max limit" counter label="Counter only" />
</div>
</Story>
<!-- Number Input -->
<Story name="Number Input" asChild>
<div style="display: flex; gap: 12px; align-items: center;">
<span style="color: #666;">Quantity:</span>
<Input variant="number" type="number" value="0" />
</div>
</Story>
<!-- Range Input -->
<Story name="Range Input" asChild>
<div style="display: flex; gap: 12px; align-items: center;">
<span style="color: #666;">Level:</span>
<Input variant="range" type="number" value="1" />
</div>
</Story>
<!-- Disabled State -->
<Story name="Disabled" asChild>
<div style="display: flex; flex-direction: column; gap: 12px; max-width: 300px;">
<Input placeholder="Disabled input" disabled />
<Input label="Disabled with label" placeholder="Cannot edit" disabled />
</div>
</Story>
<!-- Read-only State -->
<Story
name="Read-only"
args={{ label: 'Read-only field', value: 'This cannot be changed', readonly: true }}
/>
<!-- Full Width -->
<Story
name="Full Width"
args={{ label: 'Full width input', placeholder: 'Takes full container width', fullWidth: true }}
/>
<!-- Password Input -->
<Story
name="Password Input"
args={{ type: 'password', label: 'Password', placeholder: 'Enter password' }}
/>
<!-- All Variants -->
<Story name="All Variants" asChild>
<div style="display: flex; flex-direction: column; gap: 16px; max-width: 300px;">
<Input variant="default" placeholder="Default variant" label="Default" />
<Input variant="contained" placeholder="Contained variant" label="Contained" />
<div style="display: flex; gap: 12px; align-items: end;">
<div>
<span style="font-size: 12px; color: #666; display: block; margin-bottom: 4px;">Number</span>
<Input variant="number" type="number" value="42" />
</div>
<div>
<span style="font-size: 12px; color: #666; display: block; margin-bottom: 4px;">Range</span>
<Input variant="range" type="number" value="100" />
</div>
</div>
</div>
</Story>
<!-- Form Example -->
<Story name="Form Example" asChild>
<div style="display: flex; flex-direction: column; gap: 16px; max-width: 350px;">
<Input label="Full Name" placeholder="John Doe" required />
<Input label="Email" type="email" placeholder="john@example.com" required leftIcon="mail" />
<Input label="Password" type="password" placeholder="Min 8 characters" required />
<Input label="Bio" placeholder="Tell us about yourself..." counter maxLength={200} />
</div>
</Story>

View file

@ -0,0 +1,138 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf'
import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
import Segment from '$lib/components/ui/segmented-control/Segment.svelte'
const { Story } = defineMeta({
title: 'Components/UI/SegmentedControl',
tags: ['autodocs']
})
</script>
<script>
let viewValue = $state('grid')
let twoOptValue = $state('on')
let blendedValue = $state('all')
let bgValue = $state('day')
let elementValue = $state('wind')
let gapValue = $state('first')
let growValue = $state('characters')
let tabValue = $state('characters')
let disabledValue = $state('enabled')
</script>
<!-- Default -->
<Story name="Default">
<SegmentedControl bind:value={viewValue}>
<Segment value="grid">Grid</Segment>
<Segment value="list">List</Segment>
<Segment value="compact">Compact</Segment>
</SegmentedControl>
</Story>
<!-- Two Options -->
<Story name="Two Options">
<SegmentedControl bind:value={twoOptValue}>
<Segment value="on">On</Segment>
<Segment value="off">Off</Segment>
</SegmentedControl>
</Story>
<!-- Blended Variant -->
<Story name="Blended Variant">
<SegmentedControl variant="blended" bind:value={blendedValue}>
<Segment value="all">All</Segment>
<Segment value="active">Active</Segment>
<Segment value="archived">Archived</Segment>
</SegmentedControl>
</Story>
<!-- Background Variant -->
<Story name="Background Variant">
<SegmentedControl variant="background" bind:value={bgValue}>
<Segment value="day">Day</Segment>
<Segment value="week">Week</Segment>
<Segment value="month">Month</Segment>
</SegmentedControl>
</Story>
<!-- Element Colors -->
<Story name="Element Colors">
<div style="display: flex; flex-direction: column; gap: 16px;">
<SegmentedControl bind:value={elementValue} element="wind">
<Segment value="wind">Wind</Segment>
<Segment value="other">Other</Segment>
</SegmentedControl>
<SegmentedControl element="fire" value="fire">
<Segment value="fire">Fire</Segment>
<Segment value="other">Other</Segment>
</SegmentedControl>
<SegmentedControl element="water" value="water">
<Segment value="water">Water</Segment>
<Segment value="other">Other</Segment>
</SegmentedControl>
<SegmentedControl element="earth" value="earth">
<Segment value="earth">Earth</Segment>
<Segment value="other">Other</Segment>
</SegmentedControl>
<SegmentedControl element="dark" value="dark">
<Segment value="dark">Dark</Segment>
<Segment value="other">Other</Segment>
</SegmentedControl>
<SegmentedControl element="light" value="light">
<Segment value="light">Light</Segment>
<Segment value="other">Other</Segment>
</SegmentedControl>
</div>
</Story>
<!-- With Gap -->
<Story name="With Gap">
<SegmentedControl gap bind:value={gapValue}>
<Segment value="first">First</Segment>
<Segment value="second">Second</Segment>
<Segment value="third">Third</Segment>
</SegmentedControl>
</Story>
<!-- Grow to Fill -->
<Story name="Grow to Fill">
<div style="width: 400px; border: 1px dashed #ccc; padding: 16px;">
<SegmentedControl grow bind:value={growValue}>
<Segment value="characters">Characters</Segment>
<Segment value="weapons">Weapons</Segment>
<Segment value="summons">Summons</Segment>
</SegmentedControl>
</div>
</Story>
<!-- Tab Example -->
<Story name="Tab Example">
<div style="max-width: 500px;">
<SegmentedControl bind:value={tabValue} grow>
<Segment value="characters">Characters</Segment>
<Segment value="weapons">Weapons</Segment>
<Segment value="summons">Summons</Segment>
</SegmentedControl>
<div
style="margin-top: 16px; padding: 16px; background: #f5f5f5; border-radius: 8px; min-height: 100px;"
>
{#if tabValue === 'characters'}
<p>Character content goes here</p>
{:else if tabValue === 'weapons'}
<p>Weapon content goes here</p>
{:else}
<p>Summon content goes here</p>
{/if}
</div>
</div>
</Story>
<!-- Disabled Segment -->
<Story name="Disabled Segment">
<SegmentedControl bind:value={disabledValue}>
<Segment value="enabled">Enabled</Segment>
<Segment value="also-enabled">Also Enabled</Segment>
<Segment value="disabled" disabled>Disabled</Segment>
</SegmentedControl>
</Story>

View file

@ -0,0 +1,154 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf'
import Select from '$lib/components/ui/Select.svelte'
import { fn } from 'storybook/test'
const { Story } = defineMeta({
title: 'Components/UI/Select',
component: Select,
tags: ['autodocs'],
argTypes: {
size: {
control: 'select',
options: ['small', 'medium', 'large'],
description: 'Select size'
},
contained: {
control: 'boolean',
description: 'Contained background style'
},
disabled: {
control: 'boolean',
description: 'Disabled state'
},
required: {
control: 'boolean',
description: 'Required field'
},
fullWidth: {
control: 'boolean',
description: 'Full width select'
},
label: {
control: 'text',
description: 'Field label'
},
error: {
control: 'text',
description: 'Error message'
},
placeholder: {
control: 'text',
description: 'Placeholder text'
}
},
args: {
onValueChange: fn()
}
})
const basicOptions = [
{ value: 'wind', label: 'Wind' },
{ value: 'fire', label: 'Fire' },
{ value: 'water', label: 'Water' },
{ value: 'earth', label: 'Earth' },
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' }
]
const numberOptions = [
{ value: 1, label: 'Level 1' },
{ value: 2, label: 'Level 2' },
{ value: 3, label: 'Level 3' },
{ value: 4, label: 'Level 4' },
{ value: 5, label: 'Level 5' }
]
const disabledOptions = [
{ value: 'option1', label: 'Available Option' },
{ value: 'option2', label: 'Also Available' },
{ value: 'option3', label: 'Unavailable', disabled: true },
{ value: 'option4', label: 'Another Available' }
]
</script>
<!-- Default - args-only for autodocs -->
<Story name="Default" args={{ options: basicOptions, placeholder: 'Select element...' }} />
<!-- With Value -->
<Story name="With Value" args={{ options: basicOptions, value: 'fire' }} />
<!-- With Label -->
<Story
name="With Label"
args={{ options: basicOptions, label: 'Element', placeholder: 'Choose element' }}
/>
<!-- Required Field -->
<Story
name="Required Field"
args={{ options: basicOptions, label: 'Primary Element', placeholder: 'Required', required: true }}
/>
<!-- With Error -->
<Story
name="With Error"
args={{ options: basicOptions, label: 'Element', error: 'Please select an element' }}
/>
<!-- Contained Variant -->
<Story name="Contained" args={{ options: basicOptions, contained: true, placeholder: 'Contained select' }} />
<!-- All Sizes -->
<Story name="All Sizes" asChild>
<div style="display: flex; flex-direction: column; gap: 12px; max-width: 200px;">
<Select options={basicOptions} size="small" value="wind" />
<Select options={basicOptions} size="medium" value="fire" />
<Select options={basicOptions} size="large" value="water" />
</div>
</Story>
<!-- Size Comparison -->
<Story name="Size Comparison" asChild>
<div style="display: flex; gap: 12px; align-items: start; flex-wrap: wrap;">
<div>
<span style="font-size: 12px; color: #666; display: block; margin-bottom: 4px;">Small</span>
<Select options={basicOptions} size="small" value="wind" />
</div>
<div>
<span style="font-size: 12px; color: #666; display: block; margin-bottom: 4px;">Medium</span>
<Select options={basicOptions} size="medium" value="fire" />
</div>
<div>
<span style="font-size: 12px; color: #666; display: block; margin-bottom: 4px;">Large</span>
<Select options={basicOptions} size="large" value="water" />
</div>
</div>
</Story>
<!-- With Number Values -->
<Story name="Number Values" args={{ options: numberOptions, label: 'Select Level', value: 3 }} />
<!-- Disabled Options -->
<Story
name="Disabled Options"
args={{ options: disabledOptions, label: 'Select Option', placeholder: 'Some options disabled' }}
/>
<!-- Disabled State -->
<Story name="Disabled State" args={{ options: basicOptions, disabled: true, value: 'earth' }} />
<!-- Full Width -->
<Story
name="Full Width"
args={{ options: basicOptions, label: 'Full Width Select', fullWidth: true, placeholder: 'Takes full width' }}
/>
<!-- Form Example -->
<Story name="Form Example" asChild>
<div style="display: flex; flex-direction: column; gap: 16px; max-width: 300px;">
<Select options={basicOptions} label="Main Element" placeholder="Select..." required />
<Select options={basicOptions} label="Secondary Element" placeholder="Optional" />
<Select options={numberOptions} label="Skill Level" value={1} />
</div>
</Story>

View file

@ -0,0 +1,134 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf'
import Switch from '$lib/components/ui/switch/Switch.svelte'
import { fn } from 'storybook/test'
const { Story } = defineMeta({
title: 'Components/UI/Switch',
component: Switch,
tags: ['autodocs'],
argTypes: {
checked: {
control: 'boolean',
description: 'Whether the switch is checked'
},
size: {
control: 'select',
options: ['small', 'medium', 'large'],
description: 'Switch size'
},
element: {
control: 'select',
options: [undefined, 'wind', 'fire', 'water', 'earth', 'dark', 'light'],
description: 'Element color theme for checked state'
},
disabled: {
control: 'boolean',
description: 'Disabled state'
},
required: {
control: 'boolean',
description: 'Required field'
},
fullWidth: {
control: 'boolean',
description: 'Full width switch'
}
},
args: {
onCheckedChange: fn()
}
})
</script>
<!-- Default - args-only for autodocs -->
<Story name="Default" args={{ checked: false }} />
<!-- Checked -->
<Story name="Checked" args={{ checked: true }} />
<!-- All Sizes -->
<Story name="All Sizes" asChild>
<div style="display: flex; flex-direction: column; gap: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 60px; font-size: 12px; color: #666;">Small</span>
<Switch size="small" />
<Switch size="small" checked />
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 60px; font-size: 12px; color: #666;">Medium</span>
<Switch size="medium" />
<Switch size="medium" checked />
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 60px; font-size: 12px; color: #666;">Large</span>
<Switch size="large" />
<Switch size="large" checked />
</div>
</div>
</Story>
<!-- Element Colors -->
<Story name="Element Colors" asChild>
<div style="display: flex; flex-direction: column; gap: 16px;">
{#each ['wind', 'fire', 'water', 'earth', 'dark', 'light'] as element}
<div style="display: flex; align-items: center; gap: 12px;">
<span style="width: 50px; font-size: 12px; color: #666;">{element}</span>
<Switch {element} />
<Switch {element} checked />
</div>
{/each}
</div>
</Story>
<!-- Disabled States -->
<Story name="Disabled" asChild>
<div style="display: flex; gap: 16px; align-items: center;">
<Switch disabled />
<Switch disabled checked />
</div>
</Story>
<!-- With Labels -->
<Story name="With Labels" asChild>
<div style="display: flex; flex-direction: column; gap: 16px;">
<label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
<Switch />
<span>Enable notifications</span>
</label>
<label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
<Switch checked />
<span>Dark mode</span>
</label>
<label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
<Switch size="small" />
<span style="font-size: 14px;">Small switch with label</span>
</label>
</div>
</Story>
<!-- Settings Example -->
<Story name="Settings Example" asChild>
<div
style="display: flex; flex-direction: column; gap: 0; max-width: 300px; background: #f5f5f5; border-radius: 8px; padding: 4px 0;"
>
<div
style="display: flex; justify-content: space-between; align-items: center; padding: 12px 16px;"
>
<span>Push Notifications</span>
<Switch size="small" checked />
</div>
<div
style="display: flex; justify-content: space-between; align-items: center; padding: 12px 16px;"
>
<span>Email Updates</span>
<Switch size="small" />
</div>
<div
style="display: flex; justify-content: space-between; align-items: center; padding: 12px 16px;"
>
<span>Auto-save</span>
<Switch size="small" checked />
</div>
</div>
</Story>

View file

@ -0,0 +1,89 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf'
import Tooltip from '$lib/components/ui/Tooltip.svelte'
import Button from '$lib/components/ui/Button.svelte'
const { Story } = defineMeta({
title: 'Components/UI/Tooltip',
tags: ['autodocs']
})
</script>
<!-- Default -->
<Story name="Default">
<Tooltip content="This is a tooltip">
<Button>Hover me</Button>
</Tooltip>
</Story>
<!-- With Long Text -->
<Story name="Long Text">
<Tooltip
content="This is a longer tooltip message that contains more detailed information about the element."
>
<Button>Hover for details</Button>
</Tooltip>
</Story>
<!-- Custom Delay -->
<Story name="Custom Delay">
<div style="display: flex; gap: 16px;">
<Tooltip content="Instant tooltip" delayDuration={0}>
<Button variant="secondary">No delay</Button>
</Tooltip>
<Tooltip content="Default delay (200ms)">
<Button variant="secondary">Default</Button>
</Tooltip>
<Tooltip content="Slow tooltip" delayDuration={500}>
<Button variant="secondary">500ms delay</Button>
</Tooltip>
</div>
</Story>
<!-- Disabled -->
<Story name="Disabled">
<div style="display: flex; gap: 16px;">
<Tooltip content="This tooltip is visible" disabled={false}>
<Button>Enabled</Button>
</Tooltip>
<Tooltip content="This tooltip won't show" disabled>
<Button variant="secondary">Disabled</Button>
</Tooltip>
</div>
</Story>
<!-- On Different Elements -->
<Story name="On Different Elements">
<div style="display: flex; gap: 24px; align-items: center;">
<Tooltip content="Button tooltip">
<Button size="small">Button</Button>
</Tooltip>
<Tooltip content="Text tooltip">
<span style="text-decoration: underline; cursor: help;">Hover this text</span>
</Tooltip>
<Tooltip content="Icon tooltip">
<span
style="display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; background: #f0f0f0; border-radius: 50%; cursor: pointer;"
>?</span
>
</Tooltip>
</div>
</Story>
<!-- Icon Button Example -->
<Story name="Icon Button Example">
<div style="display: flex; gap: 8px;">
<Tooltip content="Edit">
<Button size="small" variant="secondary">Edit</Button>
</Tooltip>
<Tooltip content="Delete">
<Button size="small" variant="secondary">Delete</Button>
</Tooltip>
<Tooltip content="Share">
<Button size="small" variant="secondary">Share</Button>
</Tooltip>
<Tooltip content="Favorite">
<Button size="small" variant="secondary">Fav</Button>
</Tooltip>
</div>
</Story>

View file

@ -0,0 +1,174 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf'
import Typeahead from '$lib/components/ui/Typeahead.svelte'
import { fn } from 'storybook/test'
const { Story } = defineMeta({
title: 'Components/UI/Typeahead',
component: Typeahead,
tags: ['autodocs'],
argTypes: {
searchable: {
control: 'boolean',
description: 'Enable search/filtering'
},
multiple: {
control: 'boolean',
description: 'Allow multiple selections'
},
creatable: {
control: 'boolean',
description: 'Allow creating new options'
},
clearable: {
control: 'boolean',
description: 'Show clear button'
},
disabled: {
control: 'boolean',
description: 'Disabled state'
},
size: {
control: 'select',
options: ['small', 'medium', 'large'],
description: 'Component size'
},
contained: {
control: 'boolean',
description: 'Contained background style'
},
fullWidth: {
control: 'boolean',
description: 'Full width'
},
placeholder: {
control: 'text',
description: 'Placeholder text'
}
},
args: {
onValueChange: fn()
}
})
const elementOptions = [
{ value: 'wind', label: 'Wind' },
{ value: 'fire', label: 'Fire' },
{ value: 'water', label: 'Water' },
{ value: 'earth', label: 'Earth' },
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' }
]
const characterOptions = [
{ value: 'narmaya', label: 'Narmaya', element: 'dark' },
{ value: 'cagliostro', label: 'Cagliostro', element: 'earth' },
{ value: 'zeta', label: 'Zeta', element: 'fire' },
{ value: 'yurius', label: 'Yurius', element: 'wind' },
{ value: 'lily', label: 'Lily', element: 'water' },
{ value: 'lucio', label: 'Lucio', element: 'light' },
{ value: 'olivia', label: 'Olivia', element: 'dark' },
{ value: 'anila', label: 'Anila', element: 'fire' }
]
const tagOptions = [
{ value: 'attack', label: 'Attack' },
{ value: 'defense', label: 'Defense' },
{ value: 'support', label: 'Support' },
{ value: 'healing', label: 'Healing' },
{ value: 'buffer', label: 'Buffer' },
{ value: 'debuffer', label: 'Debuffer' }
]
</script>
<!-- Default - args-only for autodocs -->
<Story name="Default" args={{ options: elementOptions, placeholder: 'Select element...' }} />
<!-- With Label -->
<Story
name="With Label"
args={{ options: elementOptions, label: 'Element', placeholder: 'Search elements...' }}
/>
<!-- Required Field -->
<Story
name="Required Field"
args={{ options: elementOptions, label: 'Primary Element', placeholder: 'Required', required: true }}
/>
<!-- With Error -->
<Story
name="With Error"
args={{ options: elementOptions, label: 'Element', error: 'Please select an element' }}
/>
<!-- Multiple Selection -->
<Story name="Multiple Selection" asChild>
<div style="max-width: 400px;">
<Typeahead options={tagOptions} multiple label="Tags" placeholder="Select tags..." />
</div>
</Story>
<!-- Multiple with Max -->
<Story name="Multiple with Max" asChild>
<div style="max-width: 400px;">
<Typeahead
options={characterOptions}
multiple
max={3}
label="Select up to 3 characters"
placeholder="Choose characters..."
/>
</div>
</Story>
<!-- Creatable -->
<Story name="Creatable" asChild>
<div style="max-width: 300px;">
<Typeahead
options={tagOptions}
creatable
label="Tags (can create new)"
placeholder="Type to add..."
/>
</div>
</Story>
<!-- All Sizes -->
<Story name="All Sizes" asChild>
<div style="display: flex; flex-direction: column; gap: 16px; max-width: 300px;">
<Typeahead options={elementOptions} size="small" value="wind" label="Small" />
<Typeahead options={elementOptions} size="medium" value="fire" label="Medium" />
<Typeahead options={elementOptions} size="large" value="water" label="Large" />
</div>
</Story>
<!-- Contained -->
<Story name="Contained" args={{ options: elementOptions, contained: true, placeholder: 'Contained style' }} />
<!-- Disabled -->
<Story name="Disabled" args={{ options: elementOptions, disabled: true, value: 'earth' }} />
<!-- Not Clearable -->
<Story
name="Not Clearable"
args={{ options: elementOptions, clearable: false, value: 'dark', label: 'Cannot clear' }}
/>
<!-- Character Search Example -->
<Story name="Character Search Example" asChild>
<div style="max-width: 350px;">
<Typeahead
options={characterOptions}
label="Search Character"
placeholder="Type character name..."
searchable
/>
</div>
</Story>
<!-- Full Width -->
<Story
name="Full Width"
args={{ options: elementOptions, label: 'Full Width Select', fullWidth: true, placeholder: 'Takes full width' }}
/>

View file

@ -0,0 +1,238 @@
import { Meta, ColorPalette, ColorItem } from '@storybook/addon-docs/blocks';
<Meta title="Foundations/Colors" />
# Colors
The Hensei color system is built around a neutral grey scale, accent colors, and element-specific palettes for the six Granblue Fantasy elements.
## Grey Scale
The foundation of our color system. Used for backgrounds, text, borders, and UI elements.
<ColorPalette>
<ColorItem
title="Grey 00"
subtitle="$grey-00"
colors={{ Black: '#000000' }}
/>
<ColorItem
title="Grey 10-20"
subtitle="Dark backgrounds"
colors={{ 'Grey 10': '#111111', 'Grey 15': '#191919', 'Grey 20': '#212121' }}
/>
<ColorItem
title="Grey 30-50"
subtitle="Dark mode UI"
colors={{ 'Grey 30': '#2f2f2f', 'Grey 40': '#444444', 'Grey 50': '#777777' }}
/>
<ColorItem
title="Grey 60-80"
subtitle="Secondary text, borders"
colors={{ 'Grey 60': '#a9a9a9', 'Grey 70': '#c6c6c6', 'Grey 80': '#e9e9e9' }}
/>
<ColorItem
title="Grey 85-100"
subtitle="Light backgrounds"
colors={{ 'Grey 85': '#efefef', 'Grey 90': '#f5f5f5', 'Grey 100': '#ffffff' }}
/>
</ColorPalette>
## Accent Colors
Primary accent colors for interactive elements and highlights.
<ColorPalette>
<ColorItem
title="Blue (Primary)"
subtitle="Links, primary actions"
colors={{ 'Light': '#275dc5', 'Light Focus': '#0c398d', 'Dark': '#6195f4', 'Dark Focus': '#275dc5' }}
/>
<ColorItem
title="Yellow (Highlight)"
subtitle="Selected items, highlights"
colors={{ 'Light': '#c89d39', 'Dark': '#f9cc64', 'Highlight': '#ffed4c' }}
/>
<ColorItem
title="Error"
subtitle="Destructive actions"
colors={{ 'Red': '#d13a3a', 'Error BG Light': '#fce8e8', 'Error BG Dark': '#3d1515' }}
/>
</ColorPalette>
## Element Colors
Granblue Fantasy element-specific colors used for theming characters, weapons, and summons.
### Wind
<ColorPalette>
<ColorItem
title="Wind"
subtitle="Green element"
colors={{
'Text Light': '#006a45',
'Text Dark': '#1dc688',
'BG': '#3ee489',
'Portrait': '#cdffed'
}}
/>
</ColorPalette>
### Fire
<ColorPalette>
<ColorItem
title="Fire"
subtitle="Red element"
colors={{
'Text Light': '#6e0000',
'Text Dark': '#ec5c5c',
'BG': '#fa6d6d',
'Portrait': '#ffcdcd'
}}
/>
</ColorPalette>
### Water
<ColorPalette>
<ColorItem
title="Water"
subtitle="Blue element"
colors={{
'Text Light': '#00639c',
'Text Dark': '#5cb7ec',
'BG': '#6cc9ff',
'Portrait': '#cdedff'
}}
/>
</ColorPalette>
### Earth
<ColorPalette>
<ColorItem
title="Earth"
subtitle="Orange element"
colors={{
'Text Light': '#8e3c0b',
'Text Dark': '#ec985c',
'BG': '#fd9f5b',
'Portrait': '#ffe2cd'
}}
/>
</ColorPalette>
### Light
<ColorPalette>
<ColorItem
title="Light"
subtitle="Yellow element"
colors={{
'Text Light': '#715100',
'Text Dark': '#c59c0c',
'BG': '#e8d633',
'Portrait': '#fffacd'
}}
/>
</ColorPalette>
### Dark
<ColorPalette>
<ColorItem
title="Dark"
subtitle="Purple element"
colors={{
'Text Light': '#560075',
'Text Dark': '#c65cec',
'BG': '#de7bff',
'Portrait': '#f2cdff'
}}
/>
</ColorPalette>
## Special Purpose Colors
### Extra Weapons (Purple)
<ColorPalette>
<ColorItem
title="Extra Purple"
subtitle="Additional weapon slots"
colors={{
'BG Light': '#ecebff',
'BG Dark': '#635fb7',
'Card Light': '#d5d3f6',
'Primary': '#8c86ff'
}}
/>
</ColorPalette>
### Subaura Summons (Orange)
<ColorPalette>
<ColorItem
title="Subaura Orange"
subtitle="Sub-aura summon slots"
colors={{
'BG Light': '#ffebd9',
'BG Dark': '#6b401b',
'Card Light': '#facea7',
'Primary': '#d08f57'
}}
/>
</ColorPalette>
### Game Tokens
<ColorPalette>
<ColorItem
title="Charge Attack"
subtitle="Ougi/Charge attack indicator"
colors={{ 'Background': '#ffb461', 'Text': '#885243' }}
/>
<ColorItem
title="Full Auto"
subtitle="Full auto mode indicator"
colors={{ 'Background': '#ffed4c', 'Text': '#a39200' }}
/>
<ColorItem
title="Auto Guard"
subtitle="Auto guard indicator"
colors={{ 'Background': '#b6b2fc', 'Text': '#4f3c79' }}
/>
</ColorPalette>
## CSS Custom Properties
All colors are available as CSS custom properties through the theme system. They automatically switch between light and dark mode values.
```css
/* Usage in components */
.my-component {
background: var(--card-bg);
color: var(--text-primary);
border: 1px solid var(--border-subtle);
}
/* Element-specific theming */
.wind-themed {
background: var(--wind-bg);
color: var(--wind-text);
}
```
### Common Variables
| Variable | Description |
|----------|-------------|
| `--background` | Page background |
| `--card-bg` | Card/panel background |
| `--text-primary` | Primary text color |
| `--text-secondary` | Secondary/muted text |
| `--border-subtle` | Subtle borders |
| `--accent-blue` | Primary accent |
| `--accent-yellow` | Highlight/selection |

View file

@ -0,0 +1,232 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Foundations/Elements" />
# Elements
Granblue Fantasy features six elemental types that form a core part of the game's mechanics. Each element has a distinct color palette used throughout Hensei for theming characters, weapons, summons, and UI components.
## Element Wheel
The six elements form a weakness/advantage cycle:
<div style={{ display: 'flex', justifyContent: 'center', margin: '32px 0' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px', maxWidth: '400px' }}>
<div style={{ padding: '16px', background: '#cdffed', borderRadius: '8px', textAlign: 'center' }}>
<div style={{ fontWeight: 600, color: '#006a45' }}>Wind</div>
</div>
<div style={{ padding: '16px', background: '#ffcdcd', borderRadius: '8px', textAlign: 'center' }}>
<div style={{ fontWeight: 600, color: '#6e0000' }}>Fire</div>
</div>
<div style={{ padding: '16px', background: '#cdedff', borderRadius: '8px', textAlign: 'center' }}>
<div style={{ fontWeight: 600, color: '#00639c' }}>Water</div>
</div>
<div style={{ padding: '16px', background: '#ffe2cd', borderRadius: '8px', textAlign: 'center' }}>
<div style={{ fontWeight: 600, color: '#8e3c0b' }}>Earth</div>
</div>
<div style={{ padding: '16px', background: '#fffacd', borderRadius: '8px', textAlign: 'center' }}>
<div style={{ fontWeight: 600, color: '#715100' }}>Light</div>
</div>
<div style={{ padding: '16px', background: '#f2cdff', borderRadius: '8px', textAlign: 'center' }}>
<div style={{ fontWeight: 600, color: '#560075' }}>Dark</div>
</div>
</div>
</div>
**Advantage cycle:**
- Wind → Earth → Water → Fire → Wind
- Light ↔ Dark (mutual advantage)
## Element Color Palettes
Each element has multiple color values for different contexts:
### Wind (Green)
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
<div style={{ width: '80px', height: '60px', background: '#006a45', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'white', fontSize: '11px' }}>Text Light</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#1dc688', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Text Dark</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#3ee489', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Background</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#cdffed', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Portrait</span>
</div>
</div>
### Fire (Red)
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
<div style={{ width: '80px', height: '60px', background: '#6e0000', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'white', fontSize: '11px' }}>Text Light</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#ec5c5c', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Text Dark</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#fa6d6d', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Background</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#ffcdcd', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Portrait</span>
</div>
</div>
### Water (Blue)
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
<div style={{ width: '80px', height: '60px', background: '#00639c', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'white', fontSize: '11px' }}>Text Light</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#5cb7ec', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Text Dark</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#6cc9ff', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Background</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#cdedff', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Portrait</span>
</div>
</div>
### Earth (Orange)
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
<div style={{ width: '80px', height: '60px', background: '#8e3c0b', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'white', fontSize: '11px' }}>Text Light</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#ec985c', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Text Dark</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#fd9f5b', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Background</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#ffe2cd', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Portrait</span>
</div>
</div>
### Light (Yellow)
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
<div style={{ width: '80px', height: '60px', background: '#715100', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'white', fontSize: '11px' }}>Text Light</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#c59c0c', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Text Dark</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#e8d633', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Background</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#fffacd', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Portrait</span>
</div>
</div>
### Dark (Purple)
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
<div style={{ width: '80px', height: '60px', background: '#560075', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'white', fontSize: '11px' }}>Text Light</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#c65cec', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Text Dark</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#de7bff', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Background</span>
</div>
<div style={{ width: '80px', height: '60px', background: '#f2cdff', borderRadius: '4px', display: 'flex', alignItems: 'end', padding: '4px' }}>
<span style={{ color: 'black', fontSize: '11px' }}>Portrait</span>
</div>
</div>
## CSS Custom Properties
Element colors are available as CSS variables that adapt to light/dark themes:
```css
/* Each element provides these variables */
--{element}-bg /* Main background color */
--{element}-bg-hover /* Hover state background */
--{element}-text /* Primary text color */
--{element}-text-hover /* Hover state text */
--{element}-portrait-bg /* Portrait/card background */
--{element}-shadow /* Box shadow color */
--{element}-accent /* Accent color */
```
### Example Usage
```css
.wind-card {
background: var(--wind-bg);
color: var(--wind-text);
box-shadow: 0 2px 8px var(--wind-shadow);
}
.wind-card:hover {
background: var(--wind-bg-hover);
color: var(--wind-text-hover);
}
```
## Element Buttons
Each element has a dedicated button color:
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', margin: '16px 0' }}>
<button style={{ padding: '8px 16px', background: '#1dc688', border: 'none', borderRadius: '6px', color: 'white', fontWeight: 500 }}>Wind</button>
<button style={{ padding: '8px 16px', background: '#ec5c5c', border: 'none', borderRadius: '6px', color: 'white', fontWeight: 500 }}>Fire</button>
<button style={{ padding: '8px 16px', background: '#5cb7ec', border: 'none', borderRadius: '6px', color: 'white', fontWeight: 500 }}>Water</button>
<button style={{ padding: '8px 16px', background: '#8e3c0b', border: 'none', borderRadius: '6px', color: 'white', fontWeight: 500 }}>Earth</button>
<button style={{ padding: '8px 16px', background: '#c59c0c', border: 'none', borderRadius: '6px', color: 'white', fontWeight: 500 }}>Light</button>
<button style={{ padding: '8px 16px', background: '#c65cec', border: 'none', borderRadius: '6px', color: 'white', fontWeight: 500 }}>Dark</button>
</div>
## Navigation Selected States
Elements have specific colors for selected navigation items:
| Element | Background | Text |
|---------|------------|------|
| Wind | `#cdffed` | `#006a45` |
| Fire | `#ffcdcd` | `#6e0000` |
| Water | `#cdedff` | `#00639c` |
| Earth | `#ffe2cd` | `#863504` |
| Light | `#fffacd` | `#715100` |
| Dark | `#f2cdff` | `#560075` |
## Focus Ring Mixins
SCSS mixins for element-specific focus rings:
```scss
@use 'themes/colors';
.wind-input:focus {
@include colors.focus-ring-wind();
}
.fire-input:focus {
@include colors.focus-ring-fire();
}
```
Each mixin applies:
- Appropriate border color
- Matching box shadow with transparency
- Removes default outline
## Components with Element Support
The following components support element theming:
- **Button** - `element` prop for colored buttons
- **SegmentedControl** - `element` prop for colored segments
- **ElementLabel** - Displays element icon with color
- **CharacterUnit** - Border color based on character element
- **WeaponUnit** - Border color based on weapon element
- **SummonUnit** - Border color based on summon element

View file

@ -0,0 +1,143 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Foundations/Spacing" />
# Spacing
Hensei uses an 8px base unit spacing system for consistent rhythm throughout the interface.
## Base Unit
```scss
$unit: 8px;
```
All spacing values are multiples of this base unit, creating visual harmony and predictable layouts.
## Spacing Scale
<div style={{ display: 'grid', gap: '16px', marginTop: '24px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ width: '120px', fontFamily: 'monospace', fontSize: '13px' }}>$unit-fourth</div>
<div style={{ width: '2px', height: '24px', background: '#275dc5' }}></div>
<span style={{ fontSize: '13px', color: '#666' }}>2px - Micro spacing</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ width: '120px', fontFamily: 'monospace', fontSize: '13px' }}>$unit-half</div>
<div style={{ width: '4px', height: '24px', background: '#275dc5' }}></div>
<span style={{ fontSize: '13px', color: '#666' }}>4px - Tight spacing</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ width: '120px', fontFamily: 'monospace', fontSize: '13px' }}>$unit</div>
<div style={{ width: '8px', height: '24px', background: '#275dc5' }}></div>
<span style={{ fontSize: '13px', color: '#666' }}>8px - Base unit</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ width: '120px', fontFamily: 'monospace', fontSize: '13px' }}>$unit-2x</div>
<div style={{ width: '16px', height: '24px', background: '#275dc5' }}></div>
<span style={{ fontSize: '13px', color: '#666' }}>16px - Default gap</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ width: '120px', fontFamily: 'monospace', fontSize: '13px' }}>$unit-3x</div>
<div style={{ width: '24px', height: '24px', background: '#275dc5' }}></div>
<span style={{ fontSize: '13px', color: '#666' }}>24px - Medium spacing</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ width: '120px', fontFamily: 'monospace', fontSize: '13px' }}>$unit-4x</div>
<div style={{ width: '32px', height: '24px', background: '#275dc5' }}></div>
<span style={{ fontSize: '13px', color: '#666' }}>32px - Section spacing</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ width: '120px', fontFamily: 'monospace', fontSize: '13px' }}>$unit-6x</div>
<div style={{ width: '48px', height: '24px', background: '#275dc5' }}></div>
<span style={{ fontSize: '13px', color: '#666' }}>48px - Large spacing</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ width: '120px', fontFamily: 'monospace', fontSize: '13px' }}>$unit-8x</div>
<div style={{ width: '64px', height: '24px', background: '#275dc5' }}></div>
<span style={{ fontSize: '13px', color: '#666' }}>64px - XL spacing</span>
</div>
</div>
## Full Scale Reference
| Token | Value | Pixels | Usage |
|-------|-------|--------|-------|
| `$unit-fourth` | $unit / 4 | 2px | Borders, micro adjustments |
| `$unit-half` | $unit / 2 | 4px | Tight gaps, small padding |
| `$unit-three-quarter` | $unit * 0.75 | 6px | Small gaps |
| `$unit` | 8px | 8px | Base unit |
| `$unit-2x` | $unit * 2 | 16px | Standard gap/padding |
| `$unit-3x` | $unit * 3 | 24px | Medium spacing |
| `$unit-4x` | $unit * 4 | 32px | Section spacing |
| `$unit-5x` | $unit * 5 | 40px | Large gaps |
| `$unit-6x` | $unit * 6 | 48px | XL gaps |
| `$unit-8x` | $unit * 8 | 64px | Page margins |
| `$unit-10x` | $unit * 10 | 80px | Hero spacing |
| `$unit-12x` | $unit * 12 | 96px | Section dividers |
## CSS Custom Properties
Semantic spacing aliases are available as CSS variables:
```css
:root {
--spacing-xs: 8px; /* $unit */
--spacing-sm: 16px; /* $unit-2x */
--spacing-md: 32px; /* $unit-4x */
--spacing-lg: 64px; /* $unit-8x */
--spacing-xl: 96px; /* $unit-12x */
}
```
## Breakpoints
Responsive breakpoints for different device sizes:
| Token | Value | Device |
|-------|-------|--------|
| `$phone` | 375px | Mobile phones |
| `$tablet` | 768px | Tablets |
| `$laptop` | 1280px | Laptops |
| `$desktop` | 1920px | Desktop monitors |
### Grid Width
The standard grid/content width:
```scss
$grid-width: 780px;
```
## Component Heights
Standard heights for grid representation components:
| Token | Value | Component |
|-------|-------|-----------|
| `$character-rep-height` | 111px | Character unit/rep |
| `$weapon-rep-height` | 109.75px | Weapon unit/rep |
| `$summon-rep-height` | 117px | Summon unit/rep |
## Usage in SCSS
```scss
@use 'themes/spacing' as spacing;
.card {
padding: spacing.$unit-2x;
margin-bottom: spacing.$unit-3x;
gap: spacing.$unit;
}
.hero-section {
padding: spacing.$unit-8x 0;
margin-bottom: spacing.$unit-6x;
}
@media (max-width: spacing.$tablet) {
.card {
padding: spacing.$unit;
}
}
```

View file

@ -0,0 +1,119 @@
import { Meta, Typeset } from '@storybook/addon-docs/blocks';
<Meta title="Foundations/Typography" />
# Typography
Hensei uses the **Goalking** variable font as its primary typeface, with system fonts as fallbacks.
## Font Family
```css
--font-family: 'Goalking', system-ui, sans-serif;
```
The Goalking font is a variable font supporting weights from 100-900, loaded from `/fonts/gk-variable.woff2`.
## Font Weights
| Weight | Value | Usage |
|--------|-------|-------|
| Normal | 400 | Body text, descriptions |
| Medium | 500 | Labels, emphasized text |
| Bold | 600 | Headings, important text |
## Font Sizes
Our font sizes use `rem` units based on a 10px base (62.5% of 16px), making calculations simple: 1rem = 10px.
<Typeset
fontSizes={[
'11px',
'13px',
'15px',
'16px',
'18px',
'21px',
'24px',
'28px',
]}
fontWeight={400}
sampleText="The quick brown fox jumps over the lazy dog"
fontFamily="'Goalking', system-ui, sans-serif"
/>
### Size Scale
| Token | Size | Pixels | Usage |
|-------|------|--------|-------|
| `$font-tiny` | 1.1rem | 11px | Badges, captions |
| `$font-small` | 1.3rem | 13px | Secondary text, metadata |
| `$font-button` | 1.5rem | 15px | Button text |
| `$font-name` | 1.5rem | 15px | Character/item names |
| `$font-regular` | 1.6rem | 16px | Body text |
| `$font-medium` | 1.8rem | 18px | Subheadings |
| `$font-large` | 2.1rem | 21px | Section headers |
| `$font-xlarge` | 2.4rem | 24px | Page titles |
| `$font-xxlarge` | 2.8rem | 28px | Hero text |
## Font Weight Examples
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', margin: '24px 0' }}>
<div style={{ fontSize: '18px', fontWeight: 400 }}>Normal (400): The quick brown fox jumps over the lazy dog</div>
<div style={{ fontSize: '18px', fontWeight: 500 }}>Medium (500): The quick brown fox jumps over the lazy dog</div>
<div style={{ fontSize: '18px', fontWeight: 600 }}>Bold (600): The quick brown fox jumps over the lazy dog</div>
</div>
## Usage in SCSS
Import the typography module to use font variables:
```scss
@use 'themes/typography' as typography;
.heading {
font-size: typography.$font-large;
font-weight: typography.$bold;
}
.body-text {
font-size: typography.$font-regular;
font-weight: typography.$normal;
}
.caption {
font-size: typography.$font-small;
font-weight: typography.$medium;
}
```
## Text Color Variables
Text colors are available as CSS custom properties and automatically adapt to the current theme:
| Variable | Description |
|----------|-------------|
| `--text-primary` | Main body text |
| `--text-secondary` | Muted/secondary text |
| `--text-tertiary` | Disabled/placeholder text |
| `--link-text-hover` | Link hover state |
```css
.primary-text {
color: var(--text-primary);
}
.muted-text {
color: var(--text-secondary);
}
```
## Font Smoothing
For optimal rendering, we apply antialiasing to all text:
```css
body {
-webkit-font-smoothing: antialiased;
}
```

View file

@ -0,0 +1,62 @@
import type { Character, GridCharacter } from '$lib/types/api/character';
/** Mock character data for Storybook stories */
export const mockCharacter: Character = {
id: 'char-1',
granblueId: '3040000000',
name: { en: 'Narmaya', ja: 'ナルメア' },
element: 5, // Dark
rarity: 3, // SSR
special: false,
uncap: { flb: true, ulb: true, transcendence: false },
proficiency: [1, 2] // Katana, Dagger
};
export const mockSpecialCharacter: Character = {
id: 'char-2',
granblueId: '3040100000',
name: { en: 'Cagliostro', ja: 'カリオストロ' },
element: 4, // Earth
rarity: 3, // SSR
special: true, // Limited
uncap: { flb: true, ulb: true, transcendence: false },
proficiency: [6] // Staff
};
export const mockGridCharacter: GridCharacter = {
id: 'grid-char-1',
position: 1,
uncapLevel: 5,
transcendenceStep: 0,
perpetuity: false,
character: mockCharacter
};
export const mockGridCharacterWithRing: GridCharacter = {
id: 'grid-char-2',
position: 2,
uncapLevel: 6,
transcendenceStep: 3,
perpetuity: true,
character: mockCharacter
};
/** Characters organized by element for element-specific stories */
export const mockCharactersByElement = {
wind: { ...mockCharacter, id: 'char-wind', element: 1, name: { en: 'Tiamat', ja: 'ティアマト' } },
fire: { ...mockCharacter, id: 'char-fire', element: 2, name: { en: 'Colossus', ja: 'コロッサス' } },
water: {
...mockCharacter,
id: 'char-water',
element: 3,
name: { en: 'Leviathan', ja: 'リヴァイアサン' }
},
earth: { ...mockCharacter, id: 'char-earth', element: 4, name: { en: 'Yggdrasil', ja: 'ユグドラシル' } },
dark: { ...mockCharacter, id: 'char-dark', element: 5, name: { en: 'Celeste', ja: 'セレスト' } },
light: {
...mockCharacter,
id: 'char-light',
element: 6,
name: { en: 'Luminiera', ja: 'シュヴァリエ' }
}
};

38
src/stories/mocks/jobs.ts Normal file
View file

@ -0,0 +1,38 @@
import type { Job } from '$lib/types/api/job';
/** Mock job data for Storybook stories */
export const mockJob: Job = {
id: 'job-1',
granblueId: '180001',
name: { en: 'Lumberjack', ja: 'ランバージャック' },
proficiency: [3], // Axe
row: 5,
ultimateMastery: true
};
export const mockJobNoUM: Job = {
id: 'job-2',
granblueId: '100001',
name: { en: 'Dark Fencer', ja: 'ダークフェンサー' },
proficiency: [1, 2], // Sword, Dagger
row: 3,
ultimateMastery: false
};
export const mockJobMultiProf: Job = {
id: 'job-3',
granblueId: '180101',
name: { en: 'Kengo', ja: 'ケンゴウ' },
proficiency: [1, 2], // Sword, Katana
row: 5,
ultimateMastery: true
};
/** Jobs organized by row/tier */
export const mockJobsByRow = {
row1: { ...mockJob, id: 'job-row1', granblueId: '100001', row: 1, ultimateMastery: false },
row2: { ...mockJob, id: 'job-row2', granblueId: '110001', row: 2, ultimateMastery: false },
row3: { ...mockJob, id: 'job-row3', granblueId: '120001', row: 3, ultimateMastery: false },
row4: { ...mockJob, id: 'job-row4', granblueId: '130001', row: 4, ultimateMastery: true },
row5: { ...mockJob, id: 'job-row5', granblueId: '180001', row: 5, ultimateMastery: true }
};

View file

@ -0,0 +1,51 @@
import type { Summon, GridSummon } from '$lib/types/api/summon';
/** Mock summon data for Storybook stories */
export const mockSummon: Summon = {
id: 'summon-1',
granblueId: '2040000000',
name: { en: 'Bahamut', ja: 'バハムート' },
element: 5, // Dark
rarity: 3, // SSR
uncap: { flb: true, ulb: true, transcendence: true }
};
export const mockGridSummon: GridSummon = {
id: 'grid-summon-1',
position: 0,
uncapLevel: 5,
transcendenceStep: 0,
main: false,
friend: false,
summon: mockSummon
};
export const mockMainSummon: GridSummon = {
id: 'grid-summon-main',
position: -1,
uncapLevel: 6,
transcendenceStep: 5,
main: true,
friend: false,
summon: mockSummon
};
export const mockFriendSummon: GridSummon = {
id: 'grid-summon-friend',
position: 6,
uncapLevel: 5,
transcendenceStep: 0,
main: false,
friend: true,
summon: mockSummon
};
/** Summons organized by element */
export const mockSummonsByElement = {
wind: { ...mockSummon, id: 'summon-wind', element: 1, name: { en: 'Tiamat', ja: 'ティアマト' } },
fire: { ...mockSummon, id: 'summon-fire', element: 2, name: { en: 'Colossus', ja: 'コロッサス' } },
water: { ...mockSummon, id: 'summon-water', element: 3, name: { en: 'Leviathan', ja: 'リヴァイアサン' } },
earth: { ...mockSummon, id: 'summon-earth', element: 4, name: { en: 'Yggdrasil', ja: 'ユグドラシル' } },
dark: { ...mockSummon, id: 'summon-dark', element: 5, name: { en: 'Celeste', ja: 'セレスト' } },
light: { ...mockSummon, id: 'summon-light', element: 6, name: { en: 'Luminiera', ja: 'シュヴァリエ' } }
};

View file

@ -0,0 +1,89 @@
import type { Weapon, GridWeapon } from '$lib/types/api/weapon';
/** Mock weapon data for Storybook stories */
export const mockWeapon: Weapon = {
id: 'weapon-1',
granblueId: '1040000000',
name: { en: 'Luminiera Sword Omega', ja: 'シュヴァリエソード・マグナ' },
element: 6, // Light
rarity: 3, // SSR
series: 1, // Omega
proficiency: 1, // Sword
uncap: { flb: true, ulb: true, transcendence: true }
};
export const mockOpusWeapon: Weapon = {
id: 'weapon-opus',
granblueId: '1040900000',
name: { en: 'Cosmic Sword', ja: 'コスミックソード' },
element: 6,
rarity: 3,
series: 2, // Opus
proficiency: 1,
uncap: { flb: true, ulb: true, transcendence: true }
};
export const mockDraconicWeapon: Weapon = {
id: 'weapon-draconic',
granblueId: '1040800000',
name: { en: 'Draconic Harp', ja: 'ドラゴニックハープ' },
element: 1, // Wind
rarity: 3,
series: 3, // Draconic
proficiency: 4, // Harp
uncap: { flb: true, ulb: true, transcendence: false }
};
export const mockGridWeapon: GridWeapon = {
id: 'grid-weapon-1',
position: 0,
uncapLevel: 5,
transcendenceStep: 0,
mainhand: false,
weapon: mockWeapon,
awakening: null,
weaponKeys: [],
ax: []
};
export const mockMainhandWeapon: GridWeapon = {
id: 'grid-weapon-main',
position: -1,
uncapLevel: 6,
transcendenceStep: 5,
mainhand: true,
weapon: mockOpusWeapon,
awakening: { id: 'awk-1', type: { slug: 'attack', name: { en: 'Attack', ja: '攻撃' } } },
weaponKeys: [],
ax: []
};
/** Weapons organized by element */
export const mockWeaponsByElement = {
wind: { ...mockWeapon, id: 'weapon-wind', element: 1, name: { en: 'Tiamat Bolt', ja: 'ティアマトボルト' } },
fire: {
...mockWeapon,
id: 'weapon-fire',
element: 2,
name: { en: 'Colossus Cane', ja: 'コロッサスケーン' }
},
water: {
...mockWeapon,
id: 'weapon-water',
element: 3,
name: { en: 'Leviathan Gaze', ja: 'リヴァイアサンゲイズ' }
},
earth: {
...mockWeapon,
id: 'weapon-earth',
element: 4,
name: { en: 'Yggdrasil Bow', ja: 'ユグドラシルボウ' }
},
dark: {
...mockWeapon,
id: 'weapon-dark',
element: 5,
name: { en: 'Celeste Claw', ja: 'セレストクロー' }
},
light: { ...mockWeapon, id: 'weapon-light', element: 6, name: { en: 'Luminiera Sword', ja: 'シュヴァリエソード' } }
};