Compare commits
10 Commits
53b9a8bf7a
...
9268e6c3cf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9268e6c3cf | ||
|
|
0b2eae33b1 | ||
|
|
3f9f6e3f93 | ||
|
|
45e3ef77c6 | ||
|
|
ef9d97dde0 | ||
|
|
5bbc19566d | ||
|
|
fc245407a1 | ||
|
|
30f5d01370 | ||
|
|
cb3d05b094 | ||
|
|
ac2eb6ba0b |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
.yarn
|
||||||
|
|
||||||
# Output
|
# Output
|
||||||
.output
|
.output
|
||||||
@@ -22,6 +23,8 @@ Thumbs.db
|
|||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
|
# npm (use bun instead)
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
# Git worktrees
|
# Git worktrees
|
||||||
.worktrees
|
.worktrees
|
||||||
|
|
||||||
|
|||||||
@@ -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
3
.yarnrc.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Disable Plug'n'Play, use node_modules
|
||||||
|
nodeLinker: node-modules
|
||||||
|
enableGlobalCache: false
|
||||||
59
config/storybook/main.ts
Normal file
59
config/storybook/main.ts
Normal 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;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import '../../src/app/styles/app.css';
|
||||||
import type { Preview } from '@storybook/sveltekit'
|
import type { Preview } from '@storybook/sveltekit'
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expect, afterEach } from 'vitest';
|
import { afterEach } from 'vitest';
|
||||||
import { cleanup } from '@testing-library/svelte';
|
import { cleanup } from '@testing-library/svelte';
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
4370
package-lock.json
generated
4370
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "allmywork",
|
"name": "allmywork",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"packageManager": "yarn@4.11.0",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -17,43 +18,46 @@
|
|||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:watch": "vitest --watch",
|
"test:watch": "vitest --watch",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:all": "bun run test && bun run test:e2e",
|
"test:all": "yarn test && yarn test:e2e",
|
||||||
"test:coverage": "vitest --coverage",
|
"test:coverage": "vitest --coverage",
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006 --config-dir config/storybook",
|
||||||
"build-storybook": "storybook build"
|
"build-storybook": "storybook build --config-dir config/storybook"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.5",
|
"@biomejs/biome": "^2.4.5",
|
||||||
|
"@chromatic-com/storybook": "4.1.3",
|
||||||
"@playwright/test": "^1.58.2",
|
"@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/adapter-auto": "^7.0.0",
|
||||||
"@sveltejs/kit": "^2.50.2",
|
"@sveltejs/kit": "^2.50.2",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
"@tailwindcss/postcss": "^4.2.1",
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
"@testing-library/svelte": "^5.3.1",
|
"@testing-library/svelte": "^5.3.1",
|
||||||
"@types/node": "^25.3.3",
|
"@types/node": "^25.3.3",
|
||||||
|
"@vitest/browser-playwright": "^4.0.18",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
"@vitest/ui": "^4.0.18",
|
"@vitest/ui": "^4.0.18",
|
||||||
"autoprefixer": "^10.4.27",
|
"autoprefixer": "^10.4.27",
|
||||||
"jsdom": "^28.1.0",
|
"jsdom": "^28.1.0",
|
||||||
|
"playwright": "^1.58.2",
|
||||||
"postcss": "^8.5.8",
|
"postcss": "^8.5.8",
|
||||||
"svelte": "^5.51.0",
|
"storybook": "10.1.11",
|
||||||
|
"svelte": "5.46.1",
|
||||||
"svelte-adapter-bun": "^1.0.1",
|
"svelte-adapter-bun": "^1.0.1",
|
||||||
"svelte-check": "^4.4.2",
|
"svelte-check": "^4.4.2",
|
||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.2.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vitest": "^4.0.18",
|
"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"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"clsx": "^2.1.1",
|
||||||
"tailwind-merge": "^3.5.0"
|
"tailwind-merge": "^3.5.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { defineConfig, devices } from "@playwright/test";
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: "./src/tests/e2e",
|
testDir: "./src",
|
||||||
|
testMatch: "**/*.e2e.{ts,js}",
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
@@ -26,7 +27,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
webServer: {
|
webServer: {
|
||||||
command: "npm run build && npm run preview",
|
command: "bun run build && bun run preview",
|
||||||
port: 4173,
|
port: 4173,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '../../app.css';
|
import '../../app.css';
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from '$shared/assets/favicon.svg';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
55
src/shared/ui/Badge/Badge.stories.svelte
Normal file
55
src/shared/ui/Badge/Badge.stories.svelte
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<script module>
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import Badge from './Badge.svelte';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Shared/Badge',
|
||||||
|
component: Badge,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
docs: { description: { component: 'Brutalist inline label. Uppercase, 3px border, zero radius.' } },
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
variant: {
|
||||||
|
control: 'select',
|
||||||
|
options: ['default', 'primary', 'secondary', 'outline'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story name="Default">
|
||||||
|
{#snippet template(args)}
|
||||||
|
<Badge {...args}>Tag</Badge>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Primary" args={{ variant: 'primary' }}>
|
||||||
|
{#snippet template(args)}
|
||||||
|
<Badge {...args}>Primary</Badge>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Secondary" args={{ variant: 'secondary' }}>
|
||||||
|
{#snippet template(args)}
|
||||||
|
<Badge {...args}>Secondary</Badge>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Outline" args={{ variant: 'outline' }}>
|
||||||
|
{#snippet template(args)}
|
||||||
|
<Badge {...args}>Outline</Badge>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="All variants">
|
||||||
|
{#snippet template()}
|
||||||
|
<div class="flex gap-3 flex-wrap">
|
||||||
|
<Badge variant="default">Default</Badge>
|
||||||
|
<Badge variant="primary">Primary</Badge>
|
||||||
|
<Badge variant="secondary">Secondary</Badge>
|
||||||
|
<Badge variant="outline">Outline</Badge>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
37
src/shared/ui/Badge/Badge.svelte
Normal file
37
src/shared/ui/Badge/Badge.svelte
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<!--
|
||||||
|
Component: Badge
|
||||||
|
Inline label for tags and status indicators. Brutalist border, uppercase, no radius.
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$shared/lib/cn';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import type { BadgeVariant } from './types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Visual style variant @default 'default' */
|
||||||
|
variant?: BadgeVariant;
|
||||||
|
children?: Snippet;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { variant = 'default', children, class: className }: Props = $props();
|
||||||
|
|
||||||
|
const variantStyles: Record<BadgeVariant, string> = {
|
||||||
|
default: 'brutal-border bg-carbon-black text-ochre-clay',
|
||||||
|
primary: 'brutal-border bg-burnt-oxide text-ochre-clay',
|
||||||
|
secondary: 'brutal-border bg-slate-indigo text-ochre-clay',
|
||||||
|
outline: 'brutal-border bg-transparent text-carbon-black',
|
||||||
|
};
|
||||||
|
|
||||||
|
const classes = $derived(cn(
|
||||||
|
'inline-block px-3 py-1 text-xs uppercase tracking-wider',
|
||||||
|
variantStyles[variant],
|
||||||
|
className,
|
||||||
|
));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class={classes}>
|
||||||
|
{#if children}
|
||||||
|
{@render children()}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
20
src/shared/ui/Badge/Badge.test.ts
Normal file
20
src/shared/ui/Badge/Badge.test.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { render } from '@testing-library/svelte';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import Badge from './Badge.svelte';
|
||||||
|
|
||||||
|
describe('Badge', () => {
|
||||||
|
it('renders children text', () => {
|
||||||
|
const { getByText } = render(Badge, { props: { children: 'hello' } });
|
||||||
|
expect(getByText('hello')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies default variant class', () => {
|
||||||
|
const { container } = render(Badge, { props: { variant: 'default' } });
|
||||||
|
expect(container.querySelector('span')?.className).toContain('bg-carbon-black');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies primary variant class', () => {
|
||||||
|
const { container } = render(Badge, { props: { variant: 'primary' } });
|
||||||
|
expect(container.querySelector('span')?.className).toContain('bg-burnt-oxide');
|
||||||
|
});
|
||||||
|
});
|
||||||
2
src/shared/ui/Badge/index.ts
Normal file
2
src/shared/ui/Badge/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Badge } from './Badge.svelte';
|
||||||
|
export type { BadgeVariant } from './types';
|
||||||
1
src/shared/ui/Badge/types.ts
Normal file
1
src/shared/ui/Badge/types.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type BadgeVariant = 'default' | 'primary' | 'secondary' | 'outline';
|
||||||
@@ -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>
|
|
||||||
58
src/shared/ui/Button/Button.stories.svelte
Normal file
58
src/shared/ui/Button/Button.stories.svelte
Normal 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>
|
||||||
58
src/shared/ui/Button/Button.svelte
Normal file
58
src/shared/ui/Button/Button.svelte
Normal 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>
|
||||||
20
src/shared/ui/Button/Button.test.ts
Normal file
20
src/shared/ui/Button/Button.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
2
src/shared/ui/Button/index.ts
Normal file
2
src/shared/ui/Button/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Button } from './Button.svelte';
|
||||||
|
export type { ButtonVariant, ButtonSize } from './types';
|
||||||
2
src/shared/ui/Button/types.ts
Normal file
2
src/shared/ui/Button/types.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
|
||||||
|
export type ButtonSize = 'sm' | 'md' | 'lg';
|
||||||
45
src/shared/ui/Card/Card.stories.svelte
Normal file
45
src/shared/ui/Card/Card.stories.svelte
Normal 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>
|
||||||
40
src/shared/ui/Card/Card.svelte
Normal file
40
src/shared/ui/Card/Card.svelte
Normal 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>
|
||||||
2
src/shared/ui/Card/index.ts
Normal file
2
src/shared/ui/Card/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Card } from './Card.svelte';
|
||||||
|
export type { CardBackground } from './types';
|
||||||
1
src/shared/ui/Card/types.ts
Normal file
1
src/shared/ui/Card/types.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type CardBackground = 'ochre' | 'slate' | 'white';
|
||||||
@@ -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"
|
|
||||||
/>
|
|
||||||
51
src/shared/ui/Input/Input.stories.svelte
Normal file
51
src/shared/ui/Input/Input.stories.svelte
Normal 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>
|
||||||
39
src/shared/ui/Input/Input.svelte
Normal file
39
src/shared/ui/Input/Input.svelte
Normal 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>
|
||||||
38
src/shared/ui/Input/Textarea.svelte
Normal file
38
src/shared/ui/Input/Textarea.svelte
Normal 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>
|
||||||
3
src/shared/ui/Input/index.ts
Normal file
3
src/shared/ui/Input/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as Input } from './Input.svelte';
|
||||||
|
export { default as Textarea } from './Textarea.svelte';
|
||||||
|
export type { InputSize } from './types';
|
||||||
1
src/shared/ui/Input/types.ts
Normal file
1
src/shared/ui/Input/types.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type InputSize = 'sm' | 'md' | 'lg';
|
||||||
32
src/shared/ui/Section/Container.svelte
Normal file
32
src/shared/ui/Section/Container.svelte
Normal 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>
|
||||||
42
src/shared/ui/Section/Section.stories.svelte
Normal file
42
src/shared/ui/Section/Section.stories.svelte
Normal 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>
|
||||||
37
src/shared/ui/Section/Section.svelte
Normal file
37
src/shared/ui/Section/Section.svelte
Normal 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>
|
||||||
3
src/shared/ui/Section/index.ts
Normal file
3
src/shared/ui/Section/index.ts
Normal 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';
|
||||||
2
src/shared/ui/Section/types.ts
Normal file
2
src/shared/ui/Section/types.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export type SectionBackground = 'ochre' | 'slate' | 'white';
|
||||||
|
export type ContainerSize = 'default' | 'wide' | 'ultra-wide';
|
||||||
45
src/shared/ui/TechStackBrick/TechStackBrick.stories.svelte
Normal file
45
src/shared/ui/TechStackBrick/TechStackBrick.stories.svelte
Normal 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>
|
||||||
25
src/shared/ui/TechStackBrick/TechStackBrick.svelte
Normal file
25
src/shared/ui/TechStackBrick/TechStackBrick.svelte
Normal 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>
|
||||||
26
src/shared/ui/TechStackBrick/TechStackGrid.svelte
Normal file
26
src/shared/ui/TechStackBrick/TechStackGrid.svelte
Normal 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>
|
||||||
2
src/shared/ui/TechStackBrick/index.ts
Normal file
2
src/shared/ui/TechStackBrick/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as TechStackBrick } from './TechStackBrick.svelte';
|
||||||
|
export { default as TechStackGrid } from './TechStackGrid.svelte';
|
||||||
@@ -1,2 +1,6 @@
|
|||||||
export { default as Button } from './Button.svelte';
|
export * from './Badge';
|
||||||
export { default as Input } from './Input.svelte';
|
export * from './Button';
|
||||||
|
export * from './Card';
|
||||||
|
export * from './Section';
|
||||||
|
export * from './Input';
|
||||||
|
export * from './TechStackBrick';
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test('homepage loads correctly', async ({ page }) => {
|
|
||||||
await page.goto('/');
|
|
||||||
await expect(page).toHaveTitle(/allmywork/);
|
|
||||||
});
|
|
||||||
@@ -13,7 +13,7 @@ const config = {
|
|||||||
routes: "src/app/routes",
|
routes: "src/app/routes",
|
||||||
},
|
},
|
||||||
alias: {
|
alias: {
|
||||||
$lib: path.resolve("src/lib"),
|
$lib: path.resolve("src/shared/lib"),
|
||||||
$shared: path.resolve("src/shared"),
|
$shared: path.resolve("src/shared"),
|
||||||
$pages: path.resolve("src/pages"),
|
$pages: path.resolve("src/pages"),
|
||||||
$features: path.resolve("src/features"),
|
$features: path.resolve("src/features"),
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ export default defineConfig({
|
|||||||
plugins: [sveltekit()],
|
plugins: [sveltekit()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
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/*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,52 +1,63 @@
|
|||||||
import path from "node:path";
|
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 { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||||
|
import { playwright } from "@vitest/browser-playwright";
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
|
const dirname =
|
||||||
import { playwright } from '@vitest/browser-playwright';
|
typeof __dirname !== "undefined"
|
||||||
const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
|
? __dirname
|
||||||
|
: path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
|
// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [svelte({
|
plugins: [
|
||||||
hot: !process.env.VITEST
|
svelte({
|
||||||
})],
|
hot: !process.env.VITEST,
|
||||||
|
}),
|
||||||
|
],
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
setupFiles: ["./src/tests/setup.ts"],
|
setupFiles: ["./config/vitest/setup.ts"],
|
||||||
include: ["src/**/*.{test,spec}.{js,ts}"],
|
include: ["src/**/*.test.{js,ts}"],
|
||||||
exclude: ["src/tests/e2e/**/*.{test,spec}.{js,ts}"],
|
exclude: ["src/**/*.e2e.{js,ts}"],
|
||||||
projects: [{
|
projects: [
|
||||||
|
{
|
||||||
extends: true,
|
extends: true,
|
||||||
plugins: [
|
plugins: [
|
||||||
// The plugin will run tests for the stories defined in your Storybook config
|
// 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
|
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
|
||||||
storybookTest({
|
storybookTest({
|
||||||
configDir: path.join(dirname, '.storybook')
|
configDir: path.join(dirname, "config/storybook"),
|
||||||
})],
|
}),
|
||||||
|
],
|
||||||
test: {
|
test: {
|
||||||
name: 'storybook',
|
include: ["src/**/*.stories.@(js|ts|svelte)"],
|
||||||
browser: {
|
browser: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
headless: true,
|
headless: true,
|
||||||
provider: playwright({}),
|
provider: playwright({}),
|
||||||
instances: [{
|
instances: [
|
||||||
browser: 'chromium'
|
{
|
||||||
}]
|
browser: "chromium",
|
||||||
},
|
},
|
||||||
setupFiles: ['.storybook/vitest.setup.ts']
|
],
|
||||||
}
|
},
|
||||||
}]
|
setupFiles: ["./config/storybook/vitest.setup.ts"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
$lib: path.resolve("./src/lib"),
|
$lib: path.resolve("./src/shared/lib"),
|
||||||
$shared: path.resolve("./src/shared"),
|
$shared: path.resolve("./src/shared"),
|
||||||
$pages: path.resolve("./src/pages"),
|
$pages: path.resolve("./src/pages"),
|
||||||
$features: path.resolve("./src/features"),
|
$features: path.resolve("./src/features"),
|
||||||
$entities: path.resolve("./src/entities"),
|
$entities: path.resolve("./src/entities"),
|
||||||
$widgets: path.resolve("./src/widgets")
|
$widgets: path.resolve("./src/widgets"),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
1
vitest.shims.d.ts
vendored
Normal file
1
vitest.shims.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="@vitest/browser-playwright" />
|
||||||
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
|
|||||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
const dirname =
|
const _dirname =
|
||||||
typeof __dirname !== "undefined"
|
typeof __dirname !== "undefined"
|
||||||
? __dirname
|
? __dirname
|
||||||
: path.dirname(fileURLToPath(import.meta.url));
|
: path.dirname(fileURLToPath(import.meta.url));
|
||||||
@@ -17,13 +17,13 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
setupFiles: ["./src/tests/setup.ts"],
|
setupFiles: ["./config/vitest/setup.ts"],
|
||||||
include: ["src/**/*.{test,spec}.{js,ts}"],
|
include: ["src/**/*.test.{js,ts}"],
|
||||||
exclude: ["src/tests/e2e/**/*.{test,spec}.{js,ts}"],
|
exclude: ["src/**/*.e2e.{js,ts}"],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
$lib: path.resolve("./src/lib"),
|
$lib: path.resolve("./src/shared/lib"),
|
||||||
$shared: path.resolve("./src/shared"),
|
$shared: path.resolve("./src/shared"),
|
||||||
$pages: path.resolve("./src/pages"),
|
$pages: path.resolve("./src/pages"),
|
||||||
$features: path.resolve("./src/features"),
|
$features: path.resolve("./src/features"),
|
||||||
|
|||||||
Reference in New Issue
Block a user