components and storybook for development svg previews

This commit is contained in:
Henri
2026-03-05 12:27:35 +02:00
parent 567f872cc2
commit 4571d22644
12 changed files with 1730 additions and 25 deletions

3
.gitignore vendored
View File

@@ -22,3 +22,6 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
*storybook.log
storybook-static

13
.storybook/main.js Normal file
View File

@@ -0,0 +1,13 @@
/** @type { import('@storybook/svelte-vite').StorybookConfig } */
const config = {
"stories": [
"../src/**/*.stories.@(js|ts|svelte)"
],
"addons": [
"@storybook/addon-svelte-csf"
],
"framework": "@storybook/svelte-vite"
};
export default config;

13
.storybook/preview.js Normal file
View File

@@ -0,0 +1,13 @@
/** @type { import('@storybook/svelte-vite').Preview } */
const preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

1532
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,13 +6,18 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.1", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"svelte": "^5.43.8", "svelte": "^5.43.8",
"vite": "npm:rolldown-vite@7.2.5" "vite": "npm:rolldown-vite@7.2.5",
"storybook": "^10.2.15",
"@storybook/svelte-vite": "^10.2.15",
"@storybook/addon-svelte-csf": "^5.0.11"
}, },
"overrides": { "overrides": {
"vite": "npm:rolldown-vite@7.2.5" "vite": "npm:rolldown-vite@7.2.5"

View File

@@ -1,10 +1,11 @@
<!-- <!--
Multi-purpose image/SVG component with optional link wrapper, Multi-purpose image component with optional link wrapper,
hover effects, size variants, and object-fit options. hover effects, size variants, and object-fit options. Includes support for WebP format with fallback to standard formats.
--> -->
<script> <script>
export let src = ""; export let webpSrc = ""; // Optional WebP source for browsers that support it
export let src = ""; // Fallback (some devices don't support WebP) or primary source if webpSrc is not provided
export let alt = ""; export let alt = "";
export let href = null; // optional link URL export let href = null; // optional link URL
export let target = "_blank"; export let target = "_blank";
@@ -12,6 +13,12 @@ hover effects, size variants, and object-fit options.
export let objectFit = "contain"; // 'contain' | 'cover' | 'fill' | 'none' | 'scale-down' export let objectFit = "contain"; // 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'
export let hover = false; export let hover = false;
export let className = ""; export let className = "";
// optional srcset for responsive images (w descriptors means width-based, x descriptors means pixel density-based)
// e.g. "image-400.jpg 400w, image-800.jpg 800w"
export let srcSet = ""; // always set src if using this
export let webpSrcSet = ""; // always set src if using this
export let pictureClass = ""; // CSS/Tailwind classes applied to the <picture> element
$: objectFitClass = { $: objectFitClass = {
contain: "object-contain", contain: "object-contain",
@@ -25,22 +32,29 @@ hover effects, size variants, and object-fit options.
</script> </script>
{#if href} {#if href}
<a <a {href} {target} {rel} class="inline-block leading-none">
{href} <picture class={pictureClass}>
{target} {#if webpSrc || webpSrcSet}
{rel} <source srcset={webpSrcSet || webpSrc} type="image/webp" />
class="inline-block leading-none" {/if}
> <img
<img {src}
{src} srcset={srcSet || undefined}
{alt} alt={alt}
class="block h-auto max-w-full {objectFitClass} {hoverClasses} {className}" class="block h-auto max-w-full {objectFitClass} {hoverClasses} {className}"
/> />
</picture>
</a> </a>
{:else} {:else}
<img <picture class={pictureClass}>
{src} {#if webpSrc || webpSrcSet}
{alt} <source srcset={webpSrcSet || webpSrc} type="image/webp" />
class="block h-auto max-w-full {objectFitClass} {hoverClasses} {className}" {/if}
/> <img
{src}
srcset={srcSet || undefined}
alt={alt}
class="block h-auto max-w-full {objectFitClass} {hoverClasses} {className}"
/>
</picture>
{/if} {/if}

19
src/components/README.md Normal file
View File

@@ -0,0 +1,19 @@
# SVG previews
use `npm run storybook` which opens a browser window with all the SVG variations. You can also edit the SVG properties in storybook controls.
# Components
under components are building blocks that are not layout related.
## details
- Button: a simple button component with some styling.
- Dropdown: a dropdown component that can be used to show/hide content.
- Slideshow: a simple slideshow component that can be used to show multiple images in a slideshow format. Includes options to enable/disable autoplay and to set the autoplay interval.
- Svg: a component that renders different SVG icons based on the `type` prop. See the comment after the `type` prop in `Svg.svelte` for the available types.
- Image: a component that renders an image with some styling. It accepts a lot of props. Main ones are `src` (the main image source that is used if anything else fails or not provided), `alt` (the alt text for the image), `srcSet` (a string of comma-separated image sources for different screen sizes, jpg or png files), `webpSrc` (the webp image source as a webp file), `webpSrcSet` (a string of comma-separated webp image sources for different screen sizes). We use 2 webp images: 400 and 800 width, and one jpg image 800 width as default src for fallback.
# Layout components
under components/layout are components that are used to structure the page, such as Container, Stack, Center, etc.

54
src/components/Svg.svelte Normal file
View File

@@ -0,0 +1,54 @@
<script>
export let type; // "connector", "mirrorV", "mirrorH", "mirrorVH", "branch"
export let className = "";
</script>
{#if type === "connector"}
<svg viewBox="0 0 182 66" class={className} fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 63.5H67.5L102.5 15.5H156" stroke="currentColor" stroke-width="5"/>
<circle cx="166" cy="15.5" r="10" stroke="currentColor" stroke-width="5"/>
</svg>
{:else if type === "mirrorV"}
<svg viewBox="0 0 182 66" class={className} fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="scale(1,-1) translate(0,-66)">
<path d="M0 63.5H67.5L102.5 15.5H156" stroke="currentColor" stroke-width="5"/>
<circle cx="166" cy="15.5" r="10" stroke="currentColor" stroke-width="5"/>
</g>
</svg>
{:else if type === "mirrorH"}
<svg viewBox="0 0 182 66" class={className} fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="scale(-1,1) translate(-182,0)">
<path d="M0 63.5H67.5L102.5 15.5H156" stroke="currentColor" stroke-width="5"/>
<circle cx="166" cy="15.5" r="10" stroke="currentColor" stroke-width="5"/>
</g>
</svg>
{:else if type === "mirrorVH"}
<svg viewBox="0 0 182 66" class={className} fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="scale(-1,-1) translate(-182,-66)">
<path d="M0 63.5H67.5L102.5 15.5H156" stroke="currentColor" stroke-width="5"/>
<circle cx="166" cy="15.5" r="10" stroke="currentColor" stroke-width="5"/>
</g>
</svg>
{:else if type === "branch"}
<svg viewBox="0 0 75 74" class={className} fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 31.4784H46L68 1.47842M45.5 30.9784L66.5 63.9784"
stroke="currentColor"
stroke-width="5"/>
</svg>
{:else if type === "branchH"}
<svg viewBox="0 0 75 74" class={className} fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="scale(-1,1) translate(-75,0)">
<path d="M0 31.4784H46L68 1.47842M45.5 30.9784L66.5 63.9784"
stroke="currentColor"
stroke-width="5"/>
</g>
</svg>
{:else}
<div>Unknown type: {type}</div>
{/if}

View File

@@ -1,3 +1,4 @@
// layout components
export { default as Container } from './layout/Container.svelte' export { default as Container } from './layout/Container.svelte'
export { default as Card } from './layout/Card.svelte' export { default as Card } from './layout/Card.svelte'
export { default as Stack } from './layout/Stack.svelte' export { default as Stack } from './layout/Stack.svelte'
@@ -5,6 +6,9 @@ export { default as Grid } from './layout/Grid.svelte'
export { default as Center } from './layout/Center.svelte' export { default as Center } from './layout/Center.svelte'
export { default as Section } from './layout/Section.svelte' export { default as Section } from './layout/Section.svelte'
// components
export { default as Button } from './Button.svelte' export { default as Button } from './Button.svelte'
export { default as Dropdown } from './Dropdown.svelte' export { default as Dropdown } from './Dropdown.svelte'
export { default as Slideshow } from './Slideshow.svelte' export { default as Slideshow } from './Slideshow.svelte'
export { default as Svg } from './Svg.svelte'
export { default as Image } from './Image.svelte'

View File

@@ -7,6 +7,7 @@
const paddingClasses = { const paddingClasses = {
none: "py-0", none: "py-0",
tight: "py-2 md:py-3",
small: "py-4 md:py-8", small: "py-4 md:py-8",
normal: "py-12 max-md:py-8", normal: "py-12 max-md:py-8",
large: "py-[clamp(3rem,8vw,6rem)] max-md:py-12", large: "py-[clamp(3rem,8vw,6rem)] max-md:py-12",

View File

@@ -105,9 +105,9 @@
} }
</style> </style>
<Section className="text-black" background="orange" padding="small"> <Section className="text-black" background="orange" padding="tight">
{#if $text && Object.keys($text).length > 0} {#if $text && Object.keys($text).length > 0}
<div class="flex items-center w-full relative min-h-16"> <div class="flex items-center w-full relative min-h-12">
<!-- Logo --> <!-- Logo -->
{#if currentPath !== "/"} {#if currentPath !== "/"}
<div class="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 md:relative md:left-auto md:translate-x-0 md:top-auto md:translate-y-0"> <div class="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 md:relative md:left-auto md:translate-x-0 md:top-auto md:translate-y-0">

View File

@@ -0,0 +1,51 @@
import Icon from '../components/Svg.svelte';
export default {
title: 'SVG/Icon',
component: Icon,
argTypes: {
type: { control: { type: 'select', options: ['connector','mirrorV','mirrorH','mirrorVH','branch','branchH'] } },
className: { control: 'text' },
},
};
const Template = (args) => ({
Component: Icon,
props: args,
});
export const Default = Template.bind({});
Default.args = {
type: 'connector',
className: 'w-64 text-cyan-500',
};
export const MirrorV = Template.bind({});
MirrorV.args = {
type: 'mirrorV',
className: 'w-64 text-green-500',
};
export const MirrorH = Template.bind({});
MirrorH.args = {
type: 'mirrorH',
className: 'w-64 text-yellow-500',
};
export const MirrorVH = Template.bind({});
MirrorVH.args = {
type: 'mirrorVH',
className: 'w-64 text-pink-500',
};
export const Branch = Template.bind({});
Branch.args = {
type: 'branch',
className: 'w-48 text-magenta-500',
};
export const BranchH = Template.bind({});
BranchH.args = {
type: 'branchH',
className: 'w-48 text-magenta-500',
};