Initial commit: SvelteKit 3D printer housing documentation site

Set up SvelteKit project with TypeScript, i18n support (English/German),
dark/light theme toggle, and component-based documentation structure.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-25 22:53:38 +01:00
commit 676b75e771
28 changed files with 4402 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
node_modules/
.svelte-kit/
build/
.env
.env.*
!.env.example
.DS_Store

2124
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "druckwerk",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"svelte": "^5.16.0",
"typescript": "^5.7.2",
"vite": "^6.0.7",
"@tailwindcss/vite": "^4.0.0",
"tailwindcss": "^4.0.0",
"lucide-svelte": "^0.469.0"
},
"type": "module"
}

94
src/app.css Normal file
View File

@@ -0,0 +1,94 @@
@import "tailwindcss";
@theme {
/* Wire colors from the guide */
--color-wire-brown: #8B4513;
--color-wire-blue: #1E90FF;
--color-wire-pe: #9ACD32;
--color-wire-red: #DC2626;
--color-wire-orange: #F97316;
--color-wire-black: #1F2937;
/* UI colors */
--color-primary: #1e293b;
--color-primary-light: #334155;
--color-accent: #f97316;
--color-accent-hover: #ea580c;
--color-warning: #EAB308;
--color-danger: #DC2626;
--color-success: #22c55e;
/* Dark mode backgrounds */
--color-bg-dark: #0f172a;
--color-bg-dark-secondary: #1e293b;
--color-bg-dark-tertiary: #334155;
/* Light mode backgrounds */
--color-bg-light: #f8fafc;
--color-bg-light-secondary: #f1f5f9;
--color-bg-light-tertiary: #e2e8f0;
/* Text colors */
--color-text-dark: #f8fafc;
--color-text-dark-muted: #94a3b8;
--color-text-light: #0f172a;
--color-text-light-muted: #64748b;
}
/* Base styles */
html {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* Dark mode by default, light mode when .light class is on html */
html {
background-color: var(--color-bg-dark);
color: var(--color-text-dark);
}
html.light {
background-color: var(--color-bg-light);
color: var(--color-text-light);
}
/* Smooth transitions for theme changes */
html {
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-bg-dark-secondary);
}
html.light ::-webkit-scrollbar-track {
background: var(--color-bg-light-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--color-primary-light);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-accent);
}
/* Wire color utility classes */
.wire-l { color: var(--color-wire-brown); }
.wire-n { color: var(--color-wire-blue); }
.wire-pe { color: var(--color-wire-pe); }
.wire-12v { color: var(--color-wire-red); }
.wire-5v { color: var(--color-wire-orange); }
.wire-gnd { color: var(--color-wire-black); }
.bg-wire-l { background-color: var(--color-wire-brown); }
.bg-wire-n { background-color: var(--color-wire-blue); }
.bg-wire-pe { background-color: var(--color-wire-pe); }
.bg-wire-12v { background-color: var(--color-wire-red); }
.bg-wire-5v { background-color: var(--color-wire-orange); }
.bg-wire-gnd { background-color: var(--color-wire-black); }

13
src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
/// <reference types="@sveltejs/kit" />
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

13
src/app.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="de" class="scroll-smooth">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="DruckWerk - Professionelles 3D-Drucker Gehäuse mit ESP32 Steuerung und Home Assistant Integration" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1,104 @@
<script lang="ts">
import { browser } from '$app/environment';
import { Check } from 'lucide-svelte';
interface ChecklistSection {
title: string;
items: string[];
}
interface Props {
sections: ChecklistSection[];
storageKey?: string;
}
let { sections, storageKey = 'druckwerk-checklist' }: Props = $props();
// Load checked items from localStorage
function loadChecked(): Set<string> {
if (!browser) return new Set();
try {
const stored = localStorage.getItem(storageKey);
return stored ? new Set(JSON.parse(stored)) : new Set();
} catch {
return new Set();
}
}
let checkedItems = $state(loadChecked());
function toggleItem(item: string) {
if (checkedItems.has(item)) {
checkedItems.delete(item);
} else {
checkedItems.add(item);
}
checkedItems = new Set(checkedItems); // Trigger reactivity
if (browser) {
localStorage.setItem(storageKey, JSON.stringify([...checkedItems]));
}
}
function isChecked(item: string): boolean {
return checkedItems.has(item);
}
// Calculate progress
const totalItems = $derived(sections.reduce((acc, s) => acc + s.items.length, 0));
const checkedCount = $derived(checkedItems.size);
const progress = $derived(totalItems > 0 ? (checkedCount / totalItems) * 100 : 0);
</script>
<div class="space-y-6">
<!-- Progress bar -->
<div class="rounded-lg p-4 bg-bg-dark-secondary [.light_&]:bg-bg-light-secondary">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-text-dark [.light_&]:text-text-light">
{checkedCount} / {totalItems}
</span>
<span class="text-sm text-text-dark-muted [.light_&]:text-text-light-muted">
{Math.round(progress)}%
</span>
</div>
<div class="h-2 rounded-full bg-bg-dark-tertiary [.light_&]:bg-bg-light-tertiary overflow-hidden">
<div
class="h-full rounded-full bg-accent transition-all duration-300"
style="width: {progress}%"
></div>
</div>
</div>
<!-- Checklist sections -->
{#each sections as section}
<div class="rounded-lg border p-4
border-bg-dark-tertiary [.light_&]:border-bg-light-tertiary">
<h4 class="font-semibold mb-4 text-text-dark [.light_&]:text-text-light">
{section.title}
</h4>
<ul class="space-y-2">
{#each section.items as item}
<li>
<button
onclick={() => toggleItem(item)}
class="w-full flex items-center gap-3 p-2 rounded-lg text-left transition-colors
hover:bg-bg-dark-secondary [.light_&]:hover:bg-bg-light-secondary"
>
<span class="flex-shrink-0 w-5 h-5 rounded border-2 flex items-center justify-center transition-colors
{isChecked(item)
? 'bg-success border-success text-white'
: 'border-bg-dark-tertiary [.light_&]:border-bg-light-tertiary'
}">
{#if isChecked(item)}
<Check class="w-3 h-3" />
{/if}
</span>
<span class="text-sm {isChecked(item) ? 'line-through text-text-dark-muted [.light_&]:text-text-light-muted' : 'text-text-dark [.light_&]:text-text-light'}">
{item}
</span>
</button>
</li>
{/each}
</ul>
</div>
{/each}
</div>

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import { FileCode, Clock } from 'lucide-svelte';
import { t } from '$lib/stores/language';
interface Props {
filename: string;
title: string;
placeholder?: string;
}
let { filename, title, placeholder }: Props = $props();
</script>
<div class="rounded-lg border-2 border-dashed p-6
border-accent/40 bg-accent/5">
<div class="flex items-center gap-2 mb-4">
<FileCode class="w-5 h-5 text-accent" />
<span class="font-mono text-sm text-accent">{filename}</span>
</div>
<h4 class="font-semibold mb-3 text-text-dark [.light_&]:text-text-light">
{title}
</h4>
<div class="rounded-lg p-4 font-mono text-sm
bg-bg-dark-secondary [.light_&]:bg-bg-light-secondary">
<code class="text-text-dark-muted [.light_&]:text-text-light-muted">
# {placeholder || $t.config.esphome.placeholder}
</code>
</div>
<div class="mt-4 flex items-center gap-2 text-sm text-text-dark-muted [.light_&]:text-text-light-muted">
<Clock class="w-4 h-4" />
<span>{$t.config.pending}</span>
</div>
</div>

View File

@@ -0,0 +1,114 @@
<script lang="ts">
import { theme } from '$lib/stores/theme';
import { language, t } from '$lib/stores/language';
import { Sun, Moon, Menu, X } from 'lucide-svelte';
let mobileMenuOpen = $state(false);
function toggleTheme() {
theme.toggle();
}
function toggleLanguage() {
language.toggle();
}
function toggleMobileMenu() {
mobileMenuOpen = !mobileMenuOpen;
}
</script>
<header class="fixed top-0 left-0 right-0 z-50 border-b backdrop-blur-md
bg-bg-dark/90 border-bg-dark-tertiary
dark:bg-bg-dark/90 dark:border-bg-dark-tertiary
[.light_&]:bg-bg-light/90 [.light_&]:border-bg-light-tertiary">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<!-- Logo -->
<a href="/" class="flex items-center gap-2">
<div class="w-8 h-8 bg-accent rounded-lg flex items-center justify-center">
<span class="text-white font-bold text-sm">DW</span>
</div>
<span class="font-bold text-xl text-text-dark [.light_&]:text-text-light">
DruckWerk
</span>
</a>
<!-- Desktop Navigation placeholder - will be added separately -->
<div class="hidden md:flex items-center gap-4">
<!-- Language Toggle -->
<button
onclick={toggleLanguage}
class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors
bg-bg-dark-secondary hover:bg-bg-dark-tertiary text-text-dark
[.light_&]:bg-bg-light-secondary [.light_&]:hover:bg-bg-light-tertiary [.light_&]:text-text-light"
>
{$language === 'de' ? 'EN' : 'DE'}
</button>
<!-- Theme Toggle -->
<button
onclick={toggleTheme}
class="p-2 rounded-lg transition-colors
bg-bg-dark-secondary hover:bg-bg-dark-tertiary text-text-dark
[.light_&]:bg-bg-light-secondary [.light_&]:hover:bg-bg-light-tertiary [.light_&]:text-text-light"
aria-label="Toggle theme"
>
{#if $theme === 'dark'}
<Sun class="w-5 h-5" />
{:else}
<Moon class="w-5 h-5" />
{/if}
</button>
</div>
<!-- Mobile menu button -->
<button
onclick={toggleMobileMenu}
class="md:hidden p-2 rounded-lg transition-colors
bg-bg-dark-secondary hover:bg-bg-dark-tertiary text-text-dark
[.light_&]:bg-bg-light-secondary [.light_&]:hover:bg-bg-light-tertiary [.light_&]:text-text-light"
aria-label="Toggle menu"
>
{#if mobileMenuOpen}
<X class="w-6 h-6" />
{:else}
<Menu class="w-6 h-6" />
{/if}
</button>
</div>
</div>
<!-- Mobile menu -->
{#if mobileMenuOpen}
<div class="md:hidden border-t
bg-bg-dark border-bg-dark-tertiary
[.light_&]:bg-bg-light [.light_&]:border-bg-light-tertiary">
<div class="px-4 py-4 space-y-3">
<div class="flex gap-2">
<button
onclick={toggleLanguage}
class="flex-1 px-3 py-2 rounded-lg text-sm font-medium transition-colors
bg-bg-dark-secondary hover:bg-bg-dark-tertiary text-text-dark
[.light_&]:bg-bg-light-secondary [.light_&]:hover:bg-bg-light-tertiary [.light_&]:text-text-light"
>
{$language === 'de' ? 'Switch to English' : 'Zu Deutsch wechseln'}
</button>
<button
onclick={toggleTheme}
class="px-3 py-2 rounded-lg transition-colors
bg-bg-dark-secondary hover:bg-bg-dark-tertiary text-text-dark
[.light_&]:bg-bg-light-secondary [.light_&]:hover:bg-bg-light-tertiary [.light_&]:text-text-light"
aria-label="Toggle theme"
>
{#if $theme === 'dark'}
<Sun class="w-5 h-5" />
{:else}
<Moon class="w-5 h-5" />
{/if}
</button>
</div>
</div>
</div>
{/if}
</header>

View File

@@ -0,0 +1,77 @@
<script lang="ts">
import { t } from '$lib/stores/language';
import { Cpu, Home, Layers } from 'lucide-svelte';
</script>
<section class="relative py-20 md:py-32 overflow-hidden">
<!-- Background gradient -->
<div class="absolute inset-0 bg-gradient-to-br from-accent/10 via-transparent to-primary/20"></div>
<!-- Grid pattern overlay -->
<div class="absolute inset-0 opacity-5"
style="background-image: url('data:image/svg+xml,%3Csvg width=%2240%22 height=%2240%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cpath d=%22M0 0h40v40H0z%22 fill=%22none%22 stroke=%22%23fff%22 stroke-width=%220.5%22/%3E%3C/svg%3E');">
</div>
<div class="relative max-w-4xl mx-auto text-center px-4">
<!-- Logo -->
<div class="inline-flex items-center justify-center w-20 h-20 mb-8 rounded-2xl bg-accent shadow-lg shadow-accent/25">
<svg viewBox="0 0 48 48" class="w-12 h-12 text-white" fill="currentColor">
<!-- Simplified enclosure icon -->
<rect x="6" y="10" width="36" height="28" rx="2" fill="none" stroke="currentColor" stroke-width="2.5"/>
<line x1="6" y1="18" x2="42" y2="18" stroke="currentColor" stroke-width="2"/>
<circle cx="12" cy="14" r="1.5" fill="currentColor"/>
<circle cx="17" cy="14" r="1.5" fill="currentColor"/>
<rect x="14" y="24" width="20" height="10" rx="1" fill="none" stroke="currentColor" stroke-width="1.5"/>
<line x1="10" y1="42" x2="10" y2="38" stroke="currentColor" stroke-width="2"/>
<line x1="38" y1="42" x2="38" y2="38" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
<!-- Title -->
<h1 class="text-4xl md:text-6xl font-bold mb-4 text-text-dark [.light_&]:text-text-light">
{$t.hero.title}
</h1>
<!-- Subtitle -->
<p class="text-xl md:text-2xl mb-6 text-accent font-medium">
{$t.hero.subtitle}
</p>
<!-- Tagline badges -->
<div class="flex flex-wrap justify-center gap-3 mb-8">
<span class="inline-flex items-center gap-2 px-4 py-2 rounded-full text-sm
bg-bg-dark-secondary border border-bg-dark-tertiary text-text-dark
[.light_&]:bg-bg-light-secondary [.light_&]:border-bg-light-tertiary [.light_&]:text-text-light">
<Cpu class="w-4 h-4 text-accent" />
ESP32
</span>
<span class="inline-flex items-center gap-2 px-4 py-2 rounded-full text-sm
bg-bg-dark-secondary border border-bg-dark-tertiary text-text-dark
[.light_&]:bg-bg-light-secondary [.light_&]:border-bg-light-tertiary [.light_&]:text-text-light">
<Home class="w-4 h-4 text-accent" />
Home Assistant
</span>
<span class="inline-flex items-center gap-2 px-4 py-2 rounded-full text-sm
bg-bg-dark-secondary border border-bg-dark-tertiary text-text-dark
[.light_&]:bg-bg-light-secondary [.light_&]:border-bg-light-tertiary [.light_&]:text-text-light">
<Layers class="w-4 h-4 text-accent" />
Dual-Zone
</span>
</div>
<!-- Description -->
<p class="text-lg text-text-dark-muted [.light_&]:text-text-light-muted max-w-2xl mx-auto">
{$t.hero.description}
</p>
<!-- Scroll indicator -->
<div class="mt-12 animate-bounce">
<a href="#overview" class="inline-flex flex-col items-center gap-2 text-text-dark-muted [.light_&]:text-text-light-muted hover:text-accent transition-colors">
<span class="text-sm">{$t.nav.overview}</span>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
</a>
</div>
</div>
</section>

View File

@@ -0,0 +1,113 @@
<script lang="ts">
import { ExternalLink, Printer } from 'lucide-svelte';
import { t } from '$lib/stores/language';
interface Material {
category: string;
article: string;
quantity: string;
specification: string;
link: string | null;
}
interface Props {
materials: Material[];
}
let { materials }: Props = $props();
// Group materials by category
const groupedMaterials = $derived(
materials.reduce((acc, item) => {
if (!acc[item.category]) {
acc[item.category] = [];
}
acc[item.category].push(item);
return acc;
}, {} as Record<string, Material[]>)
);
const categoryOrder = [
'Möbel', 'Holz', 'Tür', 'Druckteile', 'Strom', 'Verdrahtung',
'Controller', 'Sensorik', 'Elektronik', 'Lüfter', 'Filter', 'Beleuchtung', 'Werkzeug'
];
const sortedCategories = $derived(
Object.keys(groupedMaterials).sort((a, b) => {
const indexA = categoryOrder.indexOf(a);
const indexB = categoryOrder.indexOf(b);
if (indexA === -1 && indexB === -1) return a.localeCompare(b);
if (indexA === -1) return 1;
if (indexB === -1) return -1;
return indexA - indexB;
})
);
</script>
<div class="space-y-8">
{#each sortedCategories as category}
<div class="rounded-lg border overflow-hidden
border-bg-dark-tertiary [.light_&]:border-bg-light-tertiary">
<div class="px-4 py-3 bg-bg-dark-secondary [.light_&]:bg-bg-light-secondary">
<h4 class="font-semibold text-text-dark [.light_&]:text-text-light">
{category}
</h4>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-bg-dark-tertiary/50 [.light_&]:bg-bg-light-tertiary/50">
<tr>
<th class="px-4 py-2 text-left font-medium text-text-dark-muted [.light_&]:text-text-light-muted">
{$t.materials.columns.article}
</th>
<th class="px-4 py-2 text-left font-medium text-text-dark-muted [.light_&]:text-text-light-muted">
{$t.materials.columns.quantity}
</th>
<th class="px-4 py-2 text-left font-medium text-text-dark-muted [.light_&]:text-text-light-muted">
{$t.materials.columns.specification}
</th>
<th class="px-4 py-2 text-right font-medium text-text-dark-muted [.light_&]:text-text-light-muted">
{$t.materials.columns.link}
</th>
</tr>
</thead>
<tbody class="divide-y divide-bg-dark-tertiary [.light_&]:divide-bg-light-tertiary">
{#each groupedMaterials[category] as item}
<tr class="hover:bg-bg-dark-secondary/30 [.light_&]:hover:bg-bg-light-secondary/30 transition-colors">
<td class="px-4 py-3 text-text-dark [.light_&]:text-text-light">
{item.article}
</td>
<td class="px-4 py-3 text-text-dark [.light_&]:text-text-light">
{item.quantity}
</td>
<td class="px-4 py-3 text-text-dark-muted [.light_&]:text-text-light-muted">
{item.specification}
</td>
<td class="px-4 py-3 text-right">
{#if item.link}
<a
href={item.link}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors
bg-accent/10 text-accent hover:bg-accent/20"
>
<ExternalLink class="w-3 h-3" />
{$t.materials.buyNow}
</a>
{:else}
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium
bg-primary-light/50 text-text-dark-muted [.light_&]:bg-bg-light-tertiary [.light_&]:text-text-light-muted">
<Printer class="w-3 h-3" />
{$t.materials.printedPart}
</span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/each}
</div>

View File

@@ -0,0 +1,74 @@
<script lang="ts">
import { t } from '$lib/stores/language';
import { Box, List, Gauge, Wrench, Layers, Zap, Battery, Lightbulb, Fan, Cpu, LayoutGrid, Code, CheckSquare, Puzzle } from 'lucide-svelte';
interface NavItem {
id: string;
labelKey: keyof typeof $t.nav;
icon: typeof Box;
}
const navItems: NavItem[] = [
{ id: 'overview', labelKey: 'overview', icon: Box },
{ id: 'materials', labelKey: 'materials', icon: List },
{ id: 'standards', labelKey: 'standards', icon: Gauge },
{ id: 'mechanical', labelKey: 'mechanical', icon: Wrench },
{ id: 'zones', labelKey: 'zones', icon: Layers },
{ id: 'ac-wiring', labelKey: 'acWiring', icon: Zap },
{ id: 'dc-distribution', labelKey: 'dcDistribution', icon: Battery },
{ id: 'leds', labelKey: 'leds', icon: Lightbulb },
{ id: 'fans', labelKey: 'fans', icon: Fan },
{ id: 'esp32', labelKey: 'esp32', icon: Cpu },
{ id: 'panel', labelKey: 'panel', icon: LayoutGrid },
{ id: 'software', labelKey: 'software', icon: Code },
{ id: 'checklist', labelKey: 'checklist', icon: CheckSquare },
{ id: 'extensions', labelKey: 'extensions', icon: Puzzle }
];
let activeSection = $state('overview');
function handleScroll() {
const sections = navItems.map(item => document.getElementById(item.id));
const scrollPos = window.scrollY + 100;
for (let i = sections.length - 1; i >= 0; i--) {
const section = sections[i];
if (section && section.offsetTop <= scrollPos) {
activeSection = navItems[i].id;
break;
}
}
}
$effect(() => {
if (typeof window !== 'undefined') {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}
});
</script>
<nav class="hidden lg:block fixed left-0 top-16 bottom-0 w-64 overflow-y-auto border-r
bg-bg-dark border-bg-dark-tertiary
[.light_&]:bg-bg-light [.light_&]:border-bg-light-tertiary">
<div class="py-6 px-4">
<ul class="space-y-1">
{#each navItems as item}
{@const Icon = item.icon}
<li>
<a
href="#{item.id}"
class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors
{activeSection === item.id
? 'bg-accent text-white'
: 'text-text-dark-muted hover:text-text-dark hover:bg-bg-dark-secondary [.light_&]:text-text-light-muted [.light_&]:hover:text-text-light [.light_&]:hover:bg-bg-light-secondary'
}"
>
<Icon class="w-4 h-4 flex-shrink-0" />
<span>{$t.nav[item.labelKey]}</span>
</a>
</li>
{/each}
</ul>
</div>
</nav>

View File

@@ -0,0 +1,27 @@
<script lang="ts">
interface Props {
id: string;
title: string;
subtitle?: string;
children: import('svelte').Snippet;
}
let { id, title, subtitle, children }: Props = $props();
</script>
<section {id} class="scroll-mt-20 py-12 border-b
border-bg-dark-tertiary [.light_&]:border-bg-light-tertiary">
<div class="mb-8">
<h2 class="text-2xl md:text-3xl font-bold text-text-dark [.light_&]:text-text-light">
{title}
</h2>
{#if subtitle}
<p class="mt-2 text-text-dark-muted [.light_&]:text-text-light-muted">
{subtitle}
</p>
{/if}
</div>
<div class="space-y-6">
{@render children()}
</div>
</section>

View File

@@ -0,0 +1,30 @@
<script lang="ts">
interface Props {
title?: string;
steps: string[];
}
let { title, steps }: Props = $props();
</script>
<div class="rounded-lg border p-4
bg-bg-dark-secondary/50 border-bg-dark-tertiary
[.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
{#if title}
<h4 class="font-semibold mb-4 text-text-dark [.light_&]:text-text-light">
{title}
</h4>
{/if}
<ol class="space-y-3">
{#each steps as step, index}
<li class="flex gap-3">
<span class="flex-shrink-0 w-6 h-6 rounded-full bg-accent text-white text-sm font-medium flex items-center justify-center">
{index + 1}
</span>
<span class="text-text-dark [.light_&]:text-text-light pt-0.5">
{step}
</span>
</li>
{/each}
</ol>
</div>

View File

@@ -0,0 +1,63 @@
<script lang="ts">
interface Column {
key: string;
header: string;
wireColor?: 'l' | 'n' | 'pe' | '12v' | '5v' | 'gnd';
}
interface Props {
columns: Column[];
rows: Record<string, string>[];
caption?: string;
}
let { columns, rows, caption }: Props = $props();
function getWireColorClass(color?: string): string {
switch (color) {
case 'l': return 'bg-wire-l';
case 'n': return 'bg-wire-n';
case 'pe': return 'bg-wire-pe';
case '12v': return 'bg-wire-12v';
case '5v': return 'bg-wire-5v';
case 'gnd': return 'bg-wire-gnd';
default: return '';
}
}
</script>
<div class="overflow-x-auto rounded-lg border
border-bg-dark-tertiary [.light_&]:border-bg-light-tertiary">
<table class="w-full text-sm">
{#if caption}
<caption class="px-4 py-2 text-left text-text-dark-muted [.light_&]:text-text-light-muted bg-bg-dark-secondary [.light_&]:bg-bg-light-secondary">
{caption}
</caption>
{/if}
<thead class="bg-bg-dark-secondary [.light_&]:bg-bg-light-secondary">
<tr>
{#each columns as column}
<th class="px-4 py-3 text-left font-semibold text-text-dark [.light_&]:text-text-light">
{column.header}
</th>
{/each}
</tr>
</thead>
<tbody class="divide-y divide-bg-dark-tertiary [.light_&]:divide-bg-light-tertiary">
{#each rows as row}
<tr class="hover:bg-bg-dark-secondary/50 [.light_&]:hover:bg-bg-light-secondary/50 transition-colors">
{#each columns as column}
<td class="px-4 py-3 text-text-dark [.light_&]:text-text-light">
<span class="flex items-center gap-2">
{#if column.wireColor}
<span class="w-3 h-3 rounded-full {getWireColorClass(column.wireColor)}"></span>
{/if}
{row[column.key]}
</span>
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>

View File

@@ -0,0 +1,54 @@
<script lang="ts">
import { AlertTriangle, Info, Lightbulb, AlertCircle } from 'lucide-svelte';
interface Props {
type?: 'warning' | 'danger' | 'info' | 'tip';
title?: string;
children: import('svelte').Snippet;
}
let { type = 'warning', title, children }: Props = $props();
const config = {
warning: {
icon: AlertTriangle,
bgClass: 'bg-warning/10 border-warning/30',
iconClass: 'text-warning',
titleClass: 'text-warning'
},
danger: {
icon: AlertCircle,
bgClass: 'bg-danger/10 border-danger/30',
iconClass: 'text-danger',
titleClass: 'text-danger'
},
info: {
icon: Info,
bgClass: 'bg-wire-blue/10 border-wire-blue/30',
iconClass: 'text-wire-blue',
titleClass: 'text-wire-blue'
},
tip: {
icon: Lightbulb,
bgClass: 'bg-success/10 border-success/30',
iconClass: 'text-success',
titleClass: 'text-success'
}
};
const currentConfig = $derived(config[type]);
</script>
<div class="rounded-lg border p-4 {currentConfig.bgClass}">
<div class="flex gap-3">
<currentConfig.icon class="w-5 h-5 flex-shrink-0 mt-0.5 {currentConfig.iconClass}" />
<div class="flex-1">
{#if title}
<h4 class="font-semibold mb-1 {currentConfig.titleClass}">{title}</h4>
{/if}
<div class="text-sm text-text-dark [.light_&]:text-text-light">
{@render children()}
</div>
</div>
</div>
</div>

238
src/lib/data/materials.ts Normal file
View File

@@ -0,0 +1,238 @@
export interface Material {
category: string;
article: string;
quantity: string;
specification: string;
link: string | null;
}
export const materials: Material[] = [
// Möbel
{
category: 'Möbel',
article: 'IKEA Tisch',
quantity: '1',
specification: '55x55cm Grundfläche; 50cm Höhe',
link: 'https://www.amazon.de/s?k=ikea+tisch+55x55'
},
// Holz
{
category: 'Holz',
article: 'OSB Platten',
quantity: '3',
specification: '912mm für Seiten & Rückwand',
link: 'https://www.amazon.de/s?k=osb+platte+10mm'
},
// Tür
{
category: 'Tür',
article: 'Plexiglas Acryl',
quantity: '1',
specification: '46mm Fronttür',
link: 'https://www.amazon.de/s?k=plexiglas+4mm'
},
// Druckteile
{
category: 'Druckteile',
article: 'Corner Brackets',
quantity: '12',
specification: 'PETG',
link: null
},
{
category: 'Druckteile',
article: 'Türscharniere',
quantity: '2',
specification: 'PETG',
link: null
},
{
category: 'Druckteile',
article: 'Türgriff',
quantity: '1',
specification: 'PETG',
link: null
},
{
category: 'Druckteile',
article: 'Türanschläge',
quantity: '24',
specification: 'PETG',
link: null
},
{
category: 'Druckteile',
article: 'WAGO Halter 221',
quantity: '46',
specification: 'PETG',
link: null
},
{
category: 'Druckteile',
article: 'Kabelklemme',
quantity: '1',
specification: 'Zugentlastung, PETG',
link: null
},
// Strom
{
category: 'Strom',
article: 'IEC C14 Entry mit Sicherung',
quantity: '1',
specification: '5x20mm Sicherung',
link: 'https://www.amazon.de/s?k=iec+c14+einbaubuchse+sicherung'
},
{
category: 'Strom',
article: 'Feinsicherung T5A',
quantity: '1',
specification: '250V träge',
link: 'https://www.amazon.de/s?k=5x20mm+t5a'
},
{
category: 'Strom',
article: 'Dual Output PSU',
quantity: '1',
specification: '230V → 12V + 5V',
link: 'https://www.amazon.de/s?k=12v+5v+dual+power+supply'
},
// Verdrahtung
{
category: 'Verdrahtung',
article: 'WAGO 221 Klemmen',
quantity: '1 Set',
specification: '3- & 5-polig',
link: 'https://www.amazon.de/s?k=wago+221'
},
{
category: 'Verdrahtung',
article: 'AC Kabel',
quantity: '2m',
specification: 'H05VV-F 3x1.0mm²',
link: 'https://www.amazon.de/s?k=h05vv-f+3x1+0'
},
{
category: 'Verdrahtung',
article: 'DC Kabel 12V',
quantity: '5m',
specification: '2x0.75mm² rot/schwarz',
link: 'https://www.amazon.de/s?k=silikonkabel+2x0.75'
},
{
category: 'Verdrahtung',
article: 'DC Kabel 5V',
quantity: '3m',
specification: '2x0.5mm² orange/schwarz',
link: 'https://www.amazon.de/s?k=silikonkabel+orange'
},
{
category: 'Verdrahtung',
article: 'Aderendhülsen',
quantity: '1 Set',
specification: '0.251.5mm²',
link: 'https://www.amazon.de/s?k=aderendhülsen'
},
{
category: 'Verdrahtung',
article: 'Crimpzange',
quantity: '1',
specification: 'für Aderendhülsen',
link: 'https://www.amazon.de/s?k=crimpzange'
},
{
category: 'Verdrahtung',
article: 'Schrumpfschlauch',
quantity: '1 Set',
specification: '3:1 mit Kleber',
link: 'https://www.amazon.de/s?k=schrumpfschlauch'
},
{
category: 'Verdrahtung',
article: 'Kabelverschraubung',
quantity: '1',
specification: 'M20 / PG13.5',
link: 'https://www.amazon.de/s?k=kabelverschraubung+m20'
},
// Controller
{
category: 'Controller',
article: 'ESP32 DevKit',
quantity: '1',
specification: 'WROOM-32',
link: 'https://www.amazon.de/s?k=esp32+devkit'
},
// Sensorik
{
category: 'Sensorik',
article: 'BME280',
quantity: '1',
specification: 'I2C Temp/Feuchte',
link: 'https://www.amazon.de/s?k=bme280'
},
// Elektronik
{
category: 'Elektronik',
article: 'MOSFET PWM Modul',
quantity: '5',
specification: 'Logic-Level 12V',
link: 'https://www.amazon.de/s?k=mosfet+pwm+12v'
},
// Lüfter
{
category: 'Lüfter',
article: '120mm PWM Lüfter',
quantity: '4',
specification: '1 Zuluft, 3 Abluft',
link: 'https://www.amazon.de/s?k=120mm+pwm+l%C3%BCfter'
},
// Filter
{
category: 'Filter',
article: 'Aktivkohlefilter 120mm',
quantity: '1',
specification: 'Abluft',
link: 'https://www.amazon.de/s?k=120mm+aktivkohlefilter'
},
// Beleuchtung
{
category: 'Beleuchtung',
article: 'LED Strip 12V',
quantity: '1',
specification: '10mm warmweiß',
link: 'https://www.amazon.de/s?k=12v+led+strip+10mm'
},
{
category: 'Beleuchtung',
article: 'LED Alu Profil Diffusor',
quantity: '6',
specification: '10mm',
link: 'https://www.amazon.de/s?k=led+profil+10mm'
},
// Werkzeug
{
category: 'Werkzeug',
article: 'Lötkolben',
quantity: '1',
specification: 'regelbar',
link: 'https://www.amazon.de/s?k=lötkolben'
},
{
category: 'Werkzeug',
article: 'Multimeter',
quantity: '1',
specification: 'Spannungsprüfung',
link: 'https://www.amazon.de/s?k=multimeter'
}
];

335
src/lib/i18n/de.ts Normal file
View File

@@ -0,0 +1,335 @@
export const de = {
// Header & Navigation
nav: {
overview: 'Überblick',
materials: 'Materialien',
standards: 'Standards',
mechanical: 'Mechanik',
zones: 'Zonen',
acWiring: 'AC-Verdrahtung',
dcDistribution: 'DC-Verteilung',
leds: 'LEDs',
fans: 'Lüfter',
esp32: 'ESP32',
panel: 'Panel',
software: 'Software',
config: 'Konfiguration',
checklist: 'Checkliste',
extensions: 'Erweiterungen'
},
// Hero Section
hero: {
title: 'DruckWerk',
subtitle: 'Professionelles 3D-Drucker Gehäuse',
tagline: 'ESP32 gesteuert | Home Assistant Integration | Dual-Zone Design',
description: 'Ein IKEA-Tisch wird zu einem geschlossenen, belüfteten Druckergehäuse mit fester 230V-Einspeisung, interner AC-Verteilung über WAGO, und ESP32-Steuerung.'
},
// Overview Section
overview: {
title: 'Gesamtüberblick',
subtitle: 'Was entsteht',
features: [
'Feste 230V-Einspeisung über IEC C14',
'Interne AC-Verteilung über WAGO',
'Fest angeschlossener Drucker (kein Stecker innen)',
'Dual-Output-PSU (12V + 5V)',
'ESP32-Steuerung (PWM für LEDs & Lüfter)',
'Home-Assistant-Integration',
'Sauber getrennte AC- und DC-Zonen',
'Vollständig servicefähiger Aufbau'
],
conclusion: {
title: 'Fazit',
points: [
'Keine Bastellösung',
'Kein Overengineering',
'Maschinenbau-sauber'
],
benefits: 'So kannst du später umbauen, reparieren und erweitern - ohne alles neu zu machen.'
}
},
// Materials Section
materials: {
title: 'Materialliste',
subtitle: 'Alle benötigten Komponenten',
categories: {
furniture: 'Möbel',
wood: 'Holz',
door: 'Tür',
printed: '3D-Druckteile',
power: 'Strom',
wiring: 'Verdrahtung',
controller: 'Controller',
sensors: 'Sensorik',
electronics: 'Elektronik',
fans: 'Lüfter',
filter: 'Filter',
lighting: 'Beleuchtung',
tools: 'Werkzeug'
},
columns: {
article: 'Artikel',
quantity: 'Menge',
specification: 'Spezifikation',
link: 'Link'
},
printedPart: '3D-Druck',
buyNow: 'Kaufen'
},
// Standards Section
standards: {
title: 'Farb- & Spannungsstandard',
subtitle: 'Verbindlich für alle Verbindungen',
voltage: 'Spannung',
color: 'Farbe',
acSection: 'AC (Wechselstrom)',
dcSection: 'DC (Gleichstrom)',
logicSection: 'Logik'
},
// Mechanical Build Section
mechanical: {
title: 'Mechanischer Aufbau',
tableAndEnclosure: {
title: 'Tisch & Gehäuse',
steps: [
'IKEA-Tisch aufstellen',
'OSB-Platten zuschneiden: links, rechts, hinten',
'OSB mit gedruckten Corner-Brackets montieren (Material: PETG, Vorbohren 2,5-3mm, Holzschrauben)',
'Obere Tischplatte bleibt frei nutzbar'
]
},
door: {
title: 'Tür',
steps: [
'Plexiglas zuschneiden',
'Gedruckte Scharniere montieren',
'Türgriff + Anschläge montieren',
'Optional: Dichtung (Moosgummi) für leiseres & dichteres Gehäuse'
]
}
},
// Zones Section
zones: {
title: 'Zonenkonzept',
subtitle: 'Wichtig: Strikte Trennung',
acZone: {
title: 'AC-Zone (230V)',
components: [
'IEC C14',
'WAGO-Klemmen L / N / PE',
'Dual-Output-PSU',
'Druckerkabel (fest)'
],
note: 'Abgedeckt mit 3D-gedrucktem Cover'
},
dcZone: {
title: 'DC-Zone (12V / 5V)',
components: [
'WAGO-Busse',
'MOSFET-Module',
'ESP32',
'Sensoren',
'LEDs / Lüfter'
],
note: 'Keine 230V hier'
}
},
// AC Wiring Section
acWiring: {
title: 'AC-Teil (230V)',
warning: 'Nur spannungsfrei arbeiten!',
iecEntry: {
title: 'IEC C14 einbauen',
steps: [
'Ausschnitt nach Datenblatt sägen',
'IEC C14 einsetzen und verschrauben',
'Sicherung einsetzen (T5A, 5×20 mm)'
]
},
wagoHolder: {
title: 'WAGO-Halter (AC)',
steps: [
'Gedruckte WAGO-Halter (221-413) montieren',
'Drei Klemmen: L, N, PE',
'Halter auf Elektronik-Backplate schrauben',
'Abdeckung vorbereiten (wird später montiert)'
]
},
iecToWago: {
title: 'IEC C14 → WAGO',
note: 'Aderendhülsen verwenden, keine losen Litzen'
},
printer: {
title: 'Drucker anschließen (fest)',
steps: [
'Druckerkabel durch Kabelverschraubung (M20/PG13.5) führen',
'Innen Zugentlastung setzen',
'Schuko-Stecker abschneiden',
'Adern abisolieren + Aderendhülsen',
'Anschluss an WAGO (Braun→L, Blau→N, Gelb-Grün→PE)'
],
note: 'Drucker ist jetzt Teil der festen Installation'
},
psu: {
title: 'Dual-Output-PSU anschließen (AC-Seite)',
note: 'Von denselben WAGOs: L, N, PE (falls vorhanden). PSU fest verschrauben.'
}
},
// DC Distribution Section
dcDistribution: {
title: 'DC-Teil Stromverteilung',
wagoBuses: {
title: 'WAGO-Busse (DC)',
description: 'Gedruckte Halter für:',
buses: ['12V+', '5V+', 'GND (gemeinsam)'],
recommendation: 'Empfohlen: WAGO 221-415 (5-polig)'
},
psuToWago: {
title: 'PSU → DC-WAGOs',
note: '12V- und 5V-GND dürfen gemeinsam sein'
}
},
// LEDs Section
leds: {
title: 'LEDs (PWM, 12V)',
mechanics: {
title: 'Mechanik',
description: '6 LED-Segmente in Alu-Profilen mit Diffusor. Je Bein: vorne 1 Segment, hinten 2 Segmente.'
},
electrical: {
title: 'Elektrik',
description: 'LED+ direkt an 12V+, LED- über MOSFET Drain.',
mosfet: 'MOSFET: Source → GND, Gate → ESP32 PWM-Pin',
pwm: 'PWM-Frequenz: 15 kHz'
}
},
// Fans Section
fans: {
title: 'Lüfter & Aktivkohle (PWM)',
layout: {
title: 'Layout',
items: ['1× Zuluft', '2× Abluft', '1× Aktivkohle']
},
electrical: {
title: 'Elektrik (pro Lüfter)',
description: 'Lüfter+ direkt 12V+, Lüfter- über MOSFET Drain.',
mosfet: 'MOSFET: Source → GND, Gate → ESP32 PWM-Pin',
pwm: 'PWM-Frequenz: 2025 kHz (leise)'
}
},
// ESP32 Section
esp32: {
title: 'ESP32 & Sensorik',
power: {
title: 'Versorgung',
description: 'ESP32 VIN/5V → 5V+ (orange), GND → GND'
},
voltage: {
title: '3,3V',
description: 'Kommt onboard vom ESP32. Kein Buck nötig, nur kurze Leitungen.'
},
bme280: {
title: 'BME280 Sensor',
description: 'I2C Temperatur & Feuchte'
}
},
// Panel Section
panel: {
title: 'Taster / Panel (optional)',
description: 'Taster → GPIO mit Pull-Up',
functions: ['Licht AN/AUS', 'Lüfter Boost', 'Reset']
},
// Software Section
software: {
title: 'Software (Überblick)',
esphome: {
title: 'ESPHome',
features: [
'PWM Outputs: LEDs, Zuluft, Abluft 1, Abluft 2, Aktivkohle',
'BME280 Sensor',
'Fail-Safe: bei Verbindungsverlust → Lüfter auf 50%'
]
},
homeAssistant: {
title: 'Home Assistant',
features: [
'Bambu-Integration (Status)',
'Automationen: PLA/ABS Profil, Nachlauf Aktivkohle, Übertemperatur'
]
}
},
// Config Section
config: {
title: 'Konfiguration',
esphome: {
title: 'ESPHome Konfiguration',
filename: 'esphome.yaml',
placeholder: 'Konfiguration wird hinzugefügt...'
},
homeAssistant: {
title: 'Home Assistant Automationen',
filename: 'automations.yaml',
placeholder: 'Konfiguration wird hinzugefügt...'
},
pending: 'Konfiguration ausstehend'
},
// Checklist Section
checklist: {
title: 'Abschluss-Checkliste',
subtitle: 'Pflicht vor Inbetriebnahme',
electrical: {
title: 'Elektrisch',
items: [
'PE durchgängig',
'AC & DC getrennt',
'AC-WAGOs abgedeckt',
'Zugentlastung gesetzt',
'Keine offenen 230V-Teile'
]
},
functional: {
title: 'Funktional',
items: [
'LEDs dimmen sauber',
'Lüfter PWM leise',
'Temp/Feuchte sichtbar',
'HASS erreichbar'
]
}
},
// Extensions Section
extensions: {
title: 'Nächste sinnvolle Erweiterungen',
subtitle: 'Optional',
items: [
'Rauch-/VOC-Sensor',
'Türkontakt',
'Geräuschprofil-Automationen',
'Druck-abhängige Lüftersteuerung'
]
},
// Common
common: {
warning: 'Warnung',
note: 'Hinweis',
tip: 'Tipp',
important: 'Wichtig'
}
};

335
src/lib/i18n/en.ts Normal file
View File

@@ -0,0 +1,335 @@
export const en = {
// Header & Navigation
nav: {
overview: 'Overview',
materials: 'Materials',
standards: 'Standards',
mechanical: 'Mechanical',
zones: 'Zones',
acWiring: 'AC Wiring',
dcDistribution: 'DC Distribution',
leds: 'LEDs',
fans: 'Fans',
esp32: 'ESP32',
panel: 'Panel',
software: 'Software',
config: 'Configuration',
checklist: 'Checklist',
extensions: 'Extensions'
},
// Hero Section
hero: {
title: 'DruckWerk',
subtitle: 'Professional 3D Printer Enclosure',
tagline: 'ESP32 Controlled | Home Assistant Integration | Dual-Zone Design',
description: 'An IKEA table transformed into a sealed, ventilated printer enclosure with fixed 230V power input, internal AC distribution via WAGO, and ESP32 control.'
},
// Overview Section
overview: {
title: 'Overview',
subtitle: 'What We Are Building',
features: [
'Fixed 230V power input via IEC C14',
'Internal AC distribution via WAGO',
'Permanently connected printer (no internal plug)',
'Dual-output PSU (12V + 5V)',
'ESP32 control (PWM for LEDs & fans)',
'Home Assistant integration',
'Cleanly separated AC and DC zones',
'Fully serviceable design'
],
conclusion: {
title: 'Conclusion',
points: [
'Not a DIY hack',
'Not over-engineered',
'Machine-building clean'
],
benefits: 'This allows you to modify, repair, and extend later - without redoing everything.'
}
},
// Materials Section
materials: {
title: 'Materials List',
subtitle: 'All Required Components',
categories: {
furniture: 'Furniture',
wood: 'Wood',
door: 'Door',
printed: '3D Printed Parts',
power: 'Power',
wiring: 'Wiring',
controller: 'Controller',
sensors: 'Sensors',
electronics: 'Electronics',
fans: 'Fans',
filter: 'Filter',
lighting: 'Lighting',
tools: 'Tools'
},
columns: {
article: 'Article',
quantity: 'Qty',
specification: 'Specification',
link: 'Link'
},
printedPart: '3D Print',
buyNow: 'Buy'
},
// Standards Section
standards: {
title: 'Color & Voltage Standard',
subtitle: 'Mandatory for All Connections',
voltage: 'Voltage',
color: 'Color',
acSection: 'AC (Alternating Current)',
dcSection: 'DC (Direct Current)',
logicSection: 'Logic'
},
// Mechanical Build Section
mechanical: {
title: 'Mechanical Build',
tableAndEnclosure: {
title: 'Table & Enclosure',
steps: [
'Set up IKEA table',
'Cut OSB panels: left, right, back',
'Mount OSB with printed corner brackets (Material: PETG, pre-drill 2.5-3mm, wood screws)',
'Top table surface remains usable'
]
},
door: {
title: 'Door',
steps: [
'Cut plexiglass',
'Mount printed hinges',
'Mount door handle + stops',
'Optional: Seal (foam rubber) for quieter & tighter enclosure'
]
}
},
// Zones Section
zones: {
title: 'Zone Concept',
subtitle: 'Important: Strict Separation',
acZone: {
title: 'AC Zone (230V)',
components: [
'IEC C14',
'WAGO terminals L / N / PE',
'Dual-output PSU',
'Printer cable (fixed)'
],
note: 'Covered with 3D-printed cover'
},
dcZone: {
title: 'DC Zone (12V / 5V)',
components: [
'WAGO buses',
'MOSFET modules',
'ESP32',
'Sensors',
'LEDs / Fans'
],
note: 'No 230V here'
}
},
// AC Wiring Section
acWiring: {
title: 'AC Section (230V)',
warning: 'Only work when power is off!',
iecEntry: {
title: 'Install IEC C14',
steps: [
'Cut opening per datasheet',
'Insert and screw in IEC C14',
'Insert fuse (T5A, 5×20 mm)'
]
},
wagoHolder: {
title: 'WAGO Holder (AC)',
steps: [
'Mount printed WAGO holders (221-413)',
'Three terminals: L, N, PE',
'Screw holder to electronics backplate',
'Prepare cover (mounted later)'
]
},
iecToWago: {
title: 'IEC C14 → WAGO',
note: 'Use ferrules, no loose strands'
},
printer: {
title: 'Connect Printer (permanent)',
steps: [
'Feed printer cable through cable gland (M20/PG13.5)',
'Add strain relief inside',
'Cut off Schuko plug',
'Strip wires + add ferrules',
'Connect to WAGO (Brown→L, Blue→N, Yellow-Green→PE)'
],
note: 'Printer is now part of the fixed installation'
},
psu: {
title: 'Connect Dual-Output PSU (AC side)',
note: 'From same WAGOs: L, N, PE (if available). Screw PSU down firmly.'
}
},
// DC Distribution Section
dcDistribution: {
title: 'DC Section Power Distribution',
wagoBuses: {
title: 'WAGO Buses (DC)',
description: 'Printed holders for:',
buses: ['12V+', '5V+', 'GND (shared)'],
recommendation: 'Recommended: WAGO 221-415 (5-pole)'
},
psuToWago: {
title: 'PSU → DC WAGOs',
note: '12V and 5V GND can be shared'
}
},
// LEDs Section
leds: {
title: 'LEDs (PWM, 12V)',
mechanics: {
title: 'Mechanics',
description: '6 LED segments in aluminum profiles with diffuser. Per leg: front 1 segment, back 2 segments.'
},
electrical: {
title: 'Electrical',
description: 'LED+ direct to 12V+, LED- via MOSFET drain.',
mosfet: 'MOSFET: Source → GND, Gate → ESP32 PWM pin',
pwm: 'PWM frequency: 15 kHz'
}
},
// Fans Section
fans: {
title: 'Fans & Carbon Filter (PWM)',
layout: {
title: 'Layout',
items: ['1× Intake', '2× Exhaust', '1× Carbon Filter']
},
electrical: {
title: 'Electrical (per fan)',
description: 'Fan+ direct 12V+, Fan- via MOSFET drain.',
mosfet: 'MOSFET: Source → GND, Gate → ESP32 PWM pin',
pwm: 'PWM frequency: 2025 kHz (quiet)'
}
},
// ESP32 Section
esp32: {
title: 'ESP32 & Sensors',
power: {
title: 'Power Supply',
description: 'ESP32 VIN/5V → 5V+ (orange), GND → GND'
},
voltage: {
title: '3.3V',
description: 'Comes from ESP32 onboard. No buck needed, keep wires short.'
},
bme280: {
title: 'BME280 Sensor',
description: 'I2C Temperature & Humidity'
}
},
// Panel Section
panel: {
title: 'Buttons / Panel (optional)',
description: 'Button → GPIO with pull-up',
functions: ['Light ON/OFF', 'Fan Boost', 'Reset']
},
// Software Section
software: {
title: 'Software (Overview)',
esphome: {
title: 'ESPHome',
features: [
'PWM Outputs: LEDs, Intake, Exhaust 1, Exhaust 2, Carbon Filter',
'BME280 Sensor',
'Fail-safe: on connection loss → fans at 50%'
]
},
homeAssistant: {
title: 'Home Assistant',
features: [
'Bambu integration (status)',
'Automations: PLA/ABS profile, carbon filter follow-up, overtemperature'
]
}
},
// Config Section
config: {
title: 'Configuration',
esphome: {
title: 'ESPHome Configuration',
filename: 'esphome.yaml',
placeholder: 'Configuration coming soon...'
},
homeAssistant: {
title: 'Home Assistant Automations',
filename: 'automations.yaml',
placeholder: 'Configuration coming soon...'
},
pending: 'Configuration pending'
},
// Checklist Section
checklist: {
title: 'Final Checklist',
subtitle: 'Mandatory Before Operation',
electrical: {
title: 'Electrical',
items: [
'PE continuous',
'AC & DC separated',
'AC WAGOs covered',
'Strain relief set',
'No exposed 230V parts'
]
},
functional: {
title: 'Functional',
items: [
'LEDs dim smoothly',
'Fan PWM quiet',
'Temp/humidity visible',
'HASS reachable'
]
}
},
// Extensions Section
extensions: {
title: 'Recommended Future Extensions',
subtitle: 'Optional',
items: [
'Smoke/VOC sensor',
'Door contact',
'Noise profile automations',
'Print-dependent fan control'
]
},
// Common
common: {
warning: 'Warning',
note: 'Note',
tip: 'Tip',
important: 'Important'
}
};

3
src/lib/i18n/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { de } from './de';
export { en } from './en';
export type { Language } from '$lib/stores/language';

View File

@@ -0,0 +1,50 @@
import { writable, derived } from 'svelte/store';
import { browser } from '$app/environment';
import { de } from '$lib/i18n/de';
import { en } from '$lib/i18n/en';
export type Language = 'de' | 'en';
const translations = { de, en };
function createLanguageStore() {
const storedLang = browser ? localStorage.getItem('language') as Language : null;
const browserLang = browser ? navigator.language.split('-')[0] : 'de';
const initialLang: Language = storedLang || (browserLang === 'en' ? 'en' : 'de');
const { subscribe, set } = writable<Language>(initialLang);
return {
subscribe,
set: (value: Language) => {
if (browser) {
localStorage.setItem('language', value);
document.documentElement.lang = value;
}
set(value);
},
toggle: () => {
let current: Language = 'de';
subscribe(v => current = v)();
const newLang = current === 'de' ? 'en' : 'de';
if (browser) {
localStorage.setItem('language', newLang);
document.documentElement.lang = newLang;
}
set(newLang);
},
init: () => {
if (browser) {
const stored = localStorage.getItem('language') as Language;
const browserLang = navigator.language.split('-')[0];
const lang = stored || (browserLang === 'en' ? 'en' : 'de');
document.documentElement.lang = lang;
set(lang);
}
}
};
}
export const language = createLanguageStore();
export const t = derived(language, ($language) => translations[$language]);

54
src/lib/stores/theme.ts Normal file
View File

@@ -0,0 +1,54 @@
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
type Theme = 'dark' | 'light';
function createThemeStore() {
const storedTheme = browser ? localStorage.getItem('theme') as Theme : null;
const prefersDark = browser ? window.matchMedia('(prefers-color-scheme: dark)').matches : true;
const initialTheme: Theme = storedTheme || (prefersDark ? 'dark' : 'light');
const { subscribe, set, update } = writable<Theme>(initialTheme);
return {
subscribe,
set: (value: Theme) => {
if (browser) {
localStorage.setItem('theme', value);
if (value === 'light') {
document.documentElement.classList.add('light');
} else {
document.documentElement.classList.remove('light');
}
}
set(value);
},
toggle: () => {
update(current => {
const newTheme = current === 'dark' ? 'light' : 'dark';
if (browser) {
localStorage.setItem('theme', newTheme);
if (newTheme === 'light') {
document.documentElement.classList.add('light');
} else {
document.documentElement.classList.remove('light');
}
}
return newTheme;
});
},
init: () => {
if (browser) {
const stored = localStorage.getItem('theme') as Theme;
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = stored || (prefersDark ? 'dark' : 'light');
if (theme === 'light') {
document.documentElement.classList.add('light');
}
set(theme);
}
}
};
}
export const theme = createThemeStore();

28
src/routes/+layout.svelte Normal file
View File

@@ -0,0 +1,28 @@
<script lang="ts">
import '../app.css';
import { onMount } from 'svelte';
import { theme } from '$lib/stores/theme';
import { language } from '$lib/stores/language';
import Header from '$lib/components/Header.svelte';
import Navigation from '$lib/components/Navigation.svelte';
let { children } = $props();
onMount(() => {
theme.init();
language.init();
});
</script>
<svelte:head>
<title>DruckWerk - 3D Printer Enclosure</title>
</svelte:head>
<Header />
<Navigation />
<main class="lg:ml-64 pt-16">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{@render children()}
</div>
</main>

347
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,347 @@
<script lang="ts">
import { t } from '$lib/stores/language';
import Hero from '$lib/components/Hero.svelte';
import Section from '$lib/components/Section.svelte';
import Table from '$lib/components/Table.svelte';
import Warning from '$lib/components/Warning.svelte';
import StepList from '$lib/components/StepList.svelte';
import MaterialsList from '$lib/components/MaterialsList.svelte';
import ConfigPlaceholder from '$lib/components/ConfigPlaceholder.svelte';
import Checklist from '$lib/components/Checklist.svelte';
import { materials } from '$lib/data/materials';
import { Check, ArrowRight } from 'lucide-svelte';
</script>
<Hero />
<!-- Overview Section -->
<Section id="overview" title={$t.overview.title} subtitle={$t.overview.subtitle}>
<div class="grid md:grid-cols-2 gap-6">
<div class="rounded-lg border p-6 bg-bg-dark-secondary/50 border-bg-dark-tertiary [.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
<h4 class="font-semibold mb-4 text-text-dark [.light_&]:text-text-light">Features</h4>
<ul class="space-y-2">
{#each $t.overview.features as feature}
<li class="flex items-start gap-2 text-sm text-text-dark [.light_&]:text-text-light">
<Check class="w-4 h-4 text-success flex-shrink-0 mt-0.5" />
<span>{feature}</span>
</li>
{/each}
</ul>
</div>
<div class="rounded-lg border p-6 bg-accent/5 border-accent/20">
<h4 class="font-semibold mb-4 text-accent">{$t.overview.conclusion.title}</h4>
<ul class="space-y-2 mb-4">
{#each $t.overview.conclusion.points as point}
<li class="flex items-center gap-2 text-sm text-text-dark [.light_&]:text-text-light">
<ArrowRight class="w-4 h-4 text-accent flex-shrink-0" />
<span>{point}</span>
</li>
{/each}
</ul>
<p class="text-sm text-text-dark-muted [.light_&]:text-text-light-muted">
{$t.overview.conclusion.benefits}
</p>
</div>
</div>
</Section>
<!-- Materials Section -->
<Section id="materials" title={$t.materials.title} subtitle={$t.materials.subtitle}>
<MaterialsList {materials} />
</Section>
<!-- Standards Section -->
<Section id="standards" title={$t.standards.title} subtitle={$t.standards.subtitle}>
<div class="grid md:grid-cols-2 gap-6">
<div>
<h4 class="font-semibold mb-3 text-text-dark [.light_&]:text-text-light">{$t.standards.acSection}</h4>
<Table
columns={[
{ key: 'voltage', header: $t.standards.voltage },
{ key: 'color', header: $t.standards.color }
]}
rows={[
{ voltage: 'L', color: 'Braun / Brown' },
{ voltage: 'N', color: 'Blau / Blue' },
{ voltage: 'PE', color: 'Gelb-Grün / Yellow-Green' }
]}
/>
</div>
<div>
<h4 class="font-semibold mb-3 text-text-dark [.light_&]:text-text-light">{$t.standards.dcSection}</h4>
<Table
columns={[
{ key: 'voltage', header: $t.standards.voltage },
{ key: 'color', header: $t.standards.color }
]}
rows={[
{ voltage: '12V', color: 'Rot / Red + Schwarz / Black' },
{ voltage: '5V', color: 'Orange + Schwarz / Black' },
{ voltage: '3.3V', color: 'ESP32 onboard' }
]}
/>
</div>
</div>
</Section>
<!-- Mechanical Build Section -->
<Section id="mechanical" title={$t.mechanical.title}>
<div class="space-y-6">
<StepList title={$t.mechanical.tableAndEnclosure.title} steps={$t.mechanical.tableAndEnclosure.steps} />
<StepList title={$t.mechanical.door.title} steps={$t.mechanical.door.steps} />
</div>
</Section>
<!-- Zones Section -->
<Section id="zones" title={$t.zones.title} subtitle={$t.zones.subtitle}>
<div class="grid md:grid-cols-2 gap-6">
<div class="rounded-lg border p-6 bg-danger/5 border-danger/20">
<h4 class="font-semibold mb-4 text-danger">{$t.zones.acZone.title}</h4>
<ul class="space-y-2 mb-4">
{#each $t.zones.acZone.components as component}
<li class="text-sm text-text-dark [.light_&]:text-text-light">{component}</li>
{/each}
</ul>
<p class="text-xs text-text-dark-muted [.light_&]:text-text-light-muted italic">
{$t.zones.acZone.note}
</p>
</div>
<div class="rounded-lg border p-6 bg-success/5 border-success/20">
<h4 class="font-semibold mb-4 text-success">{$t.zones.dcZone.title}</h4>
<ul class="space-y-2 mb-4">
{#each $t.zones.dcZone.components as component}
<li class="text-sm text-text-dark [.light_&]:text-text-light">{component}</li>
{/each}
</ul>
<p class="text-xs text-text-dark-muted [.light_&]:text-text-light-muted italic">
{$t.zones.dcZone.note}
</p>
</div>
</div>
</Section>
<!-- AC Wiring Section -->
<Section id="ac-wiring" title={$t.acWiring.title}>
<Warning type="danger" title={$t.common.warning}>
<p>{$t.acWiring.warning}</p>
</Warning>
<div class="space-y-6 mt-6">
<StepList title={$t.acWiring.iecEntry.title} steps={$t.acWiring.iecEntry.steps} />
<StepList title={$t.acWiring.wagoHolder.title} steps={$t.acWiring.wagoHolder.steps} />
<div class="rounded-lg border p-4 bg-bg-dark-secondary/50 border-bg-dark-tertiary [.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
<h4 class="font-semibold mb-3 text-text-dark [.light_&]:text-text-light">{$t.acWiring.iecToWago.title}</h4>
<Table
columns={[
{ key: 'from', header: 'IEC C14' },
{ key: 'to', header: 'WAGO' }
]}
rows={[
{ from: 'L (Braun)', to: 'WAGO L' },
{ from: 'N (Blau)', to: 'WAGO N' },
{ from: 'PE (Gelb-Grün)', to: 'WAGO PE' }
]}
/>
<Warning type="info">
<p>{$t.acWiring.iecToWago.note}</p>
</Warning>
</div>
<StepList title={$t.acWiring.printer.title} steps={$t.acWiring.printer.steps} />
<Warning type="tip">
<p>{$t.acWiring.printer.note}</p>
</Warning>
<div class="rounded-lg border p-4 bg-bg-dark-secondary/50 border-bg-dark-tertiary [.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
<h4 class="font-semibold mb-3 text-text-dark [.light_&]:text-text-light">{$t.acWiring.psu.title}</h4>
<p class="text-sm text-text-dark-muted [.light_&]:text-text-light-muted">{$t.acWiring.psu.note}</p>
</div>
</div>
</Section>
<!-- DC Distribution Section -->
<Section id="dc-distribution" title={$t.dcDistribution.title}>
<div class="space-y-6">
<div class="rounded-lg border p-4 bg-bg-dark-secondary/50 border-bg-dark-tertiary [.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
<h4 class="font-semibold mb-3 text-text-dark [.light_&]:text-text-light">{$t.dcDistribution.wagoBuses.title}</h4>
<p class="text-sm mb-3 text-text-dark-muted [.light_&]:text-text-light-muted">{$t.dcDistribution.wagoBuses.description}</p>
<ul class="space-y-1 mb-3">
{#each $t.dcDistribution.wagoBuses.buses as bus}
<li class="text-sm text-text-dark [.light_&]:text-text-light">{bus}</li>
{/each}
</ul>
<p class="text-xs text-accent">{$t.dcDistribution.wagoBuses.recommendation}</p>
</div>
<div class="rounded-lg border p-4 bg-bg-dark-secondary/50 border-bg-dark-tertiary [.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
<h4 class="font-semibold mb-3 text-text-dark [.light_&]:text-text-light">{$t.dcDistribution.psuToWago.title}</h4>
<Table
columns={[
{ key: 'psu', header: 'PSU' },
{ key: 'wago', header: 'WAGO' }
]}
rows={[
{ psu: '+12V', wago: '12V+ (rot)' },
{ psu: '0V', wago: 'GND (schwarz)' },
{ psu: '+5V', wago: '5V+ (orange)' }
]}
/>
<Warning type="info">
<p>{$t.dcDistribution.psuToWago.note}</p>
</Warning>
</div>
</div>
</Section>
<!-- LEDs Section -->
<Section id="leds" title={$t.leds.title}>
<div class="grid md:grid-cols-2 gap-6">
<div class="rounded-lg border p-4 bg-bg-dark-secondary/50 border-bg-dark-tertiary [.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
<h4 class="font-semibold mb-3 text-text-dark [.light_&]:text-text-light">{$t.leds.mechanics.title}</h4>
<p class="text-sm text-text-dark-muted [.light_&]:text-text-light-muted">{$t.leds.mechanics.description}</p>
</div>
<div class="rounded-lg border p-4 bg-bg-dark-secondary/50 border-bg-dark-tertiary [.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
<h4 class="font-semibold mb-3 text-text-dark [.light_&]:text-text-light">{$t.leds.electrical.title}</h4>
<p class="text-sm mb-2 text-text-dark-muted [.light_&]:text-text-light-muted">{$t.leds.electrical.description}</p>
<p class="text-sm mb-1 text-text-dark [.light_&]:text-text-light">{$t.leds.electrical.mosfet}</p>
<p class="text-xs text-accent">{$t.leds.electrical.pwm}</p>
</div>
</div>
</Section>
<!-- Fans Section -->
<Section id="fans" title={$t.fans.title}>
<div class="grid md:grid-cols-2 gap-6">
<div class="rounded-lg border p-4 bg-bg-dark-secondary/50 border-bg-dark-tertiary [.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
<h4 class="font-semibold mb-3 text-text-dark [.light_&]:text-text-light">{$t.fans.layout.title}</h4>
<ul class="space-y-1">
{#each $t.fans.layout.items as item}
<li class="text-sm text-text-dark [.light_&]:text-text-light">{item}</li>
{/each}
</ul>
</div>
<div class="rounded-lg border p-4 bg-bg-dark-secondary/50 border-bg-dark-tertiary [.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
<h4 class="font-semibold mb-3 text-text-dark [.light_&]:text-text-light">{$t.fans.electrical.title}</h4>
<p class="text-sm mb-2 text-text-dark-muted [.light_&]:text-text-light-muted">{$t.fans.electrical.description}</p>
<p class="text-sm mb-1 text-text-dark [.light_&]:text-text-light">{$t.fans.electrical.mosfet}</p>
<p class="text-xs text-accent">{$t.fans.electrical.pwm}</p>
</div>
</div>
</Section>
<!-- ESP32 Section -->
<Section id="esp32" title={$t.esp32.title}>
<div class="grid md:grid-cols-3 gap-6">
<div class="rounded-lg border p-4 bg-bg-dark-secondary/50 border-bg-dark-tertiary [.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
<h4 class="font-semibold mb-3 text-text-dark [.light_&]:text-text-light">{$t.esp32.power.title}</h4>
<p class="text-sm text-text-dark-muted [.light_&]:text-text-light-muted">{$t.esp32.power.description}</p>
</div>
<div class="rounded-lg border p-4 bg-bg-dark-secondary/50 border-bg-dark-tertiary [.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
<h4 class="font-semibold mb-3 text-text-dark [.light_&]:text-text-light">{$t.esp32.voltage.title}</h4>
<p class="text-sm text-text-dark-muted [.light_&]:text-text-light-muted">{$t.esp32.voltage.description}</p>
</div>
<div class="rounded-lg border p-4 bg-bg-dark-secondary/50 border-bg-dark-tertiary [.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
<h4 class="font-semibold mb-3 text-text-dark [.light_&]:text-text-light">{$t.esp32.bme280.title}</h4>
<p class="text-sm text-text-dark-muted [.light_&]:text-text-light-muted">{$t.esp32.bme280.description}</p>
</div>
</div>
<div class="mt-6">
<Table
caption="BME280 Pinout"
columns={[
{ key: 'bme', header: 'BME280' },
{ key: 'esp', header: 'ESP32' }
]}
rows={[
{ bme: 'VCC', esp: '3V3' },
{ bme: 'GND', esp: 'GND' },
{ bme: 'SDA', esp: 'GPIO 21' },
{ bme: 'SCL', esp: 'GPIO 22' }
]}
/>
</div>
</Section>
<!-- Panel Section -->
<Section id="panel" title={$t.panel.title}>
<div class="rounded-lg border p-4 bg-bg-dark-secondary/50 border-bg-dark-tertiary [.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
<p class="text-sm mb-3 text-text-dark-muted [.light_&]:text-text-light-muted">{$t.panel.description}</p>
<h4 class="font-semibold mb-2 text-text-dark [.light_&]:text-text-light">Functions:</h4>
<ul class="space-y-1">
{#each $t.panel.functions as func}
<li class="text-sm text-text-dark [.light_&]:text-text-light">{func}</li>
{/each}
</ul>
</div>
</Section>
<!-- Software Section -->
<Section id="software" title={$t.software.title}>
<div class="grid md:grid-cols-2 gap-6">
<div class="rounded-lg border p-4 bg-bg-dark-secondary/50 border-bg-dark-tertiary [.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
<h4 class="font-semibold mb-3 text-text-dark [.light_&]:text-text-light">{$t.software.esphome.title}</h4>
<ul class="space-y-1">
{#each $t.software.esphome.features as feature}
<li class="text-sm text-text-dark [.light_&]:text-text-light">{feature}</li>
{/each}
</ul>
</div>
<div class="rounded-lg border p-4 bg-bg-dark-secondary/50 border-bg-dark-tertiary [.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
<h4 class="font-semibold mb-3 text-text-dark [.light_&]:text-text-light">{$t.software.homeAssistant.title}</h4>
<ul class="space-y-1">
{#each $t.software.homeAssistant.features as feature}
<li class="text-sm text-text-dark [.light_&]:text-text-light">{feature}</li>
{/each}
</ul>
</div>
</div>
<!-- Config Placeholders -->
<div class="mt-8 space-y-6">
<h3 class="text-xl font-semibold text-text-dark [.light_&]:text-text-light">{$t.config.title}</h3>
<ConfigPlaceholder
filename={$t.config.esphome.filename}
title={$t.config.esphome.title}
placeholder={$t.config.esphome.placeholder}
/>
<ConfigPlaceholder
filename={$t.config.homeAssistant.filename}
title={$t.config.homeAssistant.title}
placeholder={$t.config.homeAssistant.placeholder}
/>
</div>
</Section>
<!-- Checklist Section -->
<Section id="checklist" title={$t.checklist.title} subtitle={$t.checklist.subtitle}>
<Checklist
sections={[
{ title: $t.checklist.electrical.title, items: $t.checklist.electrical.items },
{ title: $t.checklist.functional.title, items: $t.checklist.functional.items }
]}
/>
</Section>
<!-- Extensions Section -->
<Section id="extensions" title={$t.extensions.title} subtitle={$t.extensions.subtitle}>
<div class="rounded-lg border p-6 bg-bg-dark-secondary/50 border-bg-dark-tertiary [.light_&]:bg-bg-light-secondary/50 [.light_&]:border-bg-light-tertiary">
<ul class="grid md:grid-cols-2 gap-3">
{#each $t.extensions.items as item}
<li class="flex items-center gap-2 text-text-dark [.light_&]:text-text-light">
<span class="w-2 h-2 rounded-full bg-accent"></span>
{item}
</li>
{/each}
</ul>
</div>
</Section>
<!-- Footer -->
<footer class="py-12 text-center border-t border-bg-dark-tertiary [.light_&]:border-bg-light-tertiary">
<p class="text-sm text-text-dark-muted [.light_&]:text-text-light-muted">
DruckWerk - Built with Svelte & Tailwind CSS
</p>
</footer>

8
static/favicon.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#f97316"/>
<rect x="4" y="7" width="24" height="18" rx="1.5" fill="none" stroke="#fff" stroke-width="2"/>
<line x1="4" y1="12" x2="28" y2="12" stroke="#fff" stroke-width="1.5"/>
<circle cx="8" cy="9.5" r="1" fill="#fff"/>
<circle cx="11" cy="9.5" r="1" fill="#fff"/>
<rect x="10" y="16" width="12" height="6" rx="1" fill="none" stroke="#fff" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 485 B

18
svelte.config.js Normal file
View File

@@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: 'index.html',
precompress: false,
strict: true
})
}
};
export default config;

14
tsconfig.json Normal file
View File

@@ -0,0 +1,14 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
}

7
vite.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()]
});