Compare commits

...

10 Commits

Author SHA1 Message Date
Ilia Mashkov
9268e6c3cf chore(deps): remove bun.lock after switching to yarn 2026-03-07 13:35:33 +03:00
Ilia Mashkov
0b2eae33b1 chore(storybook): switch to yarn and fix Storybook compatibility
- Switch from bun to yarn with node_modules linker
- Downgrade Svelte to 5.46.1 for Storybook compatibility
- Switch Storybook framework from @storybook/sveltekit to @storybook/svelte-vite
- Downgrade Storybook packages to 10.1.11
- Add FSD path aliases to vite.config.ts
- Add Svelte plugin to Storybook viteFinal to handle .svelte files
2026-03-07 13:35:26 +03:00
Ilia Mashkov
3f9f6e3f93 refactor(config): move storybook and vitest configs to config/, import app.css in storybook preview 2026-03-07 11:16:21 +03:00
Ilia Mashkov
45e3ef77c6 refactor(fsd): consolidate src/lib into shared, move test setup to shared/config/tests 2026-03-07 10:55:57 +03:00
Ilia Mashkov
ef9d97dde0 chore(storybook): remove scaffolded example stories 2026-03-07 10:39:15 +03:00
Ilia Mashkov
5bbc19566d chore(deps): remove npm lock file and ignore it in favour of bun 2026-03-07 10:17:11 +03:00
Ilia Mashkov
fc245407a1 feat(shared/ui): add Card, Section, and TechStackBrick components 2026-03-06 23:16:34 +03:00
Ilia Mashkov
30f5d01370 feat(shared/ui): add Input and Textarea components with label and error states 2026-03-06 23:16:27 +03:00
Ilia Mashkov
cb3d05b094 feat(shared/ui): implement brutalist Button component replacing placeholder 2026-03-06 23:15:09 +03:00
Ilia Mashkov
ac2eb6ba0b feat(shared/ui): add Badge component with brutalist variants and Storybook story 2026-03-06 23:13:43 +03:00
52 changed files with 5147 additions and 4545 deletions

5
.gitignore vendored
View File

@@ -1,4 +1,5 @@
node_modules
.yarn
# Output
.output
@@ -22,6 +23,8 @@ Thumbs.db
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# npm (use bun instead)
package-lock.json
# Git worktrees
.worktrees

View File

@@ -1,17 +0,0 @@
import type { StorybookConfig } from '@storybook/sveltekit';
const config: StorybookConfig = {
"stories": [
"../src/**/*.mdx",
"../src/**/*.stories.@(js|ts|svelte)"
],
"addons": [
"@storybook/addon-svelte-csf",
"@chromatic-com/storybook",
"@storybook/addon-vitest",
"@storybook/addon-a11y",
"@storybook/addon-docs"
],
"framework": "@storybook/sveltekit"
};
export default config;

3
.yarnrc.yml Normal file
View File

@@ -0,0 +1,3 @@
# Disable Plug'n'Play, use node_modules
nodeLinker: node-modules
enableGlobalCache: false

59
config/storybook/main.ts Normal file
View File

@@ -0,0 +1,59 @@
import type { StorybookConfig } from '@storybook/svelte-vite';
import {
dirname,
resolve,
} from 'path';
import { fileURLToPath } from 'url';
import {
loadConfigFromFile,
mergeConfig,
} from 'vite';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const config: StorybookConfig = {
stories: [
'../../src/**/*.mdx',
'../../src/**/*.stories.@(js|ts|svelte)',
],
addons: [
{
name: '@storybook/addon-svelte-csf',
options: {
// Use modern template syntax for better performance
legacyTemplate: false,
},
},
'@chromatic-com/storybook',
'@storybook/addon-vitest',
'@storybook/addon-a11y',
'@storybook/addon-docs',
],
framework: '@storybook/svelte-vite',
async viteFinal(config) {
// This attempts to find your actual vite.config.ts
const { config: userConfig } = await loadConfigFromFile(
{ command: 'serve', mode: 'development' },
resolve(__dirname, '../../vite.config.ts'),
) || {};
const mergedConfig = mergeConfig(config, {
// Merge resolve/alias parts to maintain path aliases
resolve: userConfig?.resolve || {},
});
const { svelte } = await import('@sveltejs/vite-plugin-svelte');
mergedConfig.plugins = mergedConfig.plugins || [];
// Add Svelte plugin to process @storybook's .svelte files
// This prevents Vite from trying to parse them before compilation
mergedConfig.plugins.unshift(svelte({
include: [/node_modules\/@storybook\/.+\.svelte$/],
}));
return mergedConfig;
},
};
export default config;

View File

@@ -1,3 +1,4 @@
import '../../src/app/styles/app.css';
import type { Preview } from '@storybook/sveltekit'
const preview: Preview = {

View File

@@ -1,4 +1,4 @@
import { expect, afterEach } from 'vitest';
import { afterEach } from 'vitest';
import { cleanup } from '@testing-library/svelte';
afterEach(() => {

4370
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
{
"name": "allmywork",
"private": true,
"packageManager": "yarn@4.11.0",
"version": "0.0.1",
"type": "module",
"scripts": {
@@ -17,43 +18,46 @@
"test": "vitest",
"test:watch": "vitest --watch",
"test:e2e": "playwright test",
"test:all": "bun run test && bun run test:e2e",
"test:all": "yarn test && yarn test:e2e",
"test:coverage": "vitest --coverage",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
"storybook": "storybook dev -p 6006 --config-dir config/storybook",
"build-storybook": "storybook build --config-dir config/storybook"
},
"devDependencies": {
"@biomejs/biome": "^2.4.5",
"@chromatic-com/storybook": "4.1.3",
"@playwright/test": "^1.58.2",
"@storybook/addon-a11y": "10.1.11",
"@storybook/addon-docs": "10.1.11",
"@storybook/addon-svelte-csf": "5.0.10",
"@storybook/addon-vitest": "10.1.11",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/postcss": "^4.2.1",
"@testing-library/svelte": "^5.3.1",
"@types/node": "^25.3.3",
"@vitest/browser-playwright": "^4.0.18",
"@vitest/coverage-v8": "^4.0.18",
"@vitest/ui": "^4.0.18",
"autoprefixer": "^10.4.27",
"jsdom": "^28.1.0",
"playwright": "^1.58.2",
"postcss": "^8.5.8",
"svelte": "^5.51.0",
"storybook": "10.1.11",
"svelte": "5.46.1",
"svelte-adapter-bun": "^1.0.1",
"svelte-check": "^4.4.2",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.18",
"storybook": "^10.2.16",
"@storybook/sveltekit": "^10.2.16",
"@storybook/addon-svelte-csf": "^5.0.11",
"@chromatic-com/storybook": "^5.0.1",
"@storybook/addon-vitest": "^10.2.16",
"@storybook/addon-a11y": "^10.2.16",
"@storybook/addon-docs": "^10.2.16",
"playwright": "^1.58.2",
"@vitest/browser-playwright": "^4.0.18"
"vitest": "^4.0.18"
},
"dependencies": {
"@storybook/builder-vite": "10.1.11",
"@storybook/csf-plugin": "10.1.11",
"@storybook/svelte": "10.1.11",
"@storybook/svelte-vite": "10.1.11",
"clsx": "^2.1.1",
"tailwind-merge": "^3.5.0"
}

View File

@@ -1,7 +1,8 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./src/tests/e2e",
testDir: "./src",
testMatch: "**/*.e2e.{ts,js}",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
@@ -26,7 +27,7 @@ export default defineConfig({
},
],
webServer: {
command: "npm run build && npm run preview",
command: "bun run build && bun run preview",
port: 4173,
},
});

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import '../../app.css';
import favicon from '$lib/assets/favicon.svg';
import favicon from '$shared/assets/favicon.svg';
let { children } = $props();
</script>

View File

@@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,40 +0,0 @@
<script lang="ts">
interface Props {
type?: 'button' | 'submit' | 'reset';
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onclick?: () => void;
children: any;
}
let {
type = 'button',
variant = 'primary',
size = 'md',
disabled = false,
onclick,
children
}: Props = $props();
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
danger: 'bg-red-600 text-white hover:bg-red-700'
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg'
};
</script>
<button
{type}
{disabled}
onclick={onclick}
class="rounded-md font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed {variants[variant]} {sizes[size]}"
>
{@render children()}
</button>

View File

@@ -0,0 +1,58 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import Button from './Button.svelte';
const { Story } = defineMeta({
title: 'Shared/Button',
component: Button,
tags: ['autodocs'],
parameters: {
layout: 'centered',
docs: { description: { component: 'Brutalist CTA button. Hard shadow, uppercase, hover translates 2px.' } },
},
argTypes: {
variant: { control: 'select', options: ['primary', 'secondary', 'outline', 'ghost'] },
size: { control: 'select', options: ['sm', 'md', 'lg'] },
},
});
</script>
<Story name="Primary" args={{ variant: 'primary', size: 'md' }}>
{#snippet template(args)}
<Button {...args}>Primary</Button>
{/snippet}
</Story>
<Story name="Secondary" args={{ variant: 'secondary', size: 'md' }}>
{#snippet template(args)}
<Button {...args}>Secondary</Button>
{/snippet}
</Story>
<Story name="Outline" args={{ variant: 'outline', size: 'md' }}>
{#snippet template(args)}
<Button {...args}>Outline</Button>
{/snippet}
</Story>
<Story name="Ghost" args={{ variant: 'ghost', size: 'md' }}>
{#snippet template(args)}
<Button {...args}>Ghost</Button>
{/snippet}
</Story>
<Story name="All sizes">
{#snippet template()}
<div class="flex gap-4 items-center">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</div>
{/snippet}
</Story>
<Story name="Disabled">
{#snippet template()}
<Button disabled>Disabled</Button>
{/snippet}
</Story>

View File

@@ -0,0 +1,58 @@
<!--
Component: Button
Brutalist button. 3px border, hard shadow on hover, uppercase, zero radius.
Hover: translates 2px down-right with reduced shadow for press feel.
-->
<script lang="ts">
import { cn } from '$shared/lib/cn';
import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements';
import type { ButtonSize, ButtonVariant } from './types';
interface Props extends HTMLButtonAttributes {
/** Visual style @default 'primary' */
variant?: ButtonVariant;
/** Size preset @default 'md' */
size?: ButtonSize;
children?: Snippet;
class?: string;
}
let {
variant = 'primary',
size = 'md',
children,
class: className,
type = 'button',
disabled,
...rest
}: Props = $props();
const base =
'brutal-border transition-all duration-200 ' +
'hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[6px_6px_0_var(--carbon-black)] ' +
'active:translate-x-[4px] active:translate-y-[4px] active:shadow-[4px_4px_0_var(--carbon-black)] ' +
'uppercase tracking-wider inline-flex items-center justify-center ' +
'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none';
const variantStyles: Record<ButtonVariant, string> = {
primary: 'brutal-shadow bg-burnt-oxide text-ochre-clay',
secondary: 'brutal-shadow bg-slate-indigo text-ochre-clay',
outline: 'brutal-shadow bg-transparent text-carbon-black',
ghost: 'bg-ochre-clay text-carbon-black shadow-none hover:shadow-[6px_6px_0_var(--carbon-black)]',
};
const sizeStyles: Record<ButtonSize, string> = {
sm: 'px-4 py-2 text-sm',
md: 'px-6 py-3 text-base',
lg: 'px-8 py-4 text-lg',
};
const classes = $derived(cn(base, variantStyles[variant], sizeStyles[size], className));
</script>
<button {type} {disabled} class={classes} {...rest}>
{#if children}
{@render children()}
{/if}
</button>

View File

@@ -0,0 +1,20 @@
import { render } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';
import Button from './Button.svelte';
describe('Button', () => {
it('renders children', () => {
const { getByText } = render(Button, { props: { children: () => 'Click me' } });
expect(getByText('Click me')).toBeTruthy();
});
it('is disabled when disabled prop is true', () => {
const { container } = render(Button, { props: { disabled: true } });
expect(container.querySelector('button')?.disabled).toBe(true);
});
it('applies primary variant styles', () => {
const { container } = render(Button, { props: { variant: 'primary' } });
expect(container.querySelector('button')?.className).toContain('bg-burnt-oxide');
});
});

View File

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

View File

@@ -0,0 +1,2 @@
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
export type ButtonSize = 'sm' | 'md' | 'lg';

View File

@@ -0,0 +1,45 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import Card from './Card.svelte';
const { Story } = defineMeta({
title: 'Shared/Card',
component: Card,
tags: ['autodocs'],
parameters: {
layout: 'padded',
docs: { description: { component: 'Brutalist card. 3px border, hard shadow, three background presets.' } },
},
argTypes: {
background: { control: 'select', options: ['ochre', 'slate', 'white'] },
noPadding: { control: 'boolean' },
},
});
</script>
<Story name="Ochre (default)" args={{ background: 'ochre' }}>
{#snippet template(args)}
<Card {...args}>Card content goes here</Card>
{/snippet}
</Story>
<Story name="Slate" args={{ background: 'slate' }}>
{#snippet template(args)}
<Card {...args}>Card content goes here</Card>
{/snippet}
</Story>
<Story name="White" args={{ background: 'white' }}>
{#snippet template(args)}
<Card {...args}>Card content goes here</Card>
{/snippet}
</Story>
<Story name="No Padding" args={{ noPadding: true }}>
{#snippet template(args)}
<Card {...args}>
<div class="p-4 brutal-border-bottom">Header</div>
<div class="p-4">Body</div>
</Card>
{/snippet}
</Story>

View File

@@ -0,0 +1,40 @@
<!--
Component: Card
Brutalist content container. 3px border, 8px hard shadow, zero radius.
Backgrounds: ochre (default), slate (dark), white.
-->
<script lang="ts">
import { cn } from '$shared/lib/cn';
import type { Snippet } from 'svelte';
import type { CardBackground } from './types';
interface Props {
/** Background color preset @default 'ochre' */
background?: CardBackground;
/** Remove default padding @default false */
noPadding?: boolean;
children?: Snippet;
class?: string;
}
let { background = 'ochre', noPadding = false, children, class: className }: Props = $props();
const backgroundStyles: Record<CardBackground, string> = {
ochre: 'bg-ochre-clay text-carbon-black',
slate: 'bg-slate-indigo text-ochre-clay',
white: 'bg-white text-carbon-black',
};
const classes = $derived(cn(
'brutal-border brutal-shadow',
backgroundStyles[background],
!noPadding && 'p-6 md:p-8',
className,
));
</script>
<div class={classes}>
{#if children}
{@render children()}
{/if}
</div>

View File

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

View File

@@ -0,0 +1 @@
export type CardBackground = 'ochre' | 'slate' | 'white';

View File

@@ -1,26 +0,0 @@
<script lang="ts">
interface Props {
type?: 'text' | 'email' | 'password' | 'number';
placeholder?: string;
disabled?: boolean;
value?: string;
oninput?: (e: Event) => void;
}
let {
type = 'text',
placeholder = '',
disabled = false,
value = '',
oninput
}: Props = $props();
</script>
<input
{type}
{placeholder}
{disabled}
{value}
oninput={oninput}
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
/>

View File

@@ -0,0 +1,51 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import Input from './Input.svelte';
import Textarea from './Textarea.svelte';
const { Story } = defineMeta({
title: 'Shared/Input',
component: Input,
tags: ['autodocs'],
parameters: {
layout: 'padded',
docs: { description: { component: 'Brutalist text input with optional label and error state.' } },
},
});
</script>
<Story name="Default">
{#snippet template()}
<Input placeholder="Enter text..." />
{/snippet}
</Story>
<Story name="With Label">
{#snippet template()}
<Input label="Email address" type="email" placeholder="you@example.com" />
{/snippet}
</Story>
<Story name="With Error">
{#snippet template()}
<Input label="Email address" type="email" value="bad-email" error="Please enter a valid email address." />
{/snippet}
</Story>
<Story name="Disabled">
{#snippet template()}
<Input label="Read-only field" disabled value="Cannot edit this" />
{/snippet}
</Story>
<Story name="Textarea">
{#snippet template()}
<Textarea label="Message" placeholder="Write your message..." rows={5} />
{/snippet}
</Story>
<Story name="Textarea with Error">
{#snippet template()}
<Textarea label="Message" error="Message is required." />
{/snippet}
</Story>

View File

@@ -0,0 +1,39 @@
<!--
Component: Input
Brutalist text input. 3px border, white background, burnt-oxide focus ring.
Optional label (uppercase) and inline error message.
-->
<script lang="ts">
import { cn } from '$shared/lib/cn';
import type { HTMLInputAttributes } from 'svelte/elements';
interface Props extends HTMLInputAttributes {
/** Uppercase label rendered above the input */
label?: string;
/** Inline error message rendered below the input */
error?: string;
class?: string;
}
let { label, error, class: className, ...rest }: Props = $props();
const inputClasses = $derived(cn(
'brutal-border bg-white px-4 py-3 text-carbon-black w-full',
'focus:outline-none focus:ring-2 focus:ring-burnt-oxide focus:ring-offset-2 focus:ring-offset-ochre-clay',
'transition-all duration-150',
'disabled:opacity-50 disabled:cursor-not-allowed',
className,
));
</script>
<div class="flex flex-col gap-2">
{#if label}
<label class="text-carbon-black">{label}</label>
{/if}
<input class={inputClasses} {...rest} />
{#if error}
<span class="text-sm text-burnt-oxide">{error}</span>
{/if}
</div>

View File

@@ -0,0 +1,38 @@
<!--
Component: Textarea
Brutalist multi-line input. Same styling as Input but <textarea>.
Non-resizable by default (matches prototype).
-->
<script lang="ts">
import { cn } from '$shared/lib/cn';
import type { HTMLTextareaAttributes } from 'svelte/elements';
interface Props extends HTMLTextareaAttributes {
label?: string;
error?: string;
rows?: number;
class?: string;
}
let { label, error, rows = 4, class: className, ...rest }: Props = $props();
const textareaClasses = $derived(cn(
'brutal-border bg-white px-4 py-3 text-carbon-black w-full resize-none',
'focus:outline-none focus:ring-2 focus:ring-burnt-oxide focus:ring-offset-2 focus:ring-offset-ochre-clay',
'transition-all duration-150',
'disabled:opacity-50 disabled:cursor-not-allowed',
className,
));
</script>
<div class="flex flex-col gap-2">
{#if label}
<label class="text-carbon-black">{label}</label>
{/if}
<textarea {rows} class={textareaClasses} {...rest}></textarea>
{#if error}
<span class="text-sm text-burnt-oxide">{error}</span>
{/if}
</div>

View File

@@ -0,0 +1,3 @@
export { default as Input } from './Input.svelte';
export { default as Textarea } from './Textarea.svelte';
export type { InputSize } from './types';

View File

@@ -0,0 +1 @@
export type InputSize = 'sm' | 'md' | 'lg';

View File

@@ -0,0 +1,32 @@
<!--
Component: Container
Centered max-width wrapper. Three size presets for responsive layouts up to 4K.
-->
<script lang="ts">
import { cn } from '$shared/lib/cn';
import type { Snippet } from 'svelte';
import type { ContainerSize } from './types';
interface Props {
/** Max-width preset @default 'default' */
size?: ContainerSize;
children?: Snippet;
class?: string;
}
let { size = 'default', children, class: className }: Props = $props();
const sizeStyles: Record<ContainerSize, string> = {
'default': 'max-w-7xl',
'wide': 'max-w-[1920px]',
'ultra-wide': 'max-w-[2560px]',
};
const classes = $derived(cn('mx-auto px-6 md:px-12 lg:px-16', sizeStyles[size], className));
</script>
<div class={classes}>
{#if children}
{@render children()}
{/if}
</div>

View File

@@ -0,0 +1,42 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import Container from './Container.svelte';
import Section from './Section.svelte';
const { Story } = defineMeta({
title: 'Shared/Section',
component: Section,
tags: ['autodocs'],
parameters: { layout: 'fullscreen' },
});
</script>
<Story name="Ochre (default)">
{#snippet template()}
<Section>
<Container>
<p class="py-16">Section content</p>
</Container>
</Section>
{/snippet}
</Story>
<Story name="Slate">
{#snippet template()}
<Section background="slate">
<Container>
<p class="py-16">Section content on slate</p>
</Container>
</Section>
{/snippet}
</Story>
<Story name="Bordered">
{#snippet template()}
<Section bordered>
<Container>
<p class="py-16">Section with brutal top and bottom borders</p>
</Container>
</Section>
{/snippet}
</Story>

View File

@@ -0,0 +1,37 @@
<!--
Component: Section
Full-width page section. Controls background and optional top/bottom brutal borders.
-->
<script lang="ts">
import { cn } from '$shared/lib/cn';
import type { Snippet } from 'svelte';
import type { SectionBackground } from './types';
interface Props {
background?: SectionBackground;
/** Add brutal border on top and bottom @default false */
bordered?: boolean;
children?: Snippet;
class?: string;
}
let { background = 'ochre', bordered = false, children, class: className }: Props = $props();
const backgroundStyles: Record<SectionBackground, string> = {
ochre: 'bg-ochre-clay text-carbon-black',
slate: 'bg-slate-indigo text-ochre-clay',
white: 'bg-white text-carbon-black',
};
const classes = $derived(cn(
backgroundStyles[background],
bordered && 'brutal-border-top brutal-border-bottom',
className,
));
</script>
<section class={classes}>
{#if children}
{@render children()}
{/if}
</section>

View File

@@ -0,0 +1,3 @@
export { default as Section } from './Section.svelte';
export { default as Container } from './Container.svelte';
export type { SectionBackground, ContainerSize } from './types';

View File

@@ -0,0 +1,2 @@
export type SectionBackground = 'ochre' | 'slate' | 'white';
export type ContainerSize = 'default' | 'wide' | 'ultra-wide';

View File

@@ -0,0 +1,45 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import TechStackBrick from './TechStackBrick.svelte';
import TechStackGrid from './TechStackGrid.svelte';
const { Story } = defineMeta({
title: 'Shared/TechStackBrick',
component: TechStackBrick,
tags: ['autodocs'],
parameters: {
layout: 'padded',
docs: { description: { component: 'Brutalist skill pills. Collapse shadow on hover for pressed feel.' } },
},
});
</script>
<Story name="Single brick">
{#snippet template()}
<TechStackBrick name="SvelteKit" />
{/snippet}
</Story>
<Story name="Grid of skills">
{#snippet template()}
<TechStackGrid
skills={[
"Svelte", "SvelteKit", "TypeScript",
"Tailwind CSS", "Bun", "Docker"
]}
/>
{/snippet}
</Story>
<Story name="Large grid">
{#snippet template()}
<TechStackGrid
skills={[
"Svelte", "SvelteKit", "TypeScript",
"Tailwind CSS", "Bun", "Docker",
"Vite", "Vitest", "Playwright",
"Biome", "Git", "Linux"
]}
/>
{/snippet}
</Story>

View File

@@ -0,0 +1,25 @@
<!--
Component: TechStackBrick
Single skill pill. Brutal border, white bg, hard shadow that collapses on hover (lifted press feel).
-->
<script lang="ts">
import { cn } from '$shared/lib/cn';
interface Props {
name: string;
class?: string;
}
let { name, class: className }: Props = $props();
const classes = $derived(cn(
'brutal-border brutal-shadow bg-white px-4 py-3 text-center',
'transition-all duration-200',
'hover:shadow-none hover:translate-x-[2px] hover:translate-y-[2px]',
className,
));
</script>
<div class={classes}>
<span class="text-sm uppercase tracking-wide">{name}</span>
</div>

View File

@@ -0,0 +1,26 @@
<!--
Component: TechStackGrid
Responsive grid of TechStackBrick items. Adjusts columns from 2 to 6 across breakpoints.
-->
<script lang="ts">
import { cn } from '$shared/lib/cn';
import TechStackBrick from './TechStackBrick.svelte';
interface Props {
skills: string[];
class?: string;
}
let { skills, class: className }: Props = $props();
const classes = $derived(cn(
'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4',
className,
));
</script>
<div class={classes}>
{#each skills as skill (skill)}
<TechStackBrick name={skill} />
{/each}
</div>

View File

@@ -0,0 +1,2 @@
export { default as TechStackBrick } from './TechStackBrick.svelte';
export { default as TechStackGrid } from './TechStackGrid.svelte';

View File

@@ -1,2 +1,6 @@
export { default as Button } from './Button.svelte';
export { default as Input } from './Input.svelte';
export * from './Badge';
export * from './Button';
export * from './Card';
export * from './Section';
export * from './Input';
export * from './TechStackBrick';

View File

@@ -1,11 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('Basic tests', () => {
it('should add numbers correctly', () => {
expect(1 + 1).toBe(2);
});
it('should subtract numbers correctly', () => {
expect(5 - 3).toBe(2);
});
});

View File

@@ -1,6 +0,0 @@
import { test, expect } from '@playwright/test';
test('homepage loads correctly', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/allmywork/);
});

View File

@@ -13,7 +13,7 @@ const config = {
routes: "src/app/routes",
},
alias: {
$lib: path.resolve("src/lib"),
$lib: path.resolve("src/shared/lib"),
$shared: path.resolve("src/shared"),
$pages: path.resolve("src/pages"),
$features: path.resolve("src/features"),

View File

@@ -6,7 +6,13 @@ export default defineConfig({
plugins: [sveltekit()],
resolve: {
alias: {
$lib: path.resolve("./src/lib"),
$lib: path.resolve("./src/shared/lib"),
$shared: path.resolve("./src/shared"),
$pages: path.resolve("./src/pages"),
$features: path.resolve("./src/features"),
$entities: path.resolve("./src/entities"),
$widgets: path.resolve("./src/widgets"),
"$/*": "./src/*",
},
},
});

View File

@@ -1,52 +1,63 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { playwright } from "@vitest/browser-playwright";
import { defineConfig } from "vitest/config";
import { fileURLToPath } from 'node:url';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import { playwright } from '@vitest/browser-playwright';
const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
const dirname =
typeof __dirname !== "undefined"
? __dirname
: path.dirname(fileURLToPath(import.meta.url));
// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
export default defineConfig({
plugins: [svelte({
hot: !process.env.VITEST
})],
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./src/tests/setup.ts"],
include: ["src/**/*.{test,spec}.{js,ts}"],
exclude: ["src/tests/e2e/**/*.{test,spec}.{js,ts}"],
projects: [{
extends: true,
plugins: [
// The plugin will run tests for the stories defined in your Storybook config
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
storybookTest({
configDir: path.join(dirname, '.storybook')
})],
test: {
name: 'storybook',
browser: {
enabled: true,
headless: true,
provider: playwright({}),
instances: [{
browser: 'chromium'
}]
},
setupFiles: ['.storybook/vitest.setup.ts']
}
}]
},
resolve: {
alias: {
$lib: path.resolve("./src/lib"),
$shared: path.resolve("./src/shared"),
$pages: path.resolve("./src/pages"),
$features: path.resolve("./src/features"),
$entities: path.resolve("./src/entities"),
$widgets: path.resolve("./src/widgets")
}
}
});
plugins: [
svelte({
hot: !process.env.VITEST,
}),
],
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./config/vitest/setup.ts"],
include: ["src/**/*.test.{js,ts}"],
exclude: ["src/**/*.e2e.{js,ts}"],
projects: [
{
extends: true,
plugins: [
// The plugin will run tests for the stories defined in your Storybook config
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
storybookTest({
configDir: path.join(dirname, "config/storybook"),
}),
],
test: {
include: ["src/**/*.stories.@(js|ts|svelte)"],
browser: {
enabled: true,
headless: true,
provider: playwright({}),
instances: [
{
browser: "chromium",
},
],
},
setupFiles: ["./config/storybook/vitest.setup.ts"],
},
},
],
},
resolve: {
alias: {
$lib: path.resolve("./src/shared/lib"),
$shared: path.resolve("./src/shared"),
$pages: path.resolve("./src/pages"),
$features: path.resolve("./src/features"),
$entities: path.resolve("./src/entities"),
$widgets: path.resolve("./src/widgets"),
},
},
});

1
vitest.shims.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="@vitest/browser-playwright" />

View File

@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { defineConfig } from "vitest/config";
const dirname =
const _dirname =
typeof __dirname !== "undefined"
? __dirname
: path.dirname(fileURLToPath(import.meta.url));
@@ -17,13 +17,13 @@ export default defineConfig({
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./src/tests/setup.ts"],
include: ["src/**/*.{test,spec}.{js,ts}"],
exclude: ["src/tests/e2e/**/*.{test,spec}.{js,ts}"],
setupFiles: ["./config/vitest/setup.ts"],
include: ["src/**/*.test.{js,ts}"],
exclude: ["src/**/*.e2e.{js,ts}"],
},
resolve: {
alias: {
$lib: path.resolve("./src/lib"),
$lib: path.resolve("./src/shared/lib"),
$shared: path.resolve("./src/shared"),
$pages: path.resolve("./src/pages"),
$features: path.resolve("./src/features"),

4291
yarn.lock Normal file

File diff suppressed because it is too large Load Diff