diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 00000000..c6b0f27c
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,13 @@
+# Dependencies
+node_modules
+
+# Build outputs
+dist
+build
+*.min.js
+
+# Package files
+package-lock.json
+yarn.lock
+pnpm-lock.yaml
+
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 00000000..54736c79
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,10 @@
+{
+ "semi": false,
+ "singleQuote": true,
+ "tabWidth": 2,
+ "trailingComma": "es5",
+ "printWidth": 100,
+ "arrowParens": "avoid",
+ "endOfLine": "lf"
+}
+
diff --git a/README.md b/README.md
index 200f4282..1539725c 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,245 @@
-# Portfolio
+# π Frontend Portfolio Website
+
+## π Live Site
+
+**[technigo.daniellauding.se](https://technigo.daniellauding.se)**
+
+> This is my **Technigo frontend development portfolio** showcasing my JavaScript and React skills.
+
+---
+
+## π About
+
+Hi! I'm Daniel, and this is my **Technigo frontend portfolio** where I show off my projects and tell you about myself!
+
+### Other Portfolios
+
+- **π¨ Design Portfolio:** [daniellauding.se](https://www.daniellauding.se) - My main design portfolio
+- **π Recent Projects (Figma):** [View in Figma](https://www.figma.com/proto/ITcLm3ciPq4G5qkKP6q1d9/instinctly-selected-work?page-id=624%3A457&node-id=624-458&p=f&viewport=-9839%2C-1808%2C0.66&t=aXOv3ONToAEWFkFK-1&scaling=min-zoom&content-scaling=fixed&starting-point-node-id=624%3A458) - Recent selected work
+
+---
+
+## π― What This Website Does
+
+This is like my digital business card! It shows:
+
+- **Who I am** - A person who loves building websites
+- **What I've built** - Cool projects I've made
+- **My thoughts** - Things I think about when making websites
+- **How to contact me** - So we can chat about making cool stuff together!
+
+---
+
+## π§Έ Why I Built My Own Design Library
+
+### Like LEGO Blocks!
+
+You know how LEGO blocks can build anything? I made my own "website LEGO blocks" because:
+
+### π¨ It's Like Having My Own Crayon Box
+
+- Instead of using someone else's colors, I made my own!
+- Every button, text, and picture looks exactly how I want
+- It's like having a magic crayon that always draws the same shade of blue
+
+### π§ Building Blocks That Fit Together
+
+- I made pieces like ``, ``, and ``
+- They all work together like puzzle pieces
+- When I want to change something, I only need to change it in one place!
+
+### π It Makes Building Websites Super Fast
+
+- Instead of drawing a new button every time, I just use my `` block
+- It's like having a stamp instead of writing the same word over and over
+
+---
+
+## π My Thoughts About Code
+
+### Code Should Be Like a Good Story
+
+- Easy to read and understand
+- Each part has a clear purpose
+- Anyone should be able to follow along
+
+### Keep Things Simple
+
+- Don't use big fancy words when small ones work
+- Make one thing do one job really well
+- Clean up your toys (code) when you're done!
+
+### Be Kind to Future You
+
+- Write code like you're leaving notes for a friend
+- Tomorrow-you will thank today-you for being clear
+- Comments are like sticky notes that explain tricky parts
+
+---
+
+## π Planning with Plotta
+
+Before building this website, I used **[Plotta](https://app.plotta.io)** to plan out the entire project!
+
+### What is Plotta?
+
+**Plotta is my own project** - a planning tool I created with AI assistance (Claude) that helps organize projects using digital sticky notes. It's like having a virtual whiteboard where you can plan, organize, and visualize complex projects before diving into code.
+
+### How I Used It
+
+Plotta helped me:
+- **Organize my thoughts** - Structure all the components and features
+- **Visualize the architecture** - See how everything would fit together
+- **Plan the user flow** - Map out the entire user experience
+- **Break down tasks** - Organize development into manageable pieces
+
+### Explore the Planning Process
+
+π **View the Project Plan:** [Plotta Project](https://app.plotta.io/project/43f72570-e844-4280-9865-c3e37dc1f60d)
+π **Password:** `technigo`
+
+> π‘ **Tip:** Feel free to register and explore the planning process yourself! It's a great tool for organizing complex projects, and you can see exactly how I planned this portfolio before building it.
+
+---
+
+## ποΈ How I Built This
+
+### The Foundation (Like Building a House)
+
+1. **React** - The main tool that makes everything work
+2. **TypeScript** - Helps catch mistakes before they happen
+3. **Styled Components** - Makes everything look pretty
+4. **My Design System** - All my LEGO blocks working together
+
+### The Special Features
+
+- **Accessibility First** - Everyone can use my website, no matter how they browse
+- **Mobile Friendly** - Works great on phones and big computers
+- **Fast Loading** - Nobody likes waiting for slow websites!
+- **Dark Mode** - Easy on your eyes when browsing at night π
+- **Interactive Design System** - Browse all components in the StyleGuide
+
+---
+
+## π¨ Why Simple Design Works
+
+Think of your favorite toy. It's probably not the one with a million buttons and lights that confuse you. It's the simple one that just works perfectly.
+
+That's what I did here:
+
+- **Clean colors** that are easy on your eyes
+- **Clear text** that's easy to read
+- **Simple navigation** so you never get lost
+- **Fast loading** because waiting is boring
+
+---
+
+## π€ What Makes Good Code
+
+### The Secret Recipe
+
+#### Like Organizing Your Room
+
+- Put similar things together
+- Give everything a clear name
+- Clean up regularly
+- Make it easy to find stuff later
+
+#### Like Writing a Letter
+
+- Use clear, simple words
+- One idea per paragraph (function)
+- Check your spelling (no bugs!)
+- Make it nice to read
+
+#### Like Being a Good Friend
+
+- Be consistent and reliable
+- Don't surprise people in bad ways
+- Help others understand what you're doing
+- Share your toys (code) with others
+
+---
+
+## π± What I Learned
+
+Building this taught me that:
+
+- Small, simple pieces can build amazing things
+- Planning first saves time later (thanks Plotta!)
+- Making it accessible means everyone can enjoy it
+- Clean code is like a gift to your future self
+
+---
+
+## π Quick Start
+
+```bash
+# Install dependencies
+npm install
+
+# Start development server
+npm run dev
+
+# Build for production
+npm run build
+
+# Preview production build
+npm run preview
+```
+
+---
+
+## π Project Structure
+
+```
+src/
+βββ components/ # Reusable UI components
+βββ views/ # Page components
+βββ styles/ # Global styles and themes
+βββ utils/ # Helper functions
+βββ data/ # Static data (projects, skills, etc.)
+```
+
+---
+
+## π¨ Features
+
+- **Component-Based Architecture** - Modular, reusable components
+- **TypeScript** - Type-safe development
+- **Styled Components** - CSS-in-JS styling
+- **Dark/Light Theme** - Toggle between themes
+- **Responsive Design** - Mobile-first approach
+- **Accessibility** - WCAG 2.1 AA compliant
+- **SEO Optimized** - Meta tags and semantic HTML
+- **Performance** - Lazy loading and optimized assets
+- **Scroll Animations** - Smooth fade-in animations with animate.css
+- **Open Graph Tags** - Beautiful social media previews
+
+---
+
+## π Tech Stack
+
+- **React 18** - UI library
+- **TypeScript** - Type safety
+- **Vite** - Build tool
+- **Styled Components** - Styling
+- **React Router** - Routing
+- **Animate.css** - Scroll animations
+- **ESLint & Prettier** - Code quality
+
+---
+
+## π License
+
+This project is open source and available for learning purposes.
+
+---
+
+
+
+**Made with β€οΈ by Daniel**
+
+*A person who thinks websites should be fast, pretty, and work for everyone!*
+
+
\ No newline at end of file
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 00000000..d9f13124
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,36 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+
+export default tseslint.config(
+ { ignores: ['dist', 'node_modules'] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ['**/*.{ts,tsx,js,jsx}'],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ // Relaxed rules for beginners
+ '@typescript-eslint/no-explicit-any': 'warn',
+ '@typescript-eslint/no-unused-vars': ['warn', {
+ argsIgnorePattern: '^_',
+ varsIgnorePattern: '^_'
+ }],
+ '@typescript-eslint/no-empty-object-type': 'off', // Allow empty interfaces for type extensions
+ },
+ },
+)
+
diff --git a/index.html b/index.html
index 6676fb2d..059020ef 100644
--- a/index.html
+++ b/index.html
@@ -2,9 +2,72 @@
-
- Portfolio
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Daniel Lauding β Portfolio
diff --git a/package.json b/package.json
index 48911600..e1a9173e 100644
--- a/package.json
+++ b/package.json
@@ -7,11 +7,17 @@
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,json}\"",
+ "format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx,css,json}\"",
"preview": "vite preview"
},
"dependencies": {
+ "animate.css": "^4.1.1",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "react-router-dom": "^7.9.6",
+ "styled-components": "^6.1.19"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
@@ -19,9 +25,12 @@
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0",
+ "eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
+ "prettier": "^3.6.2",
+ "typescript-eslint": "^8.48.0",
"vite": "^6.2.0"
}
}
diff --git a/public/_redirects b/public/_redirects
new file mode 100644
index 00000000..f8243379
--- /dev/null
+++ b/public/_redirects
@@ -0,0 +1 @@
+/* /index.html 200
\ No newline at end of file
diff --git a/public/articles/thumb_01.png b/public/articles/thumb_01.png
new file mode 100644
index 00000000..87178f89
Binary files /dev/null and b/public/articles/thumb_01.png differ
diff --git a/public/articles/thumb_02.png b/public/articles/thumb_02.png
new file mode 100644
index 00000000..7de2ec46
Binary files /dev/null and b/public/articles/thumb_02.png differ
diff --git a/public/avatar.png b/public/avatar.png
new file mode 100644
index 00000000..e32043ed
Binary files /dev/null and b/public/avatar.png differ
diff --git a/public/favicon/android-icon-144x144.png b/public/favicon/android-icon-144x144.png
new file mode 100644
index 00000000..e513f736
Binary files /dev/null and b/public/favicon/android-icon-144x144.png differ
diff --git a/public/favicon/android-icon-192x192.png b/public/favicon/android-icon-192x192.png
new file mode 100644
index 00000000..281dc58e
Binary files /dev/null and b/public/favicon/android-icon-192x192.png differ
diff --git a/public/favicon/android-icon-36x36.png b/public/favicon/android-icon-36x36.png
new file mode 100644
index 00000000..63db966d
Binary files /dev/null and b/public/favicon/android-icon-36x36.png differ
diff --git a/public/favicon/android-icon-48x48.png b/public/favicon/android-icon-48x48.png
new file mode 100644
index 00000000..cdb56965
Binary files /dev/null and b/public/favicon/android-icon-48x48.png differ
diff --git a/public/favicon/android-icon-72x72.png b/public/favicon/android-icon-72x72.png
new file mode 100644
index 00000000..1e899dba
Binary files /dev/null and b/public/favicon/android-icon-72x72.png differ
diff --git a/public/favicon/android-icon-96x96.png b/public/favicon/android-icon-96x96.png
new file mode 100644
index 00000000..63c87de7
Binary files /dev/null and b/public/favicon/android-icon-96x96.png differ
diff --git a/public/favicon/apple-icon-114x114.png b/public/favicon/apple-icon-114x114.png
new file mode 100644
index 00000000..9ee819e0
Binary files /dev/null and b/public/favicon/apple-icon-114x114.png differ
diff --git a/public/favicon/apple-icon-120x120.png b/public/favicon/apple-icon-120x120.png
new file mode 100644
index 00000000..1f7c8623
Binary files /dev/null and b/public/favicon/apple-icon-120x120.png differ
diff --git a/public/favicon/apple-icon-144x144.png b/public/favicon/apple-icon-144x144.png
new file mode 100644
index 00000000..e513f736
Binary files /dev/null and b/public/favicon/apple-icon-144x144.png differ
diff --git a/public/favicon/apple-icon-152x152.png b/public/favicon/apple-icon-152x152.png
new file mode 100644
index 00000000..6598d9c7
Binary files /dev/null and b/public/favicon/apple-icon-152x152.png differ
diff --git a/public/favicon/apple-icon-180x180.png b/public/favicon/apple-icon-180x180.png
new file mode 100644
index 00000000..7ebd0bbe
Binary files /dev/null and b/public/favicon/apple-icon-180x180.png differ
diff --git a/public/favicon/apple-icon-57x57.png b/public/favicon/apple-icon-57x57.png
new file mode 100644
index 00000000..81067e63
Binary files /dev/null and b/public/favicon/apple-icon-57x57.png differ
diff --git a/public/favicon/apple-icon-60x60.png b/public/favicon/apple-icon-60x60.png
new file mode 100644
index 00000000..94a2263b
Binary files /dev/null and b/public/favicon/apple-icon-60x60.png differ
diff --git a/public/favicon/apple-icon-72x72.png b/public/favicon/apple-icon-72x72.png
new file mode 100644
index 00000000..1e899dba
Binary files /dev/null and b/public/favicon/apple-icon-72x72.png differ
diff --git a/public/favicon/apple-icon-76x76.png b/public/favicon/apple-icon-76x76.png
new file mode 100644
index 00000000..a75f60ff
Binary files /dev/null and b/public/favicon/apple-icon-76x76.png differ
diff --git a/public/favicon/apple-icon-precomposed.png b/public/favicon/apple-icon-precomposed.png
new file mode 100644
index 00000000..8ac7a5af
Binary files /dev/null and b/public/favicon/apple-icon-precomposed.png differ
diff --git a/public/favicon/apple-icon.png b/public/favicon/apple-icon.png
new file mode 100644
index 00000000..8ac7a5af
Binary files /dev/null and b/public/favicon/apple-icon.png differ
diff --git a/public/favicon/browserconfig.xml b/public/favicon/browserconfig.xml
new file mode 100644
index 00000000..e33428ce
--- /dev/null
+++ b/public/favicon/browserconfig.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+ #ffffff
+
+
+
\ No newline at end of file
diff --git a/public/favicon/favicon-16x16.png b/public/favicon/favicon-16x16.png
new file mode 100644
index 00000000..e5ddde6b
Binary files /dev/null and b/public/favicon/favicon-16x16.png differ
diff --git a/public/favicon/favicon-32x32.png b/public/favicon/favicon-32x32.png
new file mode 100644
index 00000000..1aeb6c6e
Binary files /dev/null and b/public/favicon/favicon-32x32.png differ
diff --git a/public/favicon/favicon-96x96.png b/public/favicon/favicon-96x96.png
new file mode 100644
index 00000000..63c87de7
Binary files /dev/null and b/public/favicon/favicon-96x96.png differ
diff --git a/public/favicon/favicon.ico b/public/favicon/favicon.ico
new file mode 100644
index 00000000..6111f253
Binary files /dev/null and b/public/favicon/favicon.ico differ
diff --git a/public/favicon/manifest.json b/public/favicon/manifest.json
new file mode 100644
index 00000000..39545a8a
--- /dev/null
+++ b/public/favicon/manifest.json
@@ -0,0 +1,45 @@
+{
+ "name": "Daniel Lauding Portfolio",
+ "short_name": "Portfolio",
+ "icons": [
+ {
+ "src": "/favicon/android-icon-36x36.png",
+ "sizes": "36x36",
+ "type": "image/png",
+ "density": "0.75"
+ },
+ {
+ "src": "/favicon/android-icon-48x48.png",
+ "sizes": "48x48",
+ "type": "image/png",
+ "density": "1.0"
+ },
+ {
+ "src": "/favicon/android-icon-72x72.png",
+ "sizes": "72x72",
+ "type": "image/png",
+ "density": "1.5"
+ },
+ {
+ "src": "/favicon/android-icon-96x96.png",
+ "sizes": "96x96",
+ "type": "image/png",
+ "density": "2.0"
+ },
+ {
+ "src": "/favicon/android-icon-144x144.png",
+ "sizes": "144x144",
+ "type": "image/png",
+ "density": "3.0"
+ },
+ {
+ "src": "/favicon/android-icon-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "density": "4.0"
+ }
+ ],
+ "theme_color": "#36816B",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
\ No newline at end of file
diff --git a/public/favicon/ms-icon-144x144.png b/public/favicon/ms-icon-144x144.png
new file mode 100644
index 00000000..e513f736
Binary files /dev/null and b/public/favicon/ms-icon-144x144.png differ
diff --git a/public/favicon/ms-icon-150x150.png b/public/favicon/ms-icon-150x150.png
new file mode 100644
index 00000000..9fd1a5d8
Binary files /dev/null and b/public/favicon/ms-icon-150x150.png differ
diff --git a/public/favicon/ms-icon-310x310.png b/public/favicon/ms-icon-310x310.png
new file mode 100644
index 00000000..f04f597f
Binary files /dev/null and b/public/favicon/ms-icon-310x310.png differ
diff --git a/public/favicon/ms-icon-70x70.png b/public/favicon/ms-icon-70x70.png
new file mode 100644
index 00000000..d22171b0
Binary files /dev/null and b/public/favicon/ms-icon-70x70.png differ
diff --git a/public/logo.ico b/public/logo.ico
new file mode 100644
index 00000000..626d5d74
Binary files /dev/null and b/public/logo.ico differ
diff --git a/public/projects/thumb_accessibility.png b/public/projects/thumb_accessibility.png
new file mode 100644
index 00000000..84370cca
Binary files /dev/null and b/public/projects/thumb_accessibility.png differ
diff --git a/public/projects/thumb_business.png b/public/projects/thumb_business.png
new file mode 100644
index 00000000..b52d0fb3
Binary files /dev/null and b/public/projects/thumb_business.png differ
diff --git a/public/projects/thumb_edenred.png b/public/projects/thumb_edenred.png
new file mode 100644
index 00000000..775edf94
Binary files /dev/null and b/public/projects/thumb_edenred.png differ
diff --git a/public/projects/thumb_energiforsk.png b/public/projects/thumb_energiforsk.png
new file mode 100644
index 00000000..e3c4222e
Binary files /dev/null and b/public/projects/thumb_energiforsk.png differ
diff --git a/public/projects/thumb_gavlegardarna.png b/public/projects/thumb_gavlegardarna.png
new file mode 100644
index 00000000..5e8fd755
Binary files /dev/null and b/public/projects/thumb_gavlegardarna.png differ
diff --git a/public/projects/thumb_hundra.png b/public/projects/thumb_hundra.png
new file mode 100644
index 00000000..a9438171
Binary files /dev/null and b/public/projects/thumb_hundra.png differ
diff --git a/public/projects/thumb_ikea_01.png b/public/projects/thumb_ikea_01.png
new file mode 100644
index 00000000..1714ac07
Binary files /dev/null and b/public/projects/thumb_ikea_01.png differ
diff --git a/public/projects/thumb_ikea_02.png b/public/projects/thumb_ikea_02.png
new file mode 100644
index 00000000..ed8bdfa4
Binary files /dev/null and b/public/projects/thumb_ikea_02.png differ
diff --git a/public/projects/thumb_recipe.png b/public/projects/thumb_recipe.png
new file mode 100644
index 00000000..180c71e2
Binary files /dev/null and b/public/projects/thumb_recipe.png differ
diff --git a/public/projects/thumb_swe_hockey.png b/public/projects/thumb_swe_hockey.png
new file mode 100644
index 00000000..69fe2fa1
Binary files /dev/null and b/public/projects/thumb_swe_hockey.png differ
diff --git a/public/projects/thumb_tinychat.png b/public/projects/thumb_tinychat.png
new file mode 100644
index 00000000..b97034ae
Binary files /dev/null and b/public/projects/thumb_tinychat.png differ
diff --git a/public/projects/thumb_ttela.png b/public/projects/thumb_ttela.png
new file mode 100644
index 00000000..4d55f56f
Binary files /dev/null and b/public/projects/thumb_ttela.png differ
diff --git a/public/projects/thumb_weather.png b/public/projects/thumb_weather.png
new file mode 100644
index 00000000..82bff190
Binary files /dev/null and b/public/projects/thumb_weather.png differ
diff --git a/public/projects/thumb_xmas.png b/public/projects/thumb_xmas.png
new file mode 100644
index 00000000..517fb46e
Binary files /dev/null and b/public/projects/thumb_xmas.png differ
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 00000000..7b4d21c6
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,5 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://your-domain.com/sitemap.xml
+
diff --git a/public/vite.svg b/public/vite.svg
deleted file mode 100644
index e7b8dfb1..00000000
--- a/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/pull_request_template.md b/pull_request_template.md
index 4263c7e8..1e2b2712 100644
--- a/pull_request_template.md
+++ b/pull_request_template.md
@@ -1 +1,3 @@
-Please include a link to your Figma design and a Netlify link.
\ No newline at end of file
+https://www.figma.com/design/BcGhRidulltesBVWKSknBG/Portfolio-designs--Copy-?node-id=0-1&t=h9CvDUrhv9waDkg3-1
+
+https://technigo.daniellauding.se
\ No newline at end of file
diff --git a/src/App.jsx b/src/App.jsx
index a161d8d3..8f89557a 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,8 +1,29 @@
+import { lazy, Suspense } from 'react'
+import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
+import { ThemeProvider } from 'styled-components'
+import { ThemeProvider as CustomThemeProvider } from '@/contexts/ThemeContext'
+import { ThemeToggle } from '@/components/ThemeToggle'
+import { ScrollIndicator } from '@/components/ScrollIndicator'
+import { theme } from '@/theme'
+import { Home } from '@/views/Home'
+
+const StyleGuide = lazy(() => import('@/views/StyleGuide').then(module => ({ default: module.StyleGuide })))
+
export const App = () => {
return (
- <>
- Portfolio
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatem, laborum! Maxime animi nostrum facilis distinctio neque labore consectetur beatae eum ipsum excepturi voluptatum, dicta repellendus incidunt fugiat, consequatur rem aperiam.
- >
+
+
+
+
+
+
+
+ } />
+ } />
+
+
+
+
+
)
-}
+}
\ No newline at end of file
diff --git a/src/components/Articles/ArticleCard.tsx b/src/components/Articles/ArticleCard.tsx
new file mode 100644
index 00000000..0ebad814
--- /dev/null
+++ b/src/components/Articles/ArticleCard.tsx
@@ -0,0 +1,83 @@
+import { Text, Title } from '@/components/Typography'
+import { Button } from '@/components/Button'
+import { useTheme } from '@/contexts/ThemeContext'
+import { ArticleCardProps } from './Articles.types'
+import {
+ ArticleCard as Card,
+ ArticleImage,
+ StyledImage,
+ ArticleContent,
+ ArticleDate,
+ ArticleFooter
+} from './Articles.styled'
+
+export const ArticleCard = ({ article, role, ...props }: ArticleCardProps) => {
+ const { theme } = useTheme()
+
+ if (!article) return null
+
+ const { title, excerpt, date, image, link } = article
+
+ const hasContent = title || excerpt || image
+ if (!hasContent) return null
+
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString)
+ return date.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short'
+ })
+ }
+
+ return (
+
+ {image && (
+
+
+
+ )}
+
+ {date && (
+
+
+ {formatDate(date)}
+
+
+ )}
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {excerpt && (
+
+ {excerpt}
+
+ )}
+
+
+
+ {link && (
+
+ Read article
+
+ )}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/Articles/Articles.styled.ts b/src/components/Articles/Articles.styled.ts
new file mode 100644
index 00000000..0e7dd830
--- /dev/null
+++ b/src/components/Articles/Articles.styled.ts
@@ -0,0 +1,114 @@
+import styled from 'styled-components'
+import { Image } from '@/components/Image'
+
+export const ArticlesContainer = styled.ul`
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-xl);
+ width: 100%;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0;
+ list-style: none;
+
+ ${props => props.theme.media.desktop} {
+ grid-template-columns: repeat(2, 1fr);
+ display: grid;
+ // padding: 0 var(--spacing-lg);
+ }
+`
+
+export const ArticleCard = styled.article`
+ background: transparent;
+ border-radius: var(--radius-none);
+ overflow: hidden;
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+ display: flex;
+ flex-direction: column;
+
+ &:hover {
+ transform: translateY(-4px);
+ }
+`
+
+export const ArticleImage = styled.div`
+ position: relative;
+ width: 100%;
+ max-width: 100%;
+ aspect-ratio: 408 / 280;
+ overflow: hidden;
+ border-left: 20px solid var(--section-articles-title-color);
+ border-bottom: 20px solid var(--section-articles-title-color);
+ ${props => props.theme.media.desktop} {
+ // width: 408px;
+ // max-width: 408px;
+ }
+`
+
+export const StyledImage = styled(Image)`
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+`;
+
+export const ArticleContent = styled.div`
+ padding: var(--spacing-none);
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-lg);
+`
+
+export const ArticleDate = styled.div`
+ background: var(--tag-bg-color);
+ color: var(--tag-text-color);
+ padding: 0 var(--spacing-xs) 0px var(--spacing-xs);
+ height: 24px;
+ border-radius: 0;
+ font-size: var(--text-md);
+ font-family: var(--text-font-family);
+ font-weight: var(--weight-medium);
+ width: fit-content;
+ margin-top: var(--spacing-lg);
+ margin-bottom: -16px;
+`
+
+export const ArticleFooter = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: var(--spacing-md);
+`
+
+export const ArticleReadTime = styled.span`
+ font-family: var(--text-font-family);
+ font-size: var(--text-sm);
+ color: var(--section-articles-text-color);
+ opacity: 0.7;
+`
+
+export const ArticleLink = styled.a`
+ display: inline-flex;
+ align-items: center;
+ gap: var(--spacing-md);
+ color: var(--text-color);
+ text-decoration: none;
+ font-family: var(--text-font-family);
+ font-size: var(--text-xl);
+ font-weight: var(--weight-medium);
+ padding: var(--spacing-xs) var(--spacing-md);
+ border-radius: 9999px;
+ transition: opacity 0.2s ease;
+ background: #fff;
+ height: 48px;
+
+ &:hover {
+ opacity: 0.7;
+ }
+
+ &:focus {
+ outline: 2px solid var(--section-articles-title-color);
+ outline-offset: 2px;
+ }
+`
\ No newline at end of file
diff --git a/src/components/Articles/Articles.tsx b/src/components/Articles/Articles.tsx
new file mode 100644
index 00000000..9e2997b0
--- /dev/null
+++ b/src/components/Articles/Articles.tsx
@@ -0,0 +1,35 @@
+import { Section } from '@/components/Section'
+import { ArticleCard } from './ArticleCard'
+import { ArticlesProps } from './Articles.types'
+import { ArticlesContainer } from './Articles.styled'
+
+export const Articles = ({ data }: ArticlesProps) => {
+ if (!data) return null
+
+ const articles = data?.articles
+ if (!articles || !Array.isArray(articles) || articles.length === 0) return null
+
+ return (
+
+
+ {articles.map((article) => (
+
+
+
+ ))}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/Articles/Articles.types.ts b/src/components/Articles/Articles.types.ts
new file mode 100644
index 00000000..c6082f3f
--- /dev/null
+++ b/src/components/Articles/Articles.types.ts
@@ -0,0 +1,23 @@
+export interface Article {
+ id: string
+ title: string
+ excerpt: string
+ image: string
+ date: string
+ readTime: string
+ link: string
+ tags: string[]
+}
+
+export interface ArticlesData {
+ articles: Article[]
+}
+
+export interface ArticlesProps {
+ data: ArticlesData
+}
+
+export interface ArticleCardProps {
+ article: Article
+ role?: string
+}
\ No newline at end of file
diff --git a/src/components/Articles/index.ts b/src/components/Articles/index.ts
new file mode 100644
index 00000000..ef326053
--- /dev/null
+++ b/src/components/Articles/index.ts
@@ -0,0 +1,3 @@
+export { Articles } from './Articles'
+export { ArticleCard } from './ArticleCard'
+export type { Article, ArticlesData, ArticlesProps, ArticleCardProps } from './Articles.types'
\ No newline at end of file
diff --git a/src/components/Button/Button.styled.ts b/src/components/Button/Button.styled.ts
new file mode 100644
index 00000000..ddc0fcb3
--- /dev/null
+++ b/src/components/Button/Button.styled.ts
@@ -0,0 +1,163 @@
+import styled from 'styled-components'
+import { ButtonVariant, ButtonSize } from './Button.types'
+
+interface StyledButtonProps {
+ $variant?: ButtonVariant
+ $size?: ButtonSize
+ $fullWidth?: boolean
+ $iconOnly?: boolean
+}
+
+export const StyledButton = styled.button`
+ font-family: ${props => props.theme.fonts.text};
+ font-weight: ${props => props.theme.weights.semibold};
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ transition: all 0.2s ease;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--spacing-sm);
+ text-decoration: none;
+ border: 2px solid transparent;
+
+ padding: var(--spacing-sm) var(--spacing-md);
+ font-size: ${props => props.theme.textSizes.md};
+
+ /* Small */
+ ${props => props.$size === 'sm' && `
+ padding: 6px 12px;
+ font-size: ${props.theme.textSizes.sm};
+ `}
+
+ /* Large */
+ ${props => props.$size === 'lg' && `
+ padding: 10px 20px;
+ font-size: ${props.theme.textSizes.lg};
+ `}
+
+ /* Default: primary */
+ background: ${props => props.theme.colors.primary};
+ color: var(--bg-color);
+ border-color: ${props => props.theme.colors.primary};
+
+ &:hover:not(:disabled) {
+ opacity: 0.9;
+ }
+
+ /* Secondary variant */
+ ${props => props.$variant === 'secondary' && `
+ background: var(--text-color);
+ color: var(--bg-color);
+ border-color: var(--text-color);
+ `}
+
+ /* Outline variant */
+ ${props => props.$variant === 'outline' && `
+ background: transparent;
+ color: ${props.theme.colors.primary};
+ border-color: ${props.theme.colors.primary};
+
+ &:hover:not(:disabled) {
+ background: ${props.theme.colors.primary};
+ color: var(--bg-color);
+ }
+ `}
+
+ /* Ghost variant */
+ ${props => props.$variant === 'ghost' && `
+ background: transparent;
+ color: ${props.theme.colors.primary};
+ border-color: transparent;
+
+ &:hover:not(:disabled) {
+ background: var(--card-shadow);
+ }
+ `}
+
+ /* Tertiary variant */
+ ${props => props.$variant === 'tertiary' && `
+ background: var(--color-tertiary);
+ color: var(--text-color);
+ border-color: transparent;
+ border-radius: 999px;
+
+ [data-theme="dark"] & {
+ background: ${props.theme.colors.primary};
+ color: #fff;
+ svg {
+ color: #fff;
+ }
+ }
+
+ &:hover:not(:disabled) {
+ background: var(--color-primary);
+ color: #fff;
+ svg {
+ color: #fff;
+ }
+ }
+ `}
+
+ ${props => props.$fullWidth && `
+ width: 100%;
+ `}
+
+ ${props => props.$iconOnly && `
+ padding: var(--spacing-sm);
+ aspect-ratio: 1;
+ width: 48px;
+ height: 48px;
+ border-radius: 999px;
+ `}
+
+ /* === STATES === */
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ &:focus-visible {
+ outline: 2px solid ${props => props.theme.colors.primary};
+ outline-offset: 2px;
+ }
+
+ /* === ICON STYLING === */
+ .button__icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+
+ svg, img {
+ width: auto;
+ height: 1em;
+ max-width: 100%;
+ max-height: 100%;
+ }
+
+ &[style*="width"], &[style*="height"] {
+ width: 100%;
+ height: 100%;
+
+ svg, img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+ }
+ }
+
+ ${props => props.$iconOnly && `
+ .button__icon {
+ width: 100%;
+ height: 100%;
+
+ svg, img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+ }
+ `}
+`
diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx
new file mode 100644
index 00000000..4d37f4f3
--- /dev/null
+++ b/src/components/Button/Button.tsx
@@ -0,0 +1,122 @@
+import { FC } from 'react'
+import { StyledButton } from './Button.styled'
+import { ButtonProps } from './Button.types'
+import { Icon } from '@/components/Icon'
+
+export const Button: FC = ({
+ children,
+ variant = 'primary',
+ size = 'md',
+ href,
+ onClick,
+ disabled = false,
+ fullWidth = false,
+ icon,
+ iconOnly = false,
+ target,
+ className = '',
+ ariaLabel,
+ ariaDescribedBy,
+ loading = false,
+ style,
+ rel,
+ iconColor
+}) => {
+ // BEM classname
+ const bemClass = `button button--${variant} button--${size} ${fullWidth ? 'button--full' : ''} ${iconOnly ? 'button--icon-only' : ''}`.trim()
+ const fullClassName = `${bemClass} ${className}`.trim()
+
+ const renderIcon = () => {
+ if (!icon) return null
+
+ const iconStyle = style && (style.width || style.height)
+ ? { width: style.width, height: style.height }
+ : undefined
+
+ const getIconSize = () => {
+ if (style?.width && style?.height) {
+ const width = typeof style.width === 'string' ? parseInt(style.width) : style.width
+ const height = typeof style.height === 'string' ? parseInt(style.height) : style.height
+ const minSize = Math.min(width, height)
+ return `${minSize * 0.6}px`
+ }
+ return undefined
+ }
+
+ if (typeof icon === 'string') {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+ {icon}
+
+ )
+ }
+
+ const content = (
+ <>
+ {renderIcon()}
+ {!iconOnly && {children} }
+ >
+ )
+
+ if (href) {
+ const accessibleLabel = iconOnly && !ariaLabel
+ ? (typeof icon === 'string' ? `${icon} link` : 'Link')
+ : ariaLabel
+
+ return (
+
+ {content}
+
+ )
+ }
+
+ return (
+
+ {content}
+
+ )
+}
diff --git a/src/components/Button/Button.types.ts b/src/components/Button/Button.types.ts
new file mode 100644
index 00000000..34de97b0
--- /dev/null
+++ b/src/components/Button/Button.types.ts
@@ -0,0 +1,29 @@
+import { ReactNode, MouseEvent, CSSProperties } from 'react'
+
+export type ButtonSize = 'sm' | 'md' | 'lg'
+
+export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'tertiary'
+
+export type ButtonTarget = '_blank' | '_self' | '_parent' | '_top'
+
+export type ButtonRel = 'noopener' | 'noreferrer' | 'nofollow' | 'noopener noreferrer'
+
+export interface ButtonProps {
+ children?: ReactNode
+ variant?: ButtonVariant
+ size?: ButtonSize
+ href?: string
+ onClick?: (e: MouseEvent) => void
+ disabled?: boolean
+ fullWidth?: boolean
+ icon?: ReactNode | string
+ iconOnly?: boolean
+ target?: ButtonTarget | string
+ className?: string
+ ariaLabel?: string
+ ariaDescribedBy?: string
+ loading?: boolean
+ rel?: ButtonRel | string
+ iconColor?: string
+ style?: CSSProperties
+}
diff --git a/src/components/Button/index.ts b/src/components/Button/index.ts
new file mode 100644
index 00000000..7c21e618
--- /dev/null
+++ b/src/components/Button/index.ts
@@ -0,0 +1,2 @@
+export { Button } from './Button'
+export type { ButtonProps, ButtonSize, ButtonVariant } from './Button.types'
diff --git a/src/components/CV/CV.styled.ts b/src/components/CV/CV.styled.ts
new file mode 100644
index 00000000..f2697319
--- /dev/null
+++ b/src/components/CV/CV.styled.ts
@@ -0,0 +1,110 @@
+import styled from 'styled-components'
+
+export const CVContainer = styled.div`
+ margin: 0 auto;
+ // padding: 0 var(--spacing-lg);
+
+ .cv__title {
+ margin-top: var(--spacing-xxl);
+ margin-bottom: var(--spacing-lg);
+ color: var(--title-color);
+
+ &:first-child {
+ margin-top: 0;
+ }
+ }
+
+ .cv__company-link,
+ .cv__school-link {
+ color: inherit;
+ text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+ gap: var(--spacing-xs);
+ transition: all 0.2s ease;
+ border-radius: var(--radius-xs);
+ position: relative;
+
+ &:hover {
+ color: ${props => props.theme.colors.primary};
+ text-decoration: underline;
+ text-decoration-color: ${props => props.theme.colors.primary};
+ text-underline-offset: 3px;
+ }
+
+ &:focus-visible {
+ outline: 2px solid ${props => props.theme.colors.primary};
+ outline-offset: 2px;
+ }
+
+ &::after {
+ content: 'β';
+ font-size: 0.75em;
+ opacity: 0.6;
+ margin-left: 2px;
+ transition: all 0.2s ease;
+ }
+
+ &:hover::after {
+ opacity: 1;
+ transform: translateX(1px) translateY(-1px);
+ }
+ }
+`
+
+export const ExperienceList = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-lg);
+`
+
+export const ExperienceItem = styled.div`
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: var(--spacing-xs);
+ padding-bottom: var(--spacing-xl);
+
+ ${props => props.theme.media.desktop} {
+ grid-template-columns: 180px 1fr;
+ grid-column-gap: var(--spacing-huge);
+ }
+`
+
+
+export const EducationList = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-lg);
+`
+
+export const EducationItem = styled.div`
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: var(--spacing-xs);
+ padding-bottom: var(--spacing-xl);
+
+ ${props => props.theme.media.desktop} {
+ grid-template-columns: 180px 1fr;
+ grid-column-gap: var(--spacing-huge);
+ }
+`
+
+
+export const LinksContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-md);
+ margin-top: var(--spacing-lg);
+
+ ${props => props.theme.media.tablet} {
+ flex-direction: row;
+ gap: var(--spacing-xl);
+ }
+`
+
+
+export const ButtonContainer = styled.div`
+ display: flex;
+ justify-content: center;
+ margin: var(--spacing-lg) 0;
+`
\ No newline at end of file
diff --git a/src/components/CV/CV.tsx b/src/components/CV/CV.tsx
new file mode 100644
index 00000000..0d4eb7c7
--- /dev/null
+++ b/src/components/CV/CV.tsx
@@ -0,0 +1,151 @@
+import { useState } from 'react'
+import { Section } from '@/components/Section'
+import { Title, Text } from '@/components/Typography'
+import { Button } from '@/components/Button'
+import {
+ CVContainer,
+ ExperienceList,
+ ExperienceItem,
+ EducationList,
+ EducationItem,
+ LinksContainer,
+ ButtonContainer
+} from './CV.styled'
+import cvData from '@/data/cv.json'
+
+export const CV = () => {
+ const [showAllExperience, setShowAllExperience] = useState(false)
+ const [showAllEducation, setShowAllEducation] = useState(false)
+
+ const visibleExperience = showAllExperience ? cvData.experience : cvData.experience.slice(0, 3)
+ const visibleEducation = showAllEducation ? cvData.education : cvData.education.slice(0, 3)
+
+ return (
+
+
+ Experience
+
+
+ {visibleExperience.map((job: any, index) => (
+
+ {job.companyUrl ? (
+
+
+ {job.company}
+
+
+ ) : (
+ {job.company}
+ )}
+ {job.period}
+ {job.role}
+ {job.description}
+
+ ))}
+
+
+ {cvData.experience.length > 3 && (
+
+ setShowAllExperience(!showAllExperience)}
+ ariaLabel={showAllExperience ? 'Show less experience items' : `Show ${cvData.experience.length - 3} more experience items`}
+ className="cv__show-more-button"
+ >
+ {showAllExperience ? 'Show Less' : `Show More (${cvData.experience.length - 3} more)`}
+
+
+ )}
+
+ Education
+
+
+ {visibleEducation.map((edu: any, index) => (
+
+ {edu.schoolUrl ? (
+
+
+ {edu.school}
+
+
+ ) : (
+ {edu.school}
+ )}
+ {edu.period}
+ {edu.program}
+ {edu.description}
+
+ ))}
+
+
+ {cvData.education.length > 3 && (
+
+ setShowAllEducation(!showAllEducation)}
+ ariaLabel={showAllEducation ? 'Show less education items' : `Show ${cvData.education.length - 3} more education items`}
+ className="cv__show-more-button"
+ >
+ {showAllEducation ? 'Show Less' : `Show More (${cvData.education.length - 3} more)`}
+
+
+ )}
+
+ Links
+
+
+
+ Design Portfolio β
+
+
+ LinkedIn β
+
+
+ GitHub β
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/CV/index.ts b/src/components/CV/index.ts
new file mode 100644
index 00000000..7f0c936e
--- /dev/null
+++ b/src/components/CV/index.ts
@@ -0,0 +1 @@
+export { CV } from './CV'
\ No newline at end of file
diff --git a/src/components/Card/Card.styled.ts b/src/components/Card/Card.styled.ts
new file mode 100644
index 00000000..c788a5d2
--- /dev/null
+++ b/src/components/Card/Card.styled.ts
@@ -0,0 +1,26 @@
+import styled from 'styled-components'
+
+export const StyledCard = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+`
+
+export const TagsContainer = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+`
+
+export const Tag = styled.span`
+ padding: 0.25rem 0.75rem;
+ background-color: rgba(0, 0, 0, 0.05);
+ border-radius: 1rem;
+ font-size: 0.875rem;
+`
+
+export const LinksContainer = styled.div`
+ display: flex;
+ gap: 1rem;
+ margin-top: 0.5rem;
+`
\ No newline at end of file
diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx
new file mode 100644
index 00000000..cb4191bc
--- /dev/null
+++ b/src/components/Card/Card.tsx
@@ -0,0 +1,29 @@
+import { Title, Text } from '@/components/Typography'
+import { Image } from '@/components/Image'
+import { CardProps } from './Card.types'
+import { StyledCard, TagsContainer, Tag, LinksContainer } from './Card.styled'
+
+export const Card = ({ image, title, desc, tags, netlify, github, role }: CardProps) => {
+ return (
+
+ {image && }
+ {title && {title} }
+ {desc && {desc} }
+
+ {tags && (
+
+ {tags.map(tag => (
+ {tag}
+ ))}
+
+ )}
+
+ {(netlify || github) && (
+
+ {netlify && {netlify} }
+ {github && {github} }
+
+ )}
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/Card/Card.types.ts b/src/components/Card/Card.types.ts
new file mode 100644
index 00000000..e9924b47
--- /dev/null
+++ b/src/components/Card/Card.types.ts
@@ -0,0 +1,9 @@
+export interface CardProps {
+ image?: string
+ title?: string
+ desc?: string
+ tags?: string[]
+ netlify?: string
+ github?: string
+ role?: string
+}
\ No newline at end of file
diff --git a/src/components/Card/index.ts b/src/components/Card/index.ts
new file mode 100644
index 00000000..9ce677a3
--- /dev/null
+++ b/src/components/Card/index.ts
@@ -0,0 +1,2 @@
+export { Card } from './Card'
+export type { CardProps } from './Card.types'
diff --git a/src/components/Footer/Footer.styled.ts b/src/components/Footer/Footer.styled.ts
new file mode 100644
index 00000000..35b4278a
--- /dev/null
+++ b/src/components/Footer/Footer.styled.ts
@@ -0,0 +1,62 @@
+import styled from 'styled-components'
+
+export const ContactInfo = styled.div`
+ margin-bottom: var(--spacing-xl);
+`
+
+export const ContactLink = styled.a`
+ color: var(--text-color);
+ text-decoration: none;
+
+ &:hover {
+ opacity: 0.7;
+ }
+
+ &:focus {
+ outline: 2px solid var(--title-color);
+ outline-offset: 2px;
+ }
+`
+
+export const SocialLinks = styled.ul`
+ display: flex;
+ justify-content: center;
+ gap: var(--spacing-lg);
+ margin-bottom: var(--spacing-xl);
+ color: var(--color-icon);
+ list-style: none;
+ margin: 0;
+ padding: 0;
+`
+
+export const FooterMarquee = styled.div`
+ background: var(--bg-marquee);
+ color: var(--bg-color);
+ padding: var(--spacing-md) 0;
+ overflow: hidden;
+ white-space: nowrap;
+ position: relative;
+`
+
+export const MarqueeTrack = styled.div`
+ display: flex;
+ animation: marquee 30s linear infinite;
+
+ @keyframes marquee {
+ 0% {
+ transform: translateX(0%);
+ }
+ 100% {
+ transform: translateX(-50%);
+ }
+ }
+`
+
+export const MarqueeText = styled.div`
+ display: inline-block;
+ font-family: var(--text-font-family);
+ font-size: var(--text-lg);
+ font-weight: var(--weight-medium);
+ padding-right: 50px;
+ flex-shrink: 0;
+`
\ No newline at end of file
diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
new file mode 100644
index 00000000..a05d8e7a
--- /dev/null
+++ b/src/components/Footer/Footer.tsx
@@ -0,0 +1,121 @@
+import { Avatar } from '@/components/Image'
+import { Text, Title } from '@/components/Typography'
+import { useTheme } from '@/contexts/ThemeContext'
+import { Section } from '@/components/Section'
+import { FooterProps } from './Footer.types'
+import {
+ ContactInfo,
+ ContactLink,
+ SocialLinks,
+ FooterMarquee,
+ MarqueeTrack,
+ MarqueeText
+} from './Footer.styled'
+import { Button } from '../Button'
+
+export const SOCIAL_ICON_MAP: Record = {
+ linkedin: "LinkedIn",
+ github: "Github",
+ twitter: "Twitter",
+ instagram: "Instagram",
+ stackoverflow: "StackOverflow"
+}
+
+export const Footer = ({ data }: FooterProps) => {
+ if (!data) return null
+
+ const { theme } = useTheme()
+
+ const { name, phone, email, avatar_url, socialLinks } = data
+
+ const hasContent = avatar_url || name || phone || email || socialLinks
+ if (!hasContent) return null
+
+ return (
+ <>
+
+
+
+
+
+
+ Daniel Lauding β’ Design Engineer β’ Daniel Lauding β’ Design Engineer β’ Daniel Lauding β’ Design Engineer β’ Daniel Lauding β’ Design Engineer β’
+
+
+
+
+ Daniel Lauding β’ Design Engineer β’ Daniel Lauding β’ Design Engineer β’ Daniel Lauding β’ Design Engineer β’ Daniel Lauding β’ Design Engineer β’
+
+
+
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/src/components/Footer/Footer.types.ts b/src/components/Footer/Footer.types.ts
new file mode 100644
index 00000000..5f14591b
--- /dev/null
+++ b/src/components/Footer/Footer.types.ts
@@ -0,0 +1,18 @@
+export interface FooterData {
+ name?: string
+ title?: string
+ phone?: string
+ email?: string
+ avatar_url?: string
+ socialLinks?: {
+ linkedin?: string
+ github?: string
+ twitter?: string
+ stackoverflow?: string
+ instagram?: string
+ }
+}
+
+export interface FooterProps {
+ data?: FooterData
+}
\ No newline at end of file
diff --git a/src/components/Footer/index.ts b/src/components/Footer/index.ts
new file mode 100644
index 00000000..0271f43e
--- /dev/null
+++ b/src/components/Footer/index.ts
@@ -0,0 +1,2 @@
+export { Footer } from './Footer'
+export type { FooterData, FooterProps } from './Footer.types'
\ No newline at end of file
diff --git a/src/components/Grid/Grid.styled.ts b/src/components/Grid/Grid.styled.ts
new file mode 100644
index 00000000..8f5ca46a
--- /dev/null
+++ b/src/components/Grid/Grid.styled.ts
@@ -0,0 +1,20 @@
+import styled from 'styled-components'
+
+type StyledProps = {
+ $columns?: string
+ $gap?: string
+}
+
+export const StyledGrid = styled.div`
+ display: grid;
+ width: 100%;
+
+ gap: var(--spacing-${props => props.$gap || 'md'});
+
+ grid-template-columns: 1fr;
+
+ ${props => props.theme.media.tablet} {
+ grid-template-columns: repeat(${props => props.$columns || '1'}, 1fr);
+ }
+`
+
diff --git a/src/components/Grid/Grid.tsx b/src/components/Grid/Grid.tsx
new file mode 100644
index 00000000..8111ae23
--- /dev/null
+++ b/src/components/Grid/Grid.tsx
@@ -0,0 +1,15 @@
+import { StyledGrid } from './Grid.styled'
+import { GridProps } from './Grid.types'
+
+export const Grid = ({ children, columns = '1', gap = 'md', className = '' }: GridProps) => {
+ // BEM classname
+ const bemClass = `grid grid--${columns}-col grid--gap-${gap}`
+ const fullClassName = `${bemClass} ${className}`.trim()
+
+ return (
+
+ {children}
+
+ )
+}
+
diff --git a/src/components/Grid/Grid.types.ts b/src/components/Grid/Grid.types.ts
new file mode 100644
index 00000000..268287f5
--- /dev/null
+++ b/src/components/Grid/Grid.types.ts
@@ -0,0 +1,13 @@
+import { ReactNode } from 'react'
+
+export type GridColumns = '1' | '2' | '3' | '4' | '6' | '12'
+
+export type GridGap = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'
+
+export type GridProps = {
+ children: ReactNode
+ columns?: GridColumns
+ gap?: GridGap
+ className?: string
+}
+
diff --git a/src/components/Grid/GridItem.tsx b/src/components/Grid/GridItem.tsx
new file mode 100644
index 00000000..a58635cf
--- /dev/null
+++ b/src/components/Grid/GridItem.tsx
@@ -0,0 +1,27 @@
+import styled from 'styled-components'
+
+type GridItemProps = {
+ span?: string
+ className?: string
+ children: React.ReactNode
+}
+
+const StyledGridItem = styled.div<{ $span?: string }>`
+
+ ${props => props.$span === 'full' && `grid-column: 1 / -1;`}
+ ${props => props.$span === '2' && `grid-column: span 2;`}
+ ${props => props.$span === '3' && `grid-column: span 3;`}
+ ${props => props.$span === '4' && `grid-column: span 4;`}
+`
+
+export const GridItem = ({ span, className = '', children }: GridItemProps) => {
+ const bemClass = `grid__item ${span ? `grid__item--span-${span}` : ''}`
+ const fullClassName = `${bemClass} ${className}`.trim()
+
+ return (
+
+ {children}
+
+ )
+}
+
diff --git a/src/components/Grid/index.ts b/src/components/Grid/index.ts
new file mode 100644
index 00000000..8b271c22
--- /dev/null
+++ b/src/components/Grid/index.ts
@@ -0,0 +1,5 @@
+export { Grid } from './Grid'
+export { GridItem } from './GridItem'
+export type { GridProps, GridColumns, GridGap } from './Grid.types'
+
+
diff --git a/src/components/Hero/Hero.styled.ts b/src/components/Hero/Hero.styled.ts
new file mode 100644
index 00000000..7bf17a29
--- /dev/null
+++ b/src/components/Hero/Hero.styled.ts
@@ -0,0 +1,57 @@
+import styled from 'styled-components'
+
+export const HeroGrid = styled.div`
+ display: grid;
+ width: 100%;
+ gap: var(--spacing-xs);
+
+ grid-template-columns: 1fr;
+ grid-template-areas:
+ "avatar"
+ "intro"
+ "role"
+ "desc";
+
+ ${props => props.theme.media.tablet} {
+ grid-template-columns: minmax(0, auto) 1fr;
+ grid-template-areas:
+ "intro intro"
+ "role role"
+ "avatar desc";
+ }
+
+ ${props => props.theme.media.desktop} {
+ grid-template-columns: minmax(0, auto) 1fr;
+ grid-template-areas:
+ "intro intro"
+ "role role"
+ "avatar desc";
+ }
+`
+
+export const HeroAvatar = styled.div`
+ grid-area: avatar;
+ margin-left: auto;
+ margin-right: auto;
+ ${props => props.theme.media.tablet} {
+ margin-right: var(--spacing-md);
+ }
+`
+
+export const HeroIntro = styled.div`
+ grid-area: intro;
+`
+
+export const HeroRole = styled.div`
+ grid-area: role;
+ margin-bottom: var(--spacing-md);
+`
+
+export const HeroDesc = styled.div`
+ grid-area: desc;
+ gap: var(--spacing-lg);
+ display: flex;
+ flex-direction: column;
+`
+
+
diff --git a/src/components/Hero/Hero.tsx b/src/components/Hero/Hero.tsx
new file mode 100644
index 00000000..1d6a283a
--- /dev/null
+++ b/src/components/Hero/Hero.tsx
@@ -0,0 +1,115 @@
+import { Section } from '@/components/Section'
+import { Button } from '@/components/Button'
+import { Avatar } from '@/components/Image'
+import { Text, Title } from '@/components/Typography'
+import { useTheme } from '@/contexts/ThemeContext'
+import { HeroProps } from './Hero.types'
+import {
+ HeroGrid,
+ HeroAvatar,
+ HeroIntro,
+ HeroRole,
+ HeroDesc,
+} from './Hero.styled'
+
+export const Hero = ({ data }: HeroProps) => {
+ if (!data) return null
+
+ const { theme } = useTheme()
+ const { intro, name, role, desc, avatar_url } = data
+
+ const hasContent = intro || name || role || desc || avatar_url
+ if (!hasContent) return null
+
+ return (
+
+
+
+
+
+
+
+ {avatar_url && (
+
+
+
+ )}
+
+ {intro && (
+
+
+ {intro}
+
+
+ )}
+
+ {role && (
+
+
+ {role}
+
+
+ )}
+
+ {desc && (
+
+ {Array.isArray(desc) ? (
+ desc.map((p, i) => (
+
+ {p}
+
+ ))
+ ) : (
+
+ {desc}
+
+ )}
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/src/components/Hero/Hero.types.ts b/src/components/Hero/Hero.types.ts
new file mode 100644
index 00000000..4ac8d8d6
--- /dev/null
+++ b/src/components/Hero/Hero.types.ts
@@ -0,0 +1,11 @@
+export type HeroData = {
+ intro?: string
+ name?: string
+ role?: string
+ desc?: string | string[]
+ avatar_url?: string
+}
+
+export type HeroProps = {
+ data: HeroData
+}
diff --git a/src/components/Hero/index.ts b/src/components/Hero/index.ts
new file mode 100644
index 00000000..31d5ae5d
--- /dev/null
+++ b/src/components/Hero/index.ts
@@ -0,0 +1,2 @@
+export { Hero } from './Hero'
+export type { HeroProps, HeroData } from './Hero.types'
diff --git a/src/components/Icon/Icon.styled.ts b/src/components/Icon/Icon.styled.ts
new file mode 100644
index 00000000..a864c918
--- /dev/null
+++ b/src/components/Icon/Icon.styled.ts
@@ -0,0 +1,38 @@
+import styled from 'styled-components'
+
+export const StyledIcon = styled.div<{
+ $size?: string
+ $customSize?: string
+ $color?: string
+}>`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ width: ${props => {
+ if (props.$size === 's') return '16px'
+ if (props.$size === 'l') return '32px'
+ return '24px'
+ }};
+
+ height: ${props => {
+ if (props.$size === 's') return '16px'
+ if (props.$size === 'l') return '32px'
+ return '24px'
+ }};
+
+ ${props =>
+ props.$customSize &&
+ `
+ width: ${props.$customSize};
+ height: ${props.$customSize};
+ `}
+
+ color: ${props => props.$color || 'inherit'};
+
+ svg {
+ width: 100%;
+ height: 100%;
+ display: block;
+ }
+`
\ No newline at end of file
diff --git a/src/components/Icon/Icon.tsx b/src/components/Icon/Icon.tsx
new file mode 100644
index 00000000..ea6c5179
--- /dev/null
+++ b/src/components/Icon/Icon.tsx
@@ -0,0 +1,37 @@
+import icons from '@/data/icons.json'
+import { IconProps } from './Icon.types'
+import { StyledIcon } from './Icon.styled'
+
+export const Icon = ({
+ name,
+ size = 'm',
+ customSize,
+ color,
+ decorative = false,
+ title,
+ ariaLabel,
+ ...props
+}: IconProps) => {
+ const iconSvg = (icons as Record)[name]
+
+ if (!iconSvg) {
+ if (import.meta.env.DEV) {
+ console.warn(`Icon "${name}" not found`)
+ }
+ return null
+ }
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/Icon/Icon.types.ts b/src/components/Icon/Icon.types.ts
new file mode 100644
index 00000000..3ca991b8
--- /dev/null
+++ b/src/components/Icon/Icon.types.ts
@@ -0,0 +1,10 @@
+export interface IconProps {
+ name: string
+ size?: 's' | 'm' | 'l'
+ customSize?: string
+ color?: string
+ [key: string]: unknown
+ decorative?: boolean;
+ title?: string;
+ ariaLabel?: string;
+}
\ No newline at end of file
diff --git a/src/components/Icon/index.ts b/src/components/Icon/index.ts
new file mode 100644
index 00000000..1f6c0ce0
--- /dev/null
+++ b/src/components/Icon/index.ts
@@ -0,0 +1 @@
+export { Icon } from './Icon.tsx'
diff --git a/src/components/Image/Avatar.styled.js b/src/components/Image/Avatar.styled.js
new file mode 100644
index 00000000..4b58274b
--- /dev/null
+++ b/src/components/Image/Avatar.styled.js
@@ -0,0 +1,8 @@
+import styled from 'styled-components'
+
+export const StyledAvatar = styled.img`
+ width: ${({ width }) => width || '200px'};
+ height: ${({ height }) => height || '200px'};
+ border-radius: 50%;
+ object-fit: cover;
+`
diff --git a/src/components/Image/Avatar.tsx b/src/components/Image/Avatar.tsx
new file mode 100644
index 00000000..887180b6
--- /dev/null
+++ b/src/components/Image/Avatar.tsx
@@ -0,0 +1,21 @@
+import { StyledAvatar } from './Avatar.styled'
+
+type AvatarProps = {
+ src?: string
+ alt?: string
+ width?: string
+ height?: string
+ className?: string
+ role?: string
+ 'aria-label'?: string
+ [key: string]: unknown
+ decorative?: boolean;
+ loading?: 'lazy' | 'eager';
+ fetchPriority?: 'high' | 'low' | 'auto';
+}
+
+const Avatar = ({ src, alt = 'Avatar', width, height, decorative = false, loading = 'lazy', fetchPriority, ...props }: AvatarProps) => {
+ return
+}
+
+export { Avatar }
diff --git a/src/components/Image/Image.tsx b/src/components/Image/Image.tsx
new file mode 100644
index 00000000..11e276f5
--- /dev/null
+++ b/src/components/Image/Image.tsx
@@ -0,0 +1,25 @@
+interface ImageProps {
+ src: string
+ alt?: string
+ size?: string | number
+ width?: string | number
+ height?: string | number
+ [key: string]: unknown
+ decorative?: boolean;
+ loading?: 'lazy' | 'eager';
+ fetchPriority?: 'high' | 'low' | 'auto';
+}
+
+export const Image = ({ src, alt, size, width, height, decorative = false, loading = 'lazy', fetchPriority, ...props }: ImageProps) => {
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/Image/index.js b/src/components/Image/index.js
new file mode 100644
index 00000000..79f82730
--- /dev/null
+++ b/src/components/Image/index.js
@@ -0,0 +1,2 @@
+export { Image } from './Image.tsx'
+export { Avatar } from './Avatar.tsx'
diff --git a/src/components/Projects/ProjectCard.tsx b/src/components/Projects/ProjectCard.tsx
new file mode 100644
index 00000000..a67199fe
--- /dev/null
+++ b/src/components/Projects/ProjectCard.tsx
@@ -0,0 +1,137 @@
+import { Text, Title } from '@/components/Typography'
+import { Tag } from '@/components/Tag/Tag'
+import { Button } from '@/components/Button'
+import { useTheme } from '@/contexts/ThemeContext'
+import { ProjectCardProps } from './Projects.types'
+import {
+ ProjectCard as Card,
+ ProjectImage,
+ StyledImage,
+ ProjectContent,
+ ProjectTags,
+ ProjectActions,
+ ProjectDateBadge
+} from './Projects.styled'
+
+export const ProjectCard = ({ project, ...props }: ProjectCardProps) => {
+ if (!project) return null
+
+ const { theme } = useTheme()
+
+ const { name, description, image, tags, netlify, github, date, link, codepen } = project
+
+ const hasContent = name || description || image
+ if (!hasContent) return null
+
+ return (
+
+ {image && (
+
+
+
+ )}
+
+ {date && (
+
+
+ {date}
+
+
+ )}
+ {name && (
+
+ {name}
+
+ )}
+ {description && (
+
+ {description}
+
+ )}
+ {tags && tags.length > 0 && (
+
+ {tags.map((tag, index) => (
+
+ {tag}
+
+ ))}
+
+ )}
+ {(netlify || github || link || codepen) && (
+
+ {netlify && (
+
+ Live demo
+
+ )}
+ {github && (
+
+ View the code
+
+ )}
+ {link && (
+
+ View project
+
+ )}
+ {codepen && (
+
+ View on CodePen
+
+ )}
+
+ )}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/Projects/Projects.styled.ts b/src/components/Projects/Projects.styled.ts
new file mode 100644
index 00000000..c675cc9f
--- /dev/null
+++ b/src/components/Projects/Projects.styled.ts
@@ -0,0 +1,135 @@
+import styled from 'styled-components'
+import { Image } from '@/components/Image'
+
+export const ProjectsContainer = styled.ul`
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-xl);
+ width: 100%;
+ max-width: 1200px;
+ padding: 0 0;
+ margin: var(--spacing-md) auto 0 auto;
+
+ ${props => props.theme.media.tablet} {
+ }
+
+ ${props => props.theme.media.desktop} {
+ margin: var(--spacing-huge) auto 0 auto;
+ }
+`
+
+export const TabsContainer = styled.div`
+ display: flex;
+ gap: var(--spacing-md);
+ margin-bottom: var(--spacing-md);
+ flex-wrap: wrap;
+`
+
+export const ProjectDateBadge = styled.span`
+ padding: 0px;
+ width: fit-content;
+`
+
+export const ProjectCard = styled.article`
+ background: var(--card-bg);
+ border-radius: var(--radius-none);
+ overflow: hidden;
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+ display: flex;
+ flex-direction: column;
+ ${props => props.theme.media.desktop} {
+ flex-direction: row;
+ }
+`
+
+export const ProjectImage = styled.div`
+ position: relative;
+ width: 100%;
+ max-width: 100%;
+ aspect-ratio: 408 / 280;
+ overflow: hidden;
+ border-left: 20px solid #E6DAC0;
+ border-bottom: 20px solid #E6DAC0;
+ ${props => props.theme.media.desktop} {
+ width: 408px;
+ max-width: 408px;
+ }
+`
+
+export const StyledImage = styled(Image)`
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+`;
+
+export const ProjectContent = styled.div`
+ padding: var(--spacing-lg) 0;
+ flex: 1;
+ gap: var(--spacing-md);
+ display: flex;
+ flex-direction: column;
+ ${props => props.theme.media.desktop} {
+ padding: 0 var(--spacing-xl);
+ }
+`
+
+export const ProjectTags = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--spacing-sm);
+ margin-bottom: var(--spacing-lg);
+`
+
+export const ProjectTag = styled.span`
+ background: var(--text-color);
+ color: var(--bg-color);
+ padding: var(--spacing-xs) var(--spacing-xs);
+ border-radius: var(--radius-none);
+ font-size: var(--text-sm);
+ font-family: var(--text-font-family);
+`
+
+export const ProjectActions = styled.div`
+ display: flex;
+ gap: var(--spacing-md);
+ padding-top: var(--spacing-md);
+`
+
+export const ProjectLink = styled.a`
+ display: inline-flex;
+ align-items: center;
+ gap: var(--spacing-xs);
+ color: var(--title-color);
+ text-decoration: none;
+ font-family: var(--text-font-family);
+ font-size: var(--text-md);
+ font-weight: var(--weight-medium);
+ transition: opacity 0.2s ease, outline 0.2s ease;
+ border-radius: var(--radius-sm);
+ padding: var(--spacing-xs);
+ margin: calc(-1 * var(--spacing-xs));
+
+ svg {
+ width: 20px;
+ height: 20px;
+ }
+
+ &:hover {
+ opacity: 0.7;
+ }
+
+ &:focus {
+ outline: 2px solid var(--title-color);
+ outline-offset: 2px;
+ }
+
+ &:focus:not(:focus-visible) {
+ outline: none;
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--title-color);
+ outline-offset: 2px;
+ }
+`
\ No newline at end of file
diff --git a/src/components/Projects/Projects.tsx b/src/components/Projects/Projects.tsx
new file mode 100644
index 00000000..c7f4a280
--- /dev/null
+++ b/src/components/Projects/Projects.tsx
@@ -0,0 +1,86 @@
+import { Title } from '@/components/Typography'
+import { Section } from '@/components/Section'
+import { ProjectCard } from './ProjectCard'
+import { ProjectsProps } from './Projects.types'
+import { ProjectsContainer, TabsContainer } from './Projects.styled'
+import { CV } from '@/components/CV'
+import { Tag } from '@/components/Tag'
+import { useState } from 'react'
+
+export const Projects = ({ data }: ProjectsProps) => {
+ const [activeTab, setActiveTab] = useState<'projects' | 'cv'>('projects')
+
+ if (!data) return null
+
+ const projects = data?.projects
+ if (!projects || !Array.isArray(projects) || projects.length === 0) return null
+
+ const getActiveTitle = () => {
+ return activeTab === 'projects' ? 'Featured Projects' : 'Experience & Education'
+ }
+
+ const titleId = 'projects-title'
+
+ return (
+
+
+ setActiveTab('projects')}
+ selected={activeTab === 'projects'}
+ role="tab"
+ aria-selected={activeTab === 'projects'}
+ aria-controls="projects-content"
+ variant="chip"
+ className="section__tab section__tab--projects"
+ >
+ Featured Projects
+
+ setActiveTab('cv')}
+ selected={activeTab === 'cv'}
+ role="tab"
+ aria-selected={activeTab === 'cv'}
+ aria-controls="cv-content"
+ variant="chip"
+ className="section__tab section__tab--cv"
+ >
+ Experience & Education
+
+
+
+
+ {getActiveTitle()}
+
+
+ {activeTab === 'projects' && (
+
+ {projects.map((project, index) => (
+
+
+
+ ))}
+
+ )}
+
+ {activeTab === 'cv' && (
+
+
+
+ )}
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/Projects/Projects.types.ts b/src/components/Projects/Projects.types.ts
new file mode 100644
index 00000000..ee07ccdb
--- /dev/null
+++ b/src/components/Projects/Projects.types.ts
@@ -0,0 +1,23 @@
+export interface Project {
+ name: string
+ description: string
+ image: string
+ tags: string[]
+ netlify: string
+ github: string
+ date?: string
+ codepen?: string
+ link?: string
+}
+
+export interface ProjectsData {
+ projects: Project[]
+}
+
+export interface ProjectsProps {
+ data: ProjectsData
+}
+
+export interface ProjectCardProps {
+ project: Project
+}
\ No newline at end of file
diff --git a/src/components/Projects/index.js b/src/components/Projects/index.js
new file mode 100644
index 00000000..5c0ad7f2
--- /dev/null
+++ b/src/components/Projects/index.js
@@ -0,0 +1,2 @@
+export { Projects } from './Projects'
+export { ProjectCard } from './ProjectCard'
diff --git a/src/components/Projects/index.ts b/src/components/Projects/index.ts
new file mode 100644
index 00000000..9f70e776
--- /dev/null
+++ b/src/components/Projects/index.ts
@@ -0,0 +1,3 @@
+export { Projects } from './Projects'
+export { ProjectCard } from './ProjectCard'
+export type { Project, ProjectsData, ProjectsProps, ProjectCardProps } from './Projects.types'
\ No newline at end of file
diff --git a/src/components/ScrollIndicator/ScrollIndicator.styled.ts b/src/components/ScrollIndicator/ScrollIndicator.styled.ts
new file mode 100644
index 00000000..5b313eee
--- /dev/null
+++ b/src/components/ScrollIndicator/ScrollIndicator.styled.ts
@@ -0,0 +1,36 @@
+import styled from 'styled-components'
+
+interface StyledScrollIndicatorProps {
+ $color?: string
+ $height?: string
+ $position?: 'top' | 'bottom'
+}
+
+export const StyledScrollIndicator = styled.div`
+ position: fixed;
+ ${props => props.$position === 'bottom' ? 'bottom: 0;' : 'top: 0;'}
+ left: 0;
+ right: 0;
+ height: ${props => props.$height || '4px'};
+ background-color: ${props => props.$color || props.theme.colors.primary};
+ z-index: 9999;
+ transform-origin: 0% 50%;
+
+ animation: scroll-progress linear;
+ animation-timeline: scroll();
+ animation-duration: 1ms;
+
+ @keyframes scroll-progress {
+ from {
+ transform: scaleX(0);
+ }
+ to {
+ transform: scaleX(1);
+ }
+ }
+
+ @supports not (animation-timeline: scroll()) {
+ transform: scaleX(var(--scroll-progress, 0));
+ transition: transform 0.1s ease-out;
+ }
+`
\ No newline at end of file
diff --git a/src/components/ScrollIndicator/ScrollIndicator.tsx b/src/components/ScrollIndicator/ScrollIndicator.tsx
new file mode 100644
index 00000000..a127107b
--- /dev/null
+++ b/src/components/ScrollIndicator/ScrollIndicator.tsx
@@ -0,0 +1,51 @@
+import { FC, useEffect, useRef } from 'react'
+import { StyledScrollIndicator } from './ScrollIndicator.styled'
+import { ScrollIndicatorProps } from './ScrollIndicator.types'
+
+export const ScrollIndicator: FC = ({
+ className = '',
+ color,
+ height = '4px',
+ position = 'top'
+}) => {
+ const indicatorRef = useRef(null)
+
+ useEffect(() => {
+ const updateScrollProgress = () => {
+ if (!indicatorRef.current) return
+
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop
+ const scrollHeight = document.documentElement.scrollHeight - window.innerHeight
+ const scrollProgress = scrollHeight > 0 ? scrollTop / scrollHeight : 0
+
+ indicatorRef.current.style.setProperty('--scroll-progress', scrollProgress.toString())
+ }
+
+ const supportsScrollTimeline = CSS.supports('animation-timeline: scroll()')
+
+ if (!supportsScrollTimeline) {
+ window.addEventListener('scroll', updateScrollProgress, { passive: true })
+ updateScrollProgress()
+ }
+
+ return () => {
+ if (!supportsScrollTimeline) {
+ window.removeEventListener('scroll', updateScrollProgress, { passive: true })
+ }
+ }
+ }, [])
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/ScrollIndicator/ScrollIndicator.types.ts b/src/components/ScrollIndicator/ScrollIndicator.types.ts
new file mode 100644
index 00000000..6f8eb6dc
--- /dev/null
+++ b/src/components/ScrollIndicator/ScrollIndicator.types.ts
@@ -0,0 +1,6 @@
+export interface ScrollIndicatorProps {
+ className?: string
+ color?: string
+ height?: string
+ position?: 'top' | 'bottom'
+}
\ No newline at end of file
diff --git a/src/components/ScrollIndicator/index.ts b/src/components/ScrollIndicator/index.ts
new file mode 100644
index 00000000..f290b5e4
--- /dev/null
+++ b/src/components/ScrollIndicator/index.ts
@@ -0,0 +1,2 @@
+export { ScrollIndicator } from './ScrollIndicator'
+export type { ScrollIndicatorProps } from './ScrollIndicator.types'
\ No newline at end of file
diff --git a/src/components/Section/Section.styled.ts b/src/components/Section/Section.styled.ts
new file mode 100644
index 00000000..e213a326
--- /dev/null
+++ b/src/components/Section/Section.styled.ts
@@ -0,0 +1,164 @@
+import styled from 'styled-components'
+
+const variants = {
+ default: {
+ bg: 'var(--bg-color)',
+ titleColor: 'var(--title-color)',
+ textColor: 'var(--text-color)',
+ },
+ hero: {
+ bg: 'var(--section-hero-bg-color)',
+ titleColor: 'var(--section-hero-title-color)',
+ textColor: 'var(--section-hero-text-color)',
+ },
+ tech: {
+ bg: 'var(--section-tech-bg-color)',
+ titleColor: 'var(--section-tech-title-color)',
+ textColor: 'var(--section-tech-text-color)',
+ },
+ projects: {
+ bg: 'var(--section-projects-bg-color)',
+ titleColor: 'var(--section-projects-title-color)',
+ textColor: 'var(--section-projects-text-color)',
+ },
+ articles: {
+ bg: 'var(--section-articles-bg-color)',
+ titleColor: 'var(--section-articles-title-color)',
+ textColor: 'var(--section-articles-text-color)',
+ },
+ skills: {
+ bg: 'var(--section-skills-bg-color)',
+ titleColor: 'var(--section-skills-title-color)',
+ textColor: 'var(--section-skills-text-color)',
+ },
+ footer: {
+ bg: 'var(--section-footer-bg-color)',
+ titleColor: 'var(--section-footer-title-color)',
+ textColor: 'var(--section-footer-text-color)',
+ },
+}
+
+type StyledProps = {
+ $variant?: string
+ $layout?: string
+ $flexDirection?: string
+ $alignItems?: string
+ $justifyContent?: string
+ $gap?: string
+}
+
+export const StyledSection = styled.section`
+ width: 100%;
+ padding: var(--spacing-xl) var(--spacing-xl);
+ opacity: 0;
+
+ /* Hero section should be visible immediately, then fade in with delay */
+ &.section--hero {
+ opacity: 1;
+
+ &.animate__animated {
+ animation-delay: var(--animate-delay, 0.3s);
+ }
+ }
+
+ /* Apply custom animation delay if set */
+ &[style*="--animate-delay"] {
+ &.animate__animated {
+ animation-delay: var(--animate-delay);
+ }
+ }
+
+ &.animate__animated {
+ opacity: 1;
+ }
+
+ /* Respect reduced motion preferences */
+ @media (prefers-reduced-motion: reduce) {
+ opacity: 1 !important;
+
+ &.animate__animated {
+ animation: none !important;
+ }
+ }
+
+ &.section--cv {
+ padding: 0;
+ }
+
+ ${props => props.theme.media.desktop} {
+ padding: var(--spacing-xl) var(--spacing-xl);
+ }
+
+ ${props => props.theme.media.desktop} {
+ padding: var(--spacing-huge) 20px;
+ }
+
+ background: ${props =>
+ variants[props.$variant as keyof typeof variants]?.bg || variants.default.bg};
+ position: relative;
+
+ ${props =>
+ props.$layout === 'full' &&
+ `
+ min-height: 100vh;
+ display: flex;
+ `}
+
+ ${props => props.$justifyContent && `justify-content: ${props.$justifyContent};`}
+
+ ${props =>
+ props.$variant &&
+ variants[props.$variant as keyof typeof variants] &&
+ `
+ h1, h2, h3, h4, h5, h6 {
+ color: ${variants[props.$variant as keyof typeof variants].titleColor};
+ }
+ p {
+ color: ${variants[props.$variant as keyof typeof variants].textColor};
+ }
+ `}
+
+ ${props =>
+ props.$variant === 'articles' &&
+ `
+ &::before {
+ content: '';
+ position: absolute;
+ top: -5px;
+ left: 0;
+ right: 0;
+ height: 11px;
+ background-image: url("data:image/svg+xml,%3Csvg width='175' height='11' viewBox='0 0 175 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M137.453 2.49477C144.875 -0.738314 151.752 -1.00455 157.474 2.73403C160.915 4.98284 167.89 5.24901 174.213 2.49477V7.99575C166.791 11.2288 159.914 11.4949 154.192 7.75649C150.751 5.50764 146.173 5.24137 139.85 7.99575C132.428 11.2288 125.551 11.4949 119.829 7.75649C116.387 5.50764 111.809 5.24137 105.486 7.99575C98.0643 11.2288 91.1875 11.4949 85.4658 7.75649C82.0241 5.50764 77.4462 5.24137 71.123 7.99575C63.701 11.2288 56.8243 11.4949 51.1025 7.75649C47.6608 5.50764 43.0829 5.24137 36.7598 7.99575C29.3377 11.2288 22.461 11.4949 16.7393 7.75649C13.2975 5.50764 6.32316 5.24137 0 7.99575V2.49477C7.42215 -0.738323 14.2987 -1.00459 20.0205 2.73403C23.4622 4.98285 28.0402 5.24904 34.3633 2.49477C41.7854 -0.738321 48.662 -1.00458 54.3838 2.73403C57.8255 4.98285 62.4035 5.24903 68.7266 2.49477C76.1487 -0.738319 83.0253 -1.00457 88.7471 2.73403C92.1888 4.98285 96.7668 5.24903 103.09 2.49477C110.512 -0.738316 117.389 -1.00456 123.11 2.73403C126.552 4.98284 131.13 5.24902 137.453 2.49477Z' fill='%2357B99B'/%3E%3C/svg%3E");
+ background-repeat: repeat-x;
+ background-size: 175px 11px;
+ background-position: 0 top;
+ pointer-events: none;
+ }
+
+ [data-theme="dark"] & {
+ &::before {
+ display: none;
+ }
+ }
+ `}
+`
+
+export const Container = styled.div`
+ max-width: 1200px;
+ margin: 0 auto;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-md);
+
+ ${props => props.theme.media.desktop} {
+ gap: var(--spacing-xxl);
+ flex-direction: ${props => props.$flexDirection || 'column'};
+ }
+
+ ${props => props.$alignItems && `align-items: ${props.$alignItems};`}
+
+ ${props => props.$justifyContent && `justify-content: ${props.$justifyContent};`}
+
+ ${props => props.$layout === 'full' && `min-height: 100%;`}
+`
diff --git a/src/components/Section/Section.tsx b/src/components/Section/Section.tsx
new file mode 100644
index 00000000..12f069d6
--- /dev/null
+++ b/src/components/Section/Section.tsx
@@ -0,0 +1,132 @@
+import { useEffect, useRef } from 'react'
+import { Title, Text } from '@/components/Typography'
+import { StyledSection, Container } from './Section.styled'
+import { SectionProps } from './Section.types'
+
+export const Section = ({
+ children,
+ variant,
+ layout = 'default',
+ flexDirection = 'column',
+ alignItems,
+ justifyContent,
+ gap = 'md',
+ title,
+ text,
+ id,
+ className = '',
+ hideTitle = false,
+ animate = true,
+ animationType = 'fadeIn',
+ animationDelay = 0,
+}: SectionProps) => {
+ const sectionRef = useRef(null)
+
+ useEffect(() => {
+ if (!animate || !sectionRef.current) {
+ if (sectionRef.current) {
+ sectionRef.current.style.opacity = '1'
+ }
+ return
+ }
+
+ if (variant === 'hero' && sectionRef.current) {
+ const timer = setTimeout(() => {
+ if (sectionRef.current) {
+ sectionRef.current.classList.add('animate__animated', 'animate__fadeIn')
+ sectionRef.current.style.setProperty('--animate-delay', `${animationDelay || 0.3}s`)
+ }
+ }, 100)
+
+ return () => clearTimeout(timer)
+ }
+
+ // Check if section is already visible on mount (common on mobile)
+ const checkInitialVisibility = () => {
+ if (!sectionRef.current) return false
+ const rect = sectionRef.current.getBoundingClientRect()
+ const viewportHeight = window.innerHeight
+ // Check if section is in viewport
+ return rect.top < viewportHeight && rect.bottom > 0
+ }
+
+ const triggerAnimation = (element: HTMLElement) => {
+ element.classList.add('animate__animated', `animate__${animationType}`)
+ if (animationDelay > 0) {
+ element.style.setProperty('--animate-delay', `${animationDelay}s`)
+ }
+ }
+
+ // If already visible, animate immediately (mobile fallback)
+ if (sectionRef.current && checkInitialVisibility()) {
+ const timer = setTimeout(() => {
+ if (sectionRef.current) {
+ triggerAnimation(sectionRef.current)
+ }
+ }, 150)
+ return () => clearTimeout(timer)
+ }
+
+ // Set up Intersection Observer for scroll-triggered animations
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting && entry.target instanceof HTMLElement) {
+ triggerAnimation(entry.target)
+ observer.unobserve(entry.target)
+ }
+ })
+ },
+ {
+ // More lenient for mobile - trigger when any part is visible
+ threshold: 0,
+ // Smaller rootMargin for mobile to trigger earlier
+ rootMargin: '0px 0px -30px 0px',
+ }
+ )
+
+ if (sectionRef.current) {
+ observer.observe(sectionRef.current)
+ }
+
+ return () => {
+ if (sectionRef.current) {
+ observer.unobserve(sectionRef.current)
+ }
+ }
+ }, [animate, animationType, variant, animationDelay])
+
+ const bemClass = `section ${variant ? `section--${variant}` : ''} ${layout ? `section--${layout}` : ''}`.trim()
+ const fullClassName = `${bemClass} ${className}`.trim()
+ const titleId = title ? `${id}-title` : undefined;
+ const titleClassName = [
+ "section__title",
+ variant ? `section__title--${variant}` : "",
+ hideTitle ? "sr-only" : ""
+ ].join(" ").trim();
+
+ return (
+
+
+ {title && {title} }
+ {text && {text} }
+ {children}
+
+
+ )
+}
diff --git a/src/components/Section/Section.types.ts b/src/components/Section/Section.types.ts
new file mode 100644
index 00000000..feafe283
--- /dev/null
+++ b/src/components/Section/Section.types.ts
@@ -0,0 +1,30 @@
+import { ReactNode } from 'react'
+
+export type SectionVariant = 'hero' | 'tech' | 'projects' | 'articles' | 'skills' | 'footer'
+
+export type SectionLayout = 'default' | 'container' | 'centered' | 'full'
+
+export type AlignItems = 'flex-start' | 'center' | 'flex-end' | 'stretch'
+export type JustifyContent = 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around'
+export type FlexDirection = 'row' | 'column' | 'row-reverse' | 'column-reverse'
+export type Gap = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'
+
+export type SectionProps = {
+ children: ReactNode
+ variant?: SectionVariant
+ layout?: SectionLayout
+ flexDirection?: FlexDirection
+ alignItems?: AlignItems
+ justifyContent?: JustifyContent
+ gap?: Gap
+ title?: string
+ text?: string
+ id?: string
+ className?: string
+ role?: 'main' | 'section' | 'aside' | 'article';
+ ariaLabelledBy?: string;
+ hideTitle?: boolean;
+ animate?: boolean;
+ animationType?: 'fadeInUp' | 'fadeIn' | 'fadeInLeft' | 'fadeInRight' | 'zoomIn';
+ animationDelay?: number;
+}
diff --git a/src/components/Section/index.ts b/src/components/Section/index.ts
new file mode 100644
index 00000000..ec0a85e4
--- /dev/null
+++ b/src/components/Section/index.ts
@@ -0,0 +1,2 @@
+export { Section } from './Section'
+export type { SectionProps, SectionVariant, SectionLayout } from './Section.types'
diff --git a/src/components/Skills/Skills.styled.ts b/src/components/Skills/Skills.styled.ts
new file mode 100644
index 00000000..a95550c4
--- /dev/null
+++ b/src/components/Skills/Skills.styled.ts
@@ -0,0 +1,42 @@
+import styled from 'styled-components'
+
+export const SkillsGrid = styled.div`
+ display: grid;
+ gap: var(--spacing-xxl);
+ width: 100%;
+ max-width: 1200px;
+ margin: 0 auto;
+
+ ${props => props.theme.media.tablet} {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ ${props => props.theme.media.desktop} {
+ grid-template-columns: repeat(4, 1fr);
+ }
+`
+
+export const SkillCategory = styled.div`
+ text-align: center;
+`
+
+export const CategoryHeader = styled.div`
+ margin-bottom: var(--spacing-lg);
+ border-radius: var(--radius-sm);
+`
+
+export const SkillsList = styled.ul`
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+`
+
+export const SkillItem = styled.li`
+ font-family: var(--text-font-family);
+ font-size: var(--text-md);
+ color: var(--section-skills-text-color);
+ text-align: left;
+`
\ No newline at end of file
diff --git a/src/components/Skills/Skills.tsx b/src/components/Skills/Skills.tsx
new file mode 100644
index 00000000..70aa62c8
--- /dev/null
+++ b/src/components/Skills/Skills.tsx
@@ -0,0 +1,64 @@
+import { Title, Text } from '@/components/Typography'
+import { Section } from '@/components/Section'
+import { SkillsProps } from './Skills.types'
+import {
+ SkillsGrid,
+ SkillCategory,
+ CategoryHeader,
+ SkillsList,
+ SkillItem
+} from './Skills.styled'
+
+export const Skills = ({ data }: SkillsProps) => {
+ if (!data) return null
+
+ const skills = data?.skills
+ if (!skills || !Array.isArray(skills) || skills.length === 0) return null
+
+ const getCategoryClass = (category: string) => {
+ const categoryLower = category.toLowerCase()
+ if (categoryLower.includes('frontend') || categoryLower.includes('code')) return 'code'
+ if (categoryLower.includes('backend') || categoryLower.includes('tool')) return 'toolbox'
+ if (categoryLower.includes('upcoming')) return 'upcoming'
+ return 'more'
+ }
+
+ return (
+
+
+ {skills.map((skill, index) => (
+
+
+
+ {skill.category}
+
+
+ {skill.items && skill.items.length > 0 && (
+
+ {skill.items.map((item, itemIndex) => (
+
+
+ {item}
+
+
+ ))}
+
+ )}
+
+ ))}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/Skills/Skills.types.ts b/src/components/Skills/Skills.types.ts
new file mode 100644
index 00000000..f08d1213
--- /dev/null
+++ b/src/components/Skills/Skills.types.ts
@@ -0,0 +1,12 @@
+export interface Skill {
+ category: string
+ items: string[]
+}
+
+export interface SkillsData {
+ skills: Skill[]
+}
+
+export interface SkillsProps {
+ data: SkillsData
+}
\ No newline at end of file
diff --git a/src/components/Skills/index.ts b/src/components/Skills/index.ts
new file mode 100644
index 00000000..f7f0b019
--- /dev/null
+++ b/src/components/Skills/index.ts
@@ -0,0 +1,2 @@
+export { Skills } from './Skills'
+export type { Skill, SkillsData, SkillsProps } from './Skills.types'
\ No newline at end of file
diff --git a/src/components/SkipLink/SkipLink.styled.ts b/src/components/SkipLink/SkipLink.styled.ts
new file mode 100644
index 00000000..e2af0036
--- /dev/null
+++ b/src/components/SkipLink/SkipLink.styled.ts
@@ -0,0 +1,48 @@
+import styled from 'styled-components';
+
+export const StyledSkipLink = styled.a`
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ clip-path: inset(50%);
+ white-space: nowrap;
+ border-width: 0;
+ background-color: var(--title-color);
+ color: var(--bg-color);
+ font-family: var(--text-font-family);
+ font-size: var(--text-sm);
+ font-weight: var(--weight-semibold);
+ text-decoration: none;
+ z-index: 9999;
+ transition: all 0.2s ease-in-out;
+
+ &:focus,
+ &:focus-within,
+ &:active {
+ position: fixed;
+ top: 10px;
+ left: 10px;
+ width: auto;
+ height: auto;
+ padding: 12px 24px;
+ margin: 0;
+ overflow: visible;
+ clip: auto;
+ clip-path: none;
+ white-space: nowrap;
+ z-index: 999999;
+ background: var(--title-color);
+ color: var(--bg-color);
+ text-decoration: none;
+ border-radius: 4px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ outline: 3px solid var(--color-primary);
+ outline-offset: 2px;
+ }
+`;
\ No newline at end of file
diff --git a/src/components/SkipLink/SkipLink.tsx b/src/components/SkipLink/SkipLink.tsx
new file mode 100644
index 00000000..29f70e87
--- /dev/null
+++ b/src/components/SkipLink/SkipLink.tsx
@@ -0,0 +1,23 @@
+import { SkipLinkProps } from './SkipLink.types';
+import { StyledSkipLink } from './SkipLink.styled';
+
+export const SkipLink = ({ href, children, className }: SkipLinkProps) => {
+ const hasValidChildren = children && String(children).trim().length > 0;
+ const linkText = hasValidChildren ? children : 'Skip link';
+
+ const ariaLabel = hasValidChildren
+ ? String(children).trim()
+ : `Skip to ${href.replace('#', '')}`;
+
+ return (
+
+ {linkText}
+
+ );
+};
+
+export default SkipLink;
\ No newline at end of file
diff --git a/src/components/SkipLink/SkipLink.types.ts b/src/components/SkipLink/SkipLink.types.ts
new file mode 100644
index 00000000..a9e32587
--- /dev/null
+++ b/src/components/SkipLink/SkipLink.types.ts
@@ -0,0 +1,5 @@
+export interface SkipLinkProps {
+ href: string;
+ children: React.ReactNode;
+ className?: string;
+}
\ No newline at end of file
diff --git a/src/components/SkipLink/index.ts b/src/components/SkipLink/index.ts
new file mode 100644
index 00000000..1232ec74
--- /dev/null
+++ b/src/components/SkipLink/index.ts
@@ -0,0 +1,2 @@
+export { SkipLink } from './SkipLink';
+export type { SkipLinkProps } from './SkipLink.types';
\ No newline at end of file
diff --git a/src/components/StyleGuide/StyleGuide.styled.ts b/src/components/StyleGuide/StyleGuide.styled.ts
new file mode 100644
index 00000000..6c59c4f9
--- /dev/null
+++ b/src/components/StyleGuide/StyleGuide.styled.ts
@@ -0,0 +1,255 @@
+import styled from 'styled-components';
+
+export const StyleGuideContainer = styled.div`
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: var(--spacing-xl);
+
+ ${props => props.theme.media.tablet} {
+ padding: var(--spacing-xxl);
+ }
+`;
+
+export const Navigation = styled.nav`
+ position: sticky;
+ top: var(--spacing-lg);
+ background: var(--card-bg);
+ border-radius: var(--radius-lg);
+ padding: var(--spacing-md);
+ margin: var(--spacing-xl) 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--spacing-sm);
+ border: 1px solid var(--border-color);
+ z-index: 100;
+
+ ${props => props.theme.media.mobile} {
+ justify-content: center;
+ }
+`;
+
+export const NavItem = styled.a`
+ padding: var(--spacing-xs) var(--spacing-sm);
+ background: var(--bg-color);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ text-decoration: none;
+ color: var(--text-color);
+ font-size: var(--text-sm);
+ font-weight: var(--weight-medium);
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: var(--title-color);
+ color: var(--bg-color);
+ border-color: var(--title-color);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--title-color);
+ outline-offset: 2px;
+ }
+`;
+
+export const ComponentSection = styled.section`
+ margin: var(--spacing-xxl) 0;
+ padding-top: var(--spacing-lg);
+ border-top: 1px solid var(--border-color);
+
+ &:first-of-type {
+ border-top: none;
+ margin-top: 0;
+ }
+`;
+
+export const ExampleGrid = styled.div`
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: var(--spacing-xl);
+ margin: var(--spacing-xl) 0;
+
+ ${props => props.theme.media.tablet} {
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
+ }
+`;
+
+export const ExampleCard = styled.div`
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-lg);
+ padding: var(--spacing-lg);
+ background: var(--card-bg);
+ box-shadow: 0 2px 4px var(--card-shadow);
+
+ h3 {
+ margin-top: 0;
+ margin-bottom: var(--spacing-sm);
+ }
+
+ p {
+ margin-bottom: var(--spacing-md);
+ color: var(--text-muted);
+ }
+`;
+
+export const Preview = styled.div`
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: var(--spacing-lg);
+ margin: var(--spacing-md) 0;
+
+ /* Ensure preview content displays properly */
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--spacing-sm);
+
+ /* Handle different component layouts */
+ > * {
+ margin: 0;
+ }
+
+ /* Stack items vertically for titles */
+ &:has(h1, h2, h3, h4, h5, h6) {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+`;
+
+export const CodeBlock = styled.div`
+ position: relative;
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+
+ pre {
+ margin: 0;
+ padding: var(--spacing-md);
+ background: none;
+ color: var(--text-color);
+ font-family: 'SF Mono', Monaco, Inconsolata, 'Roboto Mono', Consolas, 'Courier New', monospace;
+ font-size: var(--text-sm);
+ line-height: 1.5;
+ overflow-x: auto;
+ white-space: pre;
+ }
+`;
+
+export const CopyButton = styled.button`
+ position: absolute;
+ top: var(--spacing-sm);
+ right: var(--spacing-sm);
+ padding: var(--spacing-xs) var(--spacing-sm);
+ background: var(--title-color);
+ color: var(--bg-color);
+ border: none;
+ border-radius: var(--radius-sm);
+ font-size: var(--text-xs);
+ font-weight: var(--weight-medium);
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ opacity: 0.9;
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+
+ &.copied {
+ background: var(--color-tertiary);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--bg-color);
+ outline-offset: -2px;
+ }
+`;
+
+export const PropsTable = styled.div`
+ margin: var(--spacing-xl) 0;
+
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ background: var(--card-bg);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ box-shadow: 0 1px 3px var(--card-shadow);
+ }
+
+ th, td {
+ padding: var(--spacing-sm) var(--spacing-md);
+ text-align: left;
+ border-bottom: 1px solid var(--border-color);
+ }
+
+ th {
+ background: var(--bg-color);
+ font-weight: var(--weight-semibold);
+ color: var(--text-muted);
+ font-size: var(--text-sm);
+ }
+
+ td {
+ font-size: var(--text-sm);
+ vertical-align: top;
+ }
+
+ td:first-child {
+ font-family: 'SF Mono', Monaco, monospace;
+ background: var(--bg-color);
+ font-weight: var(--weight-medium);
+ color: var(--color-secondary);
+ }
+
+ td:nth-child(2) {
+ font-family: 'SF Mono', Monaco, monospace;
+ font-size: var(--text-xs);
+ color: var(--color-primary);
+ }
+
+ td:nth-child(3) {
+ font-family: 'SF Mono', Monaco, monospace;
+ font-size: var(--text-xs);
+ color: var(--color-tertiary);
+ }
+
+ tbody tr:last-child td {
+ border-bottom: none;
+ }
+
+ tbody tr:hover {
+ background: var(--bg-color);
+ }
+`;
+
+export const ColorPalette = styled.div`
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+ gap: var(--spacing-md);
+ margin: var(--spacing-lg) 0;
+`;
+
+export const ColorSwatch = styled.div<{ $color: string }>`
+ background: ${props => props.$color};
+ height: 80px;
+ border-radius: var(--radius-md);
+ border: 1px solid var(--border-color);
+ display: flex;
+ align-items: flex-end;
+ position: relative;
+
+ &::after {
+ content: '${props => props.$color}';
+ background: var(--text-color);
+ color: var(--bg-color);
+ padding: var(--spacing-xs);
+ font-size: var(--text-xs);
+ font-family: 'SF Mono', Monaco, monospace;
+ width: 100%;
+ text-align: center;
+ }
+`;
\ No newline at end of file
diff --git a/src/components/StyleGuide/StyleGuide.tsx b/src/components/StyleGuide/StyleGuide.tsx
new file mode 100644
index 00000000..58ee3e5a
--- /dev/null
+++ b/src/components/StyleGuide/StyleGuide.tsx
@@ -0,0 +1,12 @@
+import { Title } from '@/components/Typography';
+
+const StyleGuide = () => {
+ return (
+
+
Design System Style Guide
+
Welcome to the component library!
+
+ );
+};
+
+export default StyleGuide;
\ No newline at end of file
diff --git a/src/components/StyleGuide/index.ts b/src/components/StyleGuide/index.ts
new file mode 100644
index 00000000..98915b9d
--- /dev/null
+++ b/src/components/StyleGuide/index.ts
@@ -0,0 +1 @@
+export { default as StyleGuide } from './StyleGuide';
\ No newline at end of file
diff --git a/src/components/Tag/Tag.styled.ts b/src/components/Tag/Tag.styled.ts
new file mode 100644
index 00000000..5b81af41
--- /dev/null
+++ b/src/components/Tag/Tag.styled.ts
@@ -0,0 +1,75 @@
+import styled from 'styled-components'
+
+export const StyledTag = styled.span<{
+ $clickable?: boolean;
+ $selected?: boolean;
+ $disabled?: boolean;
+ $variant?: 'default' | 'chip';
+}>`
+ display: inline-flex;
+ align-items: center;
+ padding: ${props =>
+ props.$variant === 'chip' ? '0.5rem 1rem' : '0 var(--spacing-xs) 0px var(--spacing-xs)'
+ };
+ height: ${props =>
+ props.$variant === 'chip' ? 'auto' : '24px'
+ };
+ background-color: ${props => {
+ if (props.$variant === 'chip') {
+ return props.$selected ? 'var(--chip-bg-active)' : 'var(--chip-bg-default)'
+ }
+ return props.$selected ? 'var(--tag-bg-color)' :
+ props.$disabled ? 'var(--tag-bg-color)' :
+ 'var(--tag-bg-color)'
+ }};
+ border: ${props =>
+ props.$variant === 'chip' ? 'none' :
+ `0px solid ${props.$selected ? 'var(--title-color)' : 'var(--border-color)'}`
+ };
+ border-radius: ${props =>
+ props.$variant === 'chip' ? '20px' : '0px'
+ };
+ line-height: 1;
+ transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
+ opacity: ${props => props.$disabled ? 0.6 : 1};
+
+ .tag__text {
+ color: ${props => {
+ if (props.$variant === 'chip') {
+ return props.$selected ? 'var(--text-color)' : 'var(--text-color)'
+ }
+ return props.$selected ? 'var(--tag-text-color)' :
+ props.$disabled ? 'var(--tag-text-color)' :
+ '#fff'
+ }} !important;
+ font-weight: ${props =>
+ props.$variant === 'chip' && props.$selected ? '600' :
+ props.$variant === 'chip' ? '400' :
+ 'medium'
+ };
+ font-size: ${props =>
+ props.$variant === 'chip' ? '0.9rem' : 'inherit'
+ };
+ }
+
+ ${props => (props.$clickable && !props.$disabled) && `
+ &:hover {
+ background-color: ${
+ props.$selected ? 'var(--chip-bg-hover)' : 'var(--chip-bg-hover)'
+ };
+ }
+ `}
+
+ ${props => props.$clickable && !props.$disabled && `
+ cursor: pointer;
+ `}
+
+ ${props => props.$disabled && `
+ cursor: not-allowed;
+ `}
+
+ &:focus-visible {
+ outline: 2px solid var(--title-color);
+ outline-offset: 2px;
+ }
+`
\ No newline at end of file
diff --git a/src/components/Tag/Tag.tsx b/src/components/Tag/Tag.tsx
new file mode 100644
index 00000000..80719858
--- /dev/null
+++ b/src/components/Tag/Tag.tsx
@@ -0,0 +1,46 @@
+import { TagProps } from './Tag.types';
+import { StyledTag } from './Tag.styled';
+import { Text } from '@/components/Typography';
+
+export const Tag = ({
+ children,
+ className,
+ onClick,
+ selected,
+ disabled,
+ role,
+ ariaDescribedBy,
+ ariaPressed,
+ ariaSelected,
+ variant = 'default',
+}: TagProps) => {
+ return (
+ {
+ if (!disabled && onClick && (e.key === 'Enter' || e.key === ' ')) {
+ e.preventDefault();
+ onClick();
+ }
+ }}
+ role={role || (onClick ? 'button' : 'listitem')}
+ tabIndex={disabled ? -1 : onClick ? 0 : undefined}
+ aria-label={typeof children === 'string' ? children : undefined}
+ aria-pressed={role === 'button' ? (ariaPressed !== undefined ? ariaPressed : selected) : undefined}
+ aria-selected={role === 'tab' ? (ariaSelected !== undefined ? ariaSelected : selected) : undefined}
+ aria-disabled={disabled}
+ aria-describedby={ariaDescribedBy}
+ >
+
+ {children}
+
+
+ );
+};
+
+export default Tag;
\ No newline at end of file
diff --git a/src/components/Tag/Tag.types.ts b/src/components/Tag/Tag.types.ts
new file mode 100644
index 00000000..ad7173ef
--- /dev/null
+++ b/src/components/Tag/Tag.types.ts
@@ -0,0 +1,13 @@
+export interface TagProps {
+ children: React.ReactNode;
+ className?: string;
+ onClick?: () => void;
+ onRemove?: () => void;
+ selected?: boolean;
+ disabled?: boolean;
+ role?: string;
+ ariaDescribedBy?: string;
+ ariaPressed?: boolean;
+ ariaSelected?: boolean;
+ variant?: 'default' | 'chip';
+}
\ No newline at end of file
diff --git a/src/components/Tag/index.ts b/src/components/Tag/index.ts
new file mode 100644
index 00000000..db8705b5
--- /dev/null
+++ b/src/components/Tag/index.ts
@@ -0,0 +1,2 @@
+export { Tag } from './Tag'
+export type { TagProps } from './Tag.types'
\ No newline at end of file
diff --git a/src/components/TechStack/TechStack.tsx b/src/components/TechStack/TechStack.tsx
new file mode 100644
index 00000000..e5644a9a
--- /dev/null
+++ b/src/components/TechStack/TechStack.tsx
@@ -0,0 +1,44 @@
+import { Section } from '@/components/Section'
+import { Text, Title } from '@/components/Typography'
+import { TechStackProps } from './TechStack.types'
+
+export const TechStack = ({ data }: TechStackProps) => {
+ if (!data) return null
+
+ const { title, desc } = data
+
+ const hasContent = title || desc
+ if (!hasContent) return null
+
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {desc && (
+
+ {desc}
+
+ )}
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/TechStack/TechStack.types.ts b/src/components/TechStack/TechStack.types.ts
new file mode 100644
index 00000000..5637ab57
--- /dev/null
+++ b/src/components/TechStack/TechStack.types.ts
@@ -0,0 +1,9 @@
+export type TechStackData = {
+ title?: string
+ desc?: string
+}
+
+export type TechStackProps = {
+ data: TechStackData
+}
+
diff --git a/src/components/TechStack/index.ts b/src/components/TechStack/index.ts
new file mode 100644
index 00000000..bb14358b
--- /dev/null
+++ b/src/components/TechStack/index.ts
@@ -0,0 +1,3 @@
+export { TechStack } from './TechStack'
+export type { TechStackProps, TechStackData } from './TechStack.types'
+
diff --git a/src/components/ThemeToggle/ThemeToggle.styled.ts b/src/components/ThemeToggle/ThemeToggle.styled.ts
new file mode 100644
index 00000000..f3230cd4
--- /dev/null
+++ b/src/components/ThemeToggle/ThemeToggle.styled.ts
@@ -0,0 +1,42 @@
+import styled from 'styled-components'
+
+export const ThemeToggleWrapper = styled.div`
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ z-index: 1000;
+
+ .theme-toggle__button {
+ // box-shadow: 0 2px 8px var(--card-shadow);
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: scale(1.1) rotate(15deg);
+ box-shadow: 0 4px 12px var(--card-shadow);
+ border-color: ${props => props.theme.colors.primary} !important;
+
+ svg {
+ color: ${props => props.theme.colors.primary};
+ }
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+
+ /* Smooth transition for the SVG icon */
+ svg {
+ transition: all 0.3s ease;
+ }
+ }
+
+ @media (max-width: 768px) {
+ top: 15px;
+ right: 15px;
+
+ .theme-toggle__button {
+ width: 44px !important;
+ height: 44px !important;
+ }
+ }
+`
\ No newline at end of file
diff --git a/src/components/ThemeToggle/ThemeToggle.tsx b/src/components/ThemeToggle/ThemeToggle.tsx
new file mode 100644
index 00000000..8438b0bb
--- /dev/null
+++ b/src/components/ThemeToggle/ThemeToggle.tsx
@@ -0,0 +1,21 @@
+import { useTheme } from '@/contexts/ThemeContext'
+import { Button } from '@/components/Button'
+import { ThemeToggleWrapper } from './ThemeToggle.styled'
+
+export const ThemeToggle = () => {
+ const { theme, toggleTheme } = useTheme()
+
+ return (
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/ThemeToggle/index.ts b/src/components/ThemeToggle/index.ts
new file mode 100644
index 00000000..b22e9dd1
--- /dev/null
+++ b/src/components/ThemeToggle/index.ts
@@ -0,0 +1 @@
+export { ThemeToggle } from './ThemeToggle'
\ No newline at end of file
diff --git a/src/components/Typography/Text.tsx b/src/components/Typography/Text.tsx
new file mode 100644
index 00000000..2a687959
--- /dev/null
+++ b/src/components/Typography/Text.tsx
@@ -0,0 +1,37 @@
+import { StyledText } from './Typography.styled'
+import { TypographyProps } from './Typography.types'
+
+export const Text = ({
+ children,
+ size = 'md',
+ weight = 'normal',
+ color,
+ align = 'left',
+ className = '',
+ role,
+ ariaDescribedby,
+ ariaLevel,
+ ...rest
+}: TypographyProps & {
+ role?: string
+}) => {
+ // BEM classname
+ const bemClass = `text text--${size} text--${weight} text--${align}`
+ const fullClassName = `${bemClass} ${className}`.trim()
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/Typography/Title.tsx b/src/components/Typography/Title.tsx
new file mode 100644
index 00000000..ef0f9509
--- /dev/null
+++ b/src/components/Typography/Title.tsx
@@ -0,0 +1,37 @@
+import { StyledTitle } from './Typography.styled'
+import { TypographyProps } from './Typography.types'
+
+export const Title = ({
+ children,
+ size = 'md',
+ weight = 'bold',
+ color,
+ align = 'left',
+ className = '',
+ role,
+ as,
+ lang,
+ 'aria-level': ariaLevel,
+ ...rest
+}: TypographyProps & { role?: string; 'aria-level'?: number }) => {
+ // BEM classname
+ const bemClass = `title title--${size} title--${weight} title--${align}`
+ const fullClassName = `${bemClass} ${className}`.trim()
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/Typography/Typography.styled.ts b/src/components/Typography/Typography.styled.ts
new file mode 100644
index 00000000..28025a5b
--- /dev/null
+++ b/src/components/Typography/Typography.styled.ts
@@ -0,0 +1,28 @@
+import styled from 'styled-components'
+
+type StyledProps = {
+ $size?: string
+ $weight?: string
+ $color?: string
+ $align?: string
+}
+
+export const StyledText = styled.p`
+ font-family: var(--text-font-family);
+ font-size: var(--text-${props => props.$size || 'md'});
+ font-weight: var(--weight-${props => props.$weight || 'normal'});
+ color: ${props => (props.$color ? `${props.$color} !important` : 'var(--text-color)')};
+ text-align: ${props => props.$align || 'left'};
+ line-height: 1.6;
+ margin: 0;
+`
+
+export const StyledTitle = styled.h1`
+ font-family: var(--title-font-family);
+ font-size: var(--title-${props => props.$size || 'md'});
+ font-weight: var(--weight-${props => props.$weight || 'bold'});
+ color: ${props => (props.$color ? `${props.$color} !important` : 'var(--title-color)')};
+ text-align: ${props => props.$align || 'left'};
+ line-height: 1.2;
+ margin: 0;
+`
diff --git a/src/components/Typography/Typography.types.ts b/src/components/Typography/Typography.types.ts
new file mode 100644
index 00000000..8fb51247
--- /dev/null
+++ b/src/components/Typography/Typography.types.ts
@@ -0,0 +1,21 @@
+import { ReactNode } from 'react'
+
+export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'huge'
+
+export type Weight = 'light' | 'normal' | 'medium' | 'semibold' | 'bold'
+
+export type Align = 'left' | 'center' | 'right'
+
+export type TypographyProps = {
+ children: ReactNode
+ size?: Size
+ weight?: Weight
+ color?: string
+ align?: Align
+ className?: string
+ as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span'
+ lang?: string
+ id?: string
+ ariaDescribedby?: string
+ ariaLevel?: number
+}
diff --git a/src/components/Typography/index.ts b/src/components/Typography/index.ts
new file mode 100644
index 00000000..1278b133
--- /dev/null
+++ b/src/components/Typography/index.ts
@@ -0,0 +1,4 @@
+export { Text } from './Text'
+export { Title } from './Title'
+
+export type { TypographyProps, Size, Weight, Align } from './Typography.types'
diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx
new file mode 100644
index 00000000..df06a7b0
--- /dev/null
+++ b/src/contexts/ThemeContext.tsx
@@ -0,0 +1,60 @@
+import { createContext, useContext, useState, useEffect, ReactNode } from 'react'
+
+type ThemeMode = 'light' | 'dark'
+
+interface ThemeContextType {
+ theme: ThemeMode
+ toggleTheme: () => void
+}
+
+const ThemeContext = createContext(undefined)
+
+export const ThemeProvider = ({ children }: { children: ReactNode }) => {
+ const [theme, setTheme] = useState(() => {
+ const savedTheme = localStorage.getItem('theme') as ThemeMode | null
+ if (savedTheme) return savedTheme
+
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
+ return prefersDark ? 'dark' : 'light'
+ })
+
+ useEffect(() => {
+ const root = document.documentElement
+ root.setAttribute('data-theme', theme)
+ localStorage.setItem('theme', theme)
+ }, [theme])
+
+ useEffect(() => {
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
+
+ const handleSystemThemeChange = (e: MediaQueryListEvent) => {
+ // Always update theme when OS changes, override user preference
+ const newTheme = e.matches ? 'dark' : 'light'
+ setTheme(newTheme)
+ }
+
+ mediaQuery.addEventListener('change', handleSystemThemeChange)
+
+ return () => {
+ mediaQuery.removeEventListener('change', handleSystemThemeChange)
+ }
+ }, [])
+
+ const toggleTheme = () => {
+ setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light')
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export const useTheme = () => {
+ const context = useContext(ThemeContext)
+ if (!context) {
+ throw new Error('useTheme must be used within a ThemeProvider')
+ }
+ return context
+}
\ No newline at end of file
diff --git a/src/data/about.json b/src/data/about.json
new file mode 100644
index 00000000..57f68ba7
--- /dev/null
+++ b/src/data/about.json
@@ -0,0 +1,21 @@
+{
+ "intro": "Hi, I'm Daniel Lauding",
+ "name": "Daniel Lauding",
+ "role": "Design Engineer",
+ "phone": "+46739184410",
+ "email": "daniel@lauding.se",
+ "avatar_url": "/avatar.png",
+ "desc": [
+ "Daniel started making websites as a kid, driven by curiosity, tutorials, gaming communities, and a love for building things. He created team sites, tweaked graphics, and constantly rebuilt his own corner of the internet with every new trend or effect he discovered.",
+ "That early curiosity carried him into product design, startups, and exploring every new wave of technology. He has always been drawn to the mix of creativity, interactivity, and the ability to shape something people actually use.",
+ "Today, Daniel is sharpening his JavaScript skills and embracing AI to prototype faster, solve problems smarter, and push his design craft further β ultimately aiming to build more efficient and meaningful experiences that genuinely help people.",
+ "At his core, Daniel is a builder β whether through design or code, it's all about turning ideas into something real and useful."
+ ],
+ "socialLinks": {
+ "linkedin": "https://www.linkedin.com/in/daniellauding/",
+ "github": "https://github.com/daniel-lauding",
+ "stackoverflow": "https://stackoverflow.com/users/28012030/daniel-lauding",
+ "twitter": "https://x.com/daniellauding",
+ "instagram": "https://www.instagram.com/daniellauding/"
+ }
+}
diff --git a/src/data/articles.json b/src/data/articles.json
new file mode 100644
index 00000000..a3ed3062
--- /dev/null
+++ b/src/data/articles.json
@@ -0,0 +1,24 @@
+{
+ "articles": [
+ {
+ "id": "1",
+ "title": "My Path Wasn't Linear, But It Makes Sense Now",
+ "excerpt": "A reflection on my unconventional journey from pixel-perfect IE6 implementations to modern React development, and how every detour added value to my skillset.",
+ "date": "2025-11-29",
+ "image": "/articles/thumb_01.png",
+ "readTime": "7 min read",
+ "link": "https://medium.com/@daniellauding/my-path-wasnt-linear-but-it-makes-sense-now-408dc1aeb47e",
+ "tags": ["Career", "Frontend", "Personal Growth"]
+ },
+ {
+ "id": "2",
+ "title": "From Lego Bricks to Digital Products: My Journey as a Builder",
+ "excerpt": "How my childhood passion for building with Lego blocks evolved into crafting digital experiences, and why the fundamental joy of creation remains the same.",
+ "date": "2025-11-16",
+ "image": "/articles/thumb_02.png",
+ "readTime": "6 min read",
+ "link": "https://medium.com/@daniellauding/from-lego-bricks-to-digital-products-my-journey-as-a-builder-51a07ac6624e",
+ "tags": ["Design", "Development", "Creativity"]
+ }
+ ]
+}
diff --git a/src/data/cv.json b/src/data/cv.json
new file mode 100644
index 00000000..73e8f77a
--- /dev/null
+++ b/src/data/cv.json
@@ -0,0 +1,86 @@
+{
+ "experience": [
+ {
+ "company": "FrΓΆjd Interactive",
+ "companyUrl": "https://www.frojd.se",
+ "role": "Frontend Design Consultant",
+ "period": "2025",
+ "description": "Frontend consultant delivering sustainable web solutions for major Swedish organizations. Implemented Storybook design system components for Energiforsk's energy-efficient website using Next.js, contributing to 75% lighter page loads. Built Vue.js frontend for GavlegΓ₯rdarna's housing platform serving 30,000 residents with WCAG AA compliance. Developed Edenred's benefits platform migration from Episerver to WordPress with Google API integration.",
+ "type": "contract"
+ },
+ {
+ "company": "instinctly AB",
+ "companyUrl": "https://www.instinctly.se",
+ "role": "Frontend Developer & Design Consultant",
+ "period": "2017-Present",
+ "description": "Running frontend development consultancy specializing in React, TypeScript, and modern JavaScript frameworks. Leading frontend development projects throughout 2025 with focus on component-driven architecture, performance optimization, and accessibility. Working extensively with AI-assisted development tools like Cursor and ChatGPT to accelerate development workflows and deliver high-quality responsive web applications.",
+ "type": "founder"
+ },
+ {
+ "company": "Asteria AB",
+ "companyUrl": "https://www.asteria.ai",
+ "role": "Frontend Developer & Product Designer",
+ "period": "2017-2025",
+ "description": "Co-founded fintech startup where I handled all frontend design implementation and development using React and implemented design systems with Storybook. Built the entire frontend design for Smart Cash Flow platform, creating reusable components and establishing development workflows. Combined product design and frontend development to rapidly prototype and iterate on features.",
+ "type": "founder"
+ },
+ {
+ "company": "Valla Media",
+ "role": "Frontend Developer",
+ "period": "2010",
+ "description": "Worked on one of Scandinavia's biggest online bingo platforms. Developed frontend features using HTML, CSS and worked closely with developers and designers implementing responsive layouts. Used Bootstrap framework and Adobe Photoshop for frontend asset optimization.",
+ "type": "parttime"
+ },
+ {
+ "company": "Futurniture",
+ "companyUrl": "https://www.futurniture.se",
+ "role": "Frontend Developer",
+ "period": "2009",
+ "description": "Created campaign websites and portals for major clients including IKEA and Duni. Built custom WordPress themes and developed interactive frontend experiences for IKEA's marketing campaigns. Focused on clean HTML/CSS implementation, cross-browser compatibility, and performance optimization. Collaborated with designers to transform mockups into responsive web interfaces that handled high traffic volumes during product launches.",
+ "type": "fulltime"
+ },
+ {
+ "company": "Drumedar",
+ "companyUrl": "https://web.archive.org/web/20091124222733/http://www.drumedar.se/",
+ "role": "Frontend Developer",
+ "period": "2008-2009",
+ "description": "Frontend developer on teams building campaign websites for Swedish media and sports organizations during the IE6 era. Contributed to Svenska IshockeyfΓΆrbundet (Swedish Ice Hockey Federation) website redesign on EPiServer platform. Part of the team building mktwebb - a shared technical platform serving 40+ newspapers under mktmedia, Sweden's largest local newspaper consortium. Mastered pixel-perfect implementations from Photoshop designs, writing 3000+ lines of CSS. Specialized in HTML/CSS development with Polopoly CMS and ensured browser compatibility for IE6/7/8.",
+ "type": "fulltime"
+ }
+ ],
+ "education": [
+ {
+ "school": "Technigo",
+ "schoolUrl": "https://www.technigo.io",
+ "program": "JavaScript Bootcamp",
+ "period": "2025-2026",
+ "description": "Intensive frontend development bootcamp focusing on modern JavaScript and React."
+ },
+ {
+ "school": "Folkuniversitetet",
+ "schoolUrl": "https://www.folkuniversitetet.se",
+ "program": "React.js YH Course",
+ "period": "2025",
+ "description": "10-week intensive course covering React, Redux, and React Router."
+ },
+ {
+ "school": "Furuboda FolkhΓΆgskola",
+ "schoolUrl": "https://www.furuboda.se",
+ "program": "Music Production",
+ "period": "2023-2024",
+ "description": "Remote music production course covering Ableton, sound design, and music theory."
+ },
+ {
+ "school": "Hyper Island",
+ "schoolUrl": "https://www.hyperisland.com",
+ "program": "Digital Media Design",
+ "period": "2009-2011",
+ "description": "Design education focused on digital media and interactive experiences."
+ }
+ ],
+ "links": {
+ "portfolio": "https://daniellauding.com",
+ "linkedin": "https://linkedin.com/in/daniellauding",
+ "github": "https://github.com/daniellauding"
+ }
+}
\ No newline at end of file
diff --git a/src/data/icons.json b/src/data/icons.json
new file mode 100644
index 00000000..2ccce18c
--- /dev/null
+++ b/src/data/icons.json
@@ -0,0 +1,15 @@
+{
+ "ArrowDown": " ",
+ "Close": " ",
+ "Github": " ",
+ "Globe": " ",
+ "Doc": " ",
+ "StackOverflow": " ",
+ "Twitter": " ",
+ "LinkedIn": " ",
+ "Instagram": " ",
+ "Logo": " ",
+ "External": " ",
+ "Sun": " ",
+ "Moon": " "
+}
diff --git a/src/data/projects.json b/src/data/projects.json
index 7c426028..54b70d4c 100644
--- a/src/data/projects.json
+++ b/src/data/projects.json
@@ -1,28 +1,119 @@
{
"projects": [
{
- "name": "Business site",
- "image": "https://images.unsplash.com/photo-1557008075-7f2c5efa4cfd?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2497&q=80",
- "tags": [
- "HTML5",
- "CSS3",
- "JavaScript"
- ],
- "netlify": "link",
- "github": "link"
- },
- {
- "name": "Weather app",
- "image": "https://images.unsplash.com/photo-1520792532857-293bd046307a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2370&q=80",
- "tags": [
- "HTML5",
- "CSS3",
- "JavaScript",
- "TypeScript",
- "APIs"
- ],
- "netlify": "link",
- "github": "link"
+ "name": "Energiforsk - Energieffektiv webb",
+ "date": "2025",
+ "description": "Frontend development consultant for Sweden's most energy-efficient website, achieving 75% lighter page loads than previous site. Built with Next.js and headless CMS using code splitting and minification. Implemented Storybook design system components as part of the team creating sustainable web architecture hosted on fossil-free servers.",
+ "image": "/projects/thumb_energiforsk.png",
+ "tags": ["HTML5", "CSS3", "Storybook", "Next.js", "Sustainability", "Performance"],
+ "link": "https://energiforsk.se/"
+ },
+ {
+ "name": "GavlegΓ₯rdarna - Housing Platform",
+ "date": "2025",
+ "description": "Frontend development for Sweden's largest public housing company serving 30,000 residents. Built Vue.js frontend with WordPress backend, featuring apartment search with advanced filtering, booking systems, and accessible user portal. Implemented WCAG AA compliance and modern UX design.",
+ "image": "/projects/thumb_gavlegardarna.png",
+ "tags": ["HTML5", "CSS3", "Vue.js", "WordPress", "WCAG AA", "Real Estate", "User Portal"],
+ "link": "https://www.gavlegardarna.se/"
+ },
+ {
+ "name": "Edenred - Benefits Platform",
+ "date": "2025",
+ "description": "Frontend development for unified lunch and wellness benefits platform. Migrated from Episerver to WordPress, integrated Google APIs, and built responsive Vue.js components for seamless user experience across desktop and mobile.",
+ "image": "/projects/thumb_edenred.png",
+ "tags": ["HTML5", "CSS3", "Vue.js", "WordPress", "Google APIs", "Platform Migration", "Benefits"],
+ "link": "https://www.edenred.se/sveriges-basta-lunchrestaurang/"
+ },
+ {
+ "name": "Christmas JS Calendar Challenges",
+ "date": "2025",
+ "description": "A dynamic weather application for Swedish cities using SMHI API, featuring real-time forecasts, geolocation support, and theme switching based on conditions. Includes 7-day predictions, sunrise/sunset times, and animated weather effects.",
+ "tags": ["HTML5", "CSS3", "TypeScript", "SMHI API", "Geolocation API"],
+ "image": "/projects/thumb_xmas.png",
+ "codepen": "https://codepen.io/daniellauding/pen/gbPXLVR"
+ },
+ {
+ "name": "Responsive Real Estate Landing Page",
+ "date": "2024",
+ "description": "A modern, minimal landing page for a Stockholm apartment rental featuring responsive design, CSS Grid layouts, automated review carousels, and a validated inquiry form. Built with Apple-inspired aesthetics and mobile-first approach.",
+ "image": "/projects/thumb_business.png",
+ "tags": ["HTML5", "CSS3", "JavaScript", "Responsive Design"],
+ "netlify": "https://apt.daniellauding.se/",
+ "github": "https://github.com/daniellauding/technigo-js-project-business-site"
+ },
+ {
+ "name": "Accessible Quiz Application",
+ "date": "2024",
+ "description": "An interactive quiz application with full accessibility support, featuring keyboard navigation, ARIA labels, and WCAG compliance. Fetches quiz data from API with category filtering, instant feedback, and score tracking.",
+ "image": "/projects/thumb_accessibility.png",
+ "tags": ["TypeScript", "Tailwind CSS", "REST API", "HTML5", "CSS3", "LocalStorage API", "Geolocation API", "Accessibility (WCAG)", "PostGres"],
+ "netlify": "https://quizbonanza.netlify.app/",
+ "github": "https://github.com/daniellauding/js-project-accessibility"
+ },
+ {
+ "name": "Dynamic Recipe Discovery App",
+ "date": "2024",
+ "description": "A data-driven recipe application integrating Spoonacular API with advanced filtering, sorting, and search capabilities. Features include favorites system with localStorage persistence, smart caching, and a sliding filter sidebar.",
+ "image": "/projects/thumb_recipe.png",
+ "tags": ["JavaScript (ES6+)", "Spoonacular API", "LocalStorage API", "HTML5", "CSS3"],
+ "netlify": "https://recipe.daniellauding.se/",
+ "github": "https://github.com/daniellauding/js-project-recipe-library"
+ },
+ {
+ "name": "TypeScript Weather Forecast App",
+ "date": "2024",
+ "description": "A dynamic weather application for Swedish cities using SMHI API, featuring real-time forecasts, geolocation support, and theme switching based on conditions. Includes 7-day predictions, sunrise/sunset times, and animated weather effects.",
+ "image": "/projects/thumb_weather.png",
+ "tags": ["HTML5", "CSS3", "TypeScript", "SMHI API", "Geolocation API"],
+ "netlify": "https://js-project-weather-app.netlify.app/",
+ "github": "https://github.com/daniellauding/js-project-weather-app"
+ },
+ {
+ "name": "TinyChat",
+ "date": "2010",
+ "description": "WordPress blog development for TinyChat platform. Created custom WordPress theme and implemented blog functionality for the video chat service's content platform. Focused on clean design, SEO optimization, and content management features.",
+ "image": "/projects/thumb_tinychat.png",
+ "tags": ["HTML", "CSS", "WordPress", "Blog Development", "PHP", "CSS3", "Content Management"]
+ },
+ {
+ "name": "IKEA Stimulanspaketresan",
+ "date": "2009",
+ "description": "Frontend development for IKEA's Sweden tour campaign website. Built interactive experience following IKEA's 10-day caravan journey from Γlmhult to Stockholm. Featured behind-the-scenes content, travel diary, and interactive elements. Pixel-perfect implementation from Photoshop designs with IE6/7/8 compatibility and cross-browser CSS optimization.",
+ "image": "/projects/thumb_ikea_01.png",
+ "tags": ["HTML", "CSS", "Wordpress", "Campaign Site"],
+ "link": "https://webbsverige.wordpress.com/2009/07/16/stimulanspaketresan-ikea/#comments"
+ },
+ {
+ "name": "IKEA.se/tv",
+ "date": "2009",
+ "description": "Frontend development for IKEA's user-generated content platform where customers shared interior design tips through videos and images. Built interactive features for video upload, rating system, and commenting functionality. Implemented responsive video player and social interaction features using JavaScript and CSS.",
+ "image": "/projects/thumb_ikea_02.png",
+ "tags": ["HTML", "CSS", "User-Generated Content", "Wordpress"],
+ "link": "https://webbsverige.wordpress.com/2008/08/14/ikeasetv-ikea/#comments"
+ },
+ {
+ "name": "100% Ren HΓ₯rdtrΓ€ning",
+ "date": "2009",
+ "description": "Frontend development for Stockholm County Council's anti-doping campaign website. Built interactive training platform featuring Musse Hasselvall demonstrating proper workout techniques. Developed engaging web experience to promote clean training over illegal performance enhancers. Collaborated with large creative team including Flash animations and web development.",
+ "image": "/projects/thumb_hundra.png",
+ "tags": ["HTML", "CSS", "Flash", "Campaign Site"],
+ "link": "https://webbsverige.wordpress.com/2009/04/01/100-stockholms-lans-landsting/#respond"
+ },
+ {
+ "name": "Svenska IshockeyfΓΆrbundet",
+ "date": "2009",
+ "description": "Frontend development for Swedish Ice Hockey Federation's complete website redesign (www.swehockey.se). Built modern, scalable frontend on EPiServer platform as part of Swedish Sports Federation's unified digital infrastructure. Delivered fresh design with improved usability and commercial functionality. Focused on long-term maintainability and cross-browser compatibility.",
+ "image": "/projects/thumb_swe_hockey.png",
+ "tags": ["EPiServer", "HTML", "CSS", "Sports Federation"],
+ "link": "https://web.archive.org/web/20100411030032/https://www.swehockey.se/"
+ },
+ {
+ "name": "mktwebb - 40 Newspaper Platform",
+ "date": "2007-2009",
+ "description": "Interface Developer for Sweden's largest shared technical platform serving 40+ local newspapers under mktmedia. Built scalable frontend architecture using Polopoly CMS with 3000+ lines of CSS and extensive IE6 compatibility. Led JavaScript development and mentored team members. Collaborated with backend programmers, UX researchers, and designers to deliver cost-effective digital publishing solution.",
+ "image": "/projects/thumb_ttela.png",
+ "tags": ["Polopoly CMS", "CSS", "HTML", "IE6 Compatibility", "Large Scale Platform"],
+ "link": "https://web.archive.org/web/20120113041817/http://ttela.se/"
}
]
}
\ No newline at end of file
diff --git a/src/data/skills.json b/src/data/skills.json
new file mode 100644
index 00000000..1da3b33f
--- /dev/null
+++ b/src/data/skills.json
@@ -0,0 +1,79 @@
+{
+ "skills": [
+ {
+ "category": "Code",
+ "items": [
+ "React.js",
+ "React Native",
+ "Vue.js",
+ "Next.js",
+ "Redux",
+ "React Router",
+ "TypeScript",
+ "JavaScript ES6+",
+ "HTML5",
+ "CSS3",
+ "Tailwind CSS",
+ "Bootstrap",
+ "Styled Components",
+ "LESS/SASS",
+ "Expo",
+ "Storybook"
+ ]
+ },
+ {
+ "category": "UX & UI",
+ "items": [
+ "Product Design",
+ "UI/UX Design",
+ "Figma",
+ "Sketch",
+ "Adobe Creative Suite",
+ "Prototyping",
+ "Wireframing",
+ "Design Systems",
+ "Mobile & Web Design",
+ "User Testing",
+ "Heuristic Evaluation",
+ "Information Architecture",
+ "Accessibility (WCAG)",
+ "Design Sprints"
+ ]
+ },
+ {
+ "category": "Backend & Tools",
+ "items": [
+ "Node.js",
+ "Supabase",
+ "PostgreSQL",
+ "REST APIs",
+ "GraphQL",
+ "WordPress",
+ "Headless CMS",
+ "Git/GitHub",
+ "Docker",
+ "Vite",
+ "DNS Management",
+ "Hosting Services"
+ ]
+ },
+ {
+ "category": "Innovation",
+ "items": [
+ "AI-assisted Development",
+ "Cursor AI",
+ "Lovable",
+ "ChatGPT/GPT-4",
+ "Gamification",
+ "Growth Strategy",
+ "Startup Development",
+ "Business Strategy",
+ "Workshop Facilitation",
+ "Agile/Scrum",
+ "Cross-team Collaboration",
+ "Stakeholder Management",
+ "Leadership"
+ ]
+ }
+ ]
+}
diff --git a/src/data/stack.json b/src/data/stack.json
new file mode 100644
index 00000000..46ce942c
--- /dev/null
+++ b/src/data/stack.json
@@ -0,0 +1,4 @@
+{
+ "title": "Tech",
+ "desc": "HTML5, CSS3, JavaScript (ES6), TypeScript, React, React Native, Vue.js, Redux, React Router, Next.js, Node.js, Express, RESTful APIs, GraphQL, Tailwind CSS, Bootstrap, Styled Components, Storybook, Web Accessibility (WCAG), Figma, Sketch, Adobe Creative Suite, Git, GitHub, Supabase, Expo, WordPress, Headless CMS, EPiServer, Polopoly CMS, Google APIs, AI-assisted Development (Cursor, Lovable, ChatGPT), Agile/Scrum, Design Systems, Mobile & Web Design, UX/UI Design, Prototyping, Wireframing, User Testing, Performance Optimization, Sustainable Web Development."
+}
diff --git a/src/index.css b/src/index.css
index 61010be6..12c3162c 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,4 +1,283 @@
+@import url('https://fonts.googleapis.com/css2?family=Hind:wght@300;400;500;600;700&family=Montserrat:wght@400;500;600;700&display=swap');
+@import 'animate.css';
+
+/* === LIGHT THEME (DEFAULT) === */
+:root {
+ /* === FONTS === */
+ --title-font-family: 'Montserrat', sans-serif;
+ --text-font-family: 'Hind', sans-serif;
+
+ /* === BRAND === */
+ --color-primary: #36816B;
+ --color-secondary: #D36755;
+ --color-tertiary: #F5F5F5;
+ --color-icon: #333333;
+
+ /* === BASE COLORS === */
+ --bg-color: #ffffff;
+ --title-color: #36816B;
+ --text-color: #1F2937;
+ --text-muted: #6B7280;
+ --border-color: #E5E7EB;
+ --card-bg: #ffffff;
+ --card-shadow: rgba(0, 0, 0, 0.1);
+
+ --chip-bg-default: #f8f9fa;
+ --chip-bg-active: #E4D476;
+ --chip-bg-hover: var(--chip-bg-active);
+
+ --tag-bg-color: var(--text-color);
+ --tag-border-color: var(--border-color);
+ --tag-text-color: #fff;
+
+ --bg-marquee: var(--color-secondary);
+
+ /* === SECTION: HERO === */
+ --section-hero-bg-color: var(--bg-color);
+ --section-hero-title-color: var(--title-color);
+ --section-hero-text-color: var(--text-color);
+
+ /* === SECTION: TECH === */
+ --section-tech-bg-color: #E6DAC0;
+ --section-tech-title-color: var(--title-color);
+ --section-tech-text-color: var(--text-color);
+
+ /* === SECTION: PROJECTS === */
+ --section-projects-bg-color: #ffffff;
+ --section-projects-title-color: var(--title-color);
+ --section-projects-text-color: var(--text-color);
+
+ /* === SECTION: ARTICLES === */
+ --section-articles-bg-color: #BFD9D1;
+ --section-articles-title-color: #0D5C3F;
+ --section-articles-text-color: var(--text-color);
+
+ /* === SECTION: SKILLS === */
+ --section-skills-bg-color: #6B5632;
+ --section-skills-title-color: #ffffff;
+ --section-skills-text-color: #ffffff;
+
+ /* === SECTION: FOOTER === */
+ --section-footer-bg-color: #ffffff;
+ --section-footer-title-color: var(--title-color);
+ --section-footer-text-color: var(--text-color);
+
+ /* === SECTION: CV === */
+ --section-cv-bg-color: #F5F5F7;
+ --section-cv-title-color: var(--title-color);
+ --section-cv-text-color: var(--text-color);
+
+ /* === TITLE SIZES === */
+ --title-xs: 20px;
+ --title-sm: 24px;
+ --title-md: 32px;
+ --title-lg: 40px;
+ --title-xl: 80px;
+ --title-xxl: 64px;
+ --title-huge: 80px;
+
+ /* === TEXT SIZES === */
+ --text-xs: 12px;
+ --text-sm: 14px;
+ --text-md: 16px;
+ --text-lg: 18px;
+ --text-xl: 20px;
+ --text-xxl: 24px;
+ --text-huge: 32px;
+
+ /* === FONT WEIGHTS === */
+ --weight-light: 200;
+ --weight-normal: 400;
+ --weight-medium: 500;
+ --weight-semibold: 600;
+ --weight-bold: 700;
+
+ /* === SPACING === */
+ --spacing-xs: 4px;
+ --spacing-sm: 8px;
+ --spacing-md: 16px;
+ --spacing-lg: 24px;
+ --spacing-xl: 32px;
+ --spacing-xxl: 48px;
+ --spacing-huge: 128px;
+
+ /* === BORDER RADIUS === */
+ --radius-sm: 4px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+ --radius-round: 50%;
+}
+
+/* === DARK THEME === */
+[data-theme="dark"] {
+ /* === BRAND === */
+ --color-primary: #36816b;
+ --color-secondary: #D36755;
+ --color-tertiary: #F5F5F5;
+ --color-icon: #9CA3AF;
+
+ /* === BASE COLORS === */
+ --bg-color: #0F1714;
+ --title-color: #36816b;
+ --text-color: #E5E7EB;
+ --text-muted: #9CA3AF;
+ --border-color: #374151;
+ --card-bg: transparent;
+ --card-shadow: rgba(0, 0, 0, 0.3);
+
+ --chip-bg-default: rgba(255, 255, 255, 0.05);
+ --chip-bg-active: rgba(255, 255, 255, 0.3);
+ --chip-bg-hover: var(--chip-bg-active);
+
+ --tag-bg-color: rgba(255, 255, 255, 0.05);
+ --tag-border-color: rgba(255, 255, 255, 0.05);
+ --tag-text-color: #fff;
+
+ --bg-marquee: var(--color-primary);
+
+ /* === SECTION: HERO === */
+ --section-hero-bg-color: var(--bg-color);
+ --section-hero-title-color: var(--title-color);
+ --section-hero-text-color: var(--text-color);
+
+ /* === SECTION: TECH === */
+ --section-tech-bg-color: var(--bg-color);
+ --section-tech-title-color: var(--title-color);
+ --section-tech-text-color: var(--text-color);
+
+ /* === SECTION: PROJECTS === */
+ --section-projects-bg-color: var(--bg-color);
+ --section-projects-title-color: var(--title-color);
+ --section-projects-text-color: var(--text-color);
+
+ /* === SECTION: ARTICLES === */
+ --section-articles-bg-color: var(--bg-color);
+ --section-articles-title-color: var(--title-color);
+ --section-articles-text-color: var(--text-color);
+
+ /* === SECTION: SKILLS === */
+ --section-skills-bg-color: var(--bg-color);
+ --section-skills-title-color: var(--title-color);
+ --section-skills-text-color: var(--text-color);
+
+ /* === SECTION: FOOTER === */
+ --section-footer-bg-color: var(--bg-color);
+ --section-footer-title-color: var(--title-color);
+ --section-footer-text-color: var(--text-color);
+
+ /* === SECTION: CV === */
+ --section-cv-bg-color: var(--bg-color);
+ --section-cv-title-color: var(--title-color);
+ --section-cv-text-color: var(--text-color);
+}
+
+/* === TABLET BREAKPOINT (768px - 1023px) === */
+@media (max-width: 1023px) {
+ :root {
+ /* Title sizes for tablet */
+ --title-xs: 18px;
+ --title-sm: 20px;
+ --title-md: 24px;
+ --title-lg: 32px;
+ --title-xl: 56px;
+ --title-xxl: 64px;
+ --title-huge: 80px;
+
+ /* Text sizes for tablet */
+ --text-xs: 12px;
+ --text-sm: 14px;
+ --text-md: 16px;
+ --text-lg: 18px;
+ --text-xl: 18px;
+ --text-xxl: 20px;
+ --text-huge: 28px;
+ }
+}
+
+/* === MOBILE BREAKPOINT (<768px) === */
+@media (max-width: 767px) {
+ :root {
+ /* Title sizes for mobile */
+ --title-xs: 16px;
+ --title-sm: 16px;
+ --title-md: 18px;
+ --title-lg: 20px;
+ --title-xl: 24px;
+ --title-xxl: 28px;
+ --title-huge: 32px;
+
+ /* Text sizes for mobile */
+ --text-xs: 11px;
+ --text-sm: 13px;
+ --text-md: 14px;
+ --text-lg: 16px;
+ --text-xl: 17px;
+ --text-xxl: 18px;
+ --text-huge: 24px;
+ }
+}
+
+::selection {
+ background: rgba(19, 144, 105, 0.15);
+ color: var(--title-color);
+}
+
+/* === GLOBAL STYLES === */
+html {
+ scroll-behavior: smooth;
+ scroll-padding-top: 20px;
+}
+
body {
- background: pink;
- color: hotpink;
-}
\ No newline at end of file
+ margin: 0;
+ padding: 0;
+ background: var(--bg-color);
+ color: var(--text-color);
+ font-family: var(--text-font-family);
+}
+
+/* === BOX SIZING === */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+ul {
+ list-style: none;
+ list-style-type: none;
+}
+
+/* Screen reader only - hides visually but keeps for screen readers */
+.sr-only {
+ position: absolute !important;
+ width: 1px !important;
+ height: 1px !important;
+ padding: 0 !important;
+ margin: -1px !important;
+ overflow: hidden !important;
+ clip: rect(0, 0, 0, 0) !important;
+ white-space: nowrap !important;
+ border: 0 !important;
+}
+
+.primary {
+ color: var(--color-primary);
+}
+
+.secondary {
+ color: var(--color-secondary);
+}
+
+.tertiary {
+ color: var(--color-tertiary);
+}
+
+.icon, .icon span {
+ color: var(--color-icon);
+}
+
+a:hover.icon, a:hover.icon span {
+ color: var(--title-color);
+}
+
diff --git a/src/main.jsx b/src/main.jsx
index ed109d76..d37526f3 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -8,5 +8,5 @@ import './index.css'
createRoot(document.getElementById('root')).render(
- ,
+
)
diff --git a/src/styled.d.ts b/src/styled.d.ts
new file mode 100644
index 00000000..1f054e83
--- /dev/null
+++ b/src/styled.d.ts
@@ -0,0 +1,6 @@
+import 'styled-components'
+import type { Theme } from './theme/theme'
+
+declare module 'styled-components' {
+ export interface DefaultTheme extends Theme {}
+}
diff --git a/src/theme/index.ts b/src/theme/index.ts
new file mode 100644
index 00000000..f618ce69
--- /dev/null
+++ b/src/theme/index.ts
@@ -0,0 +1,2 @@
+export { theme, getThemeValue } from './theme'
+export type { Theme } from './theme'
diff --git a/src/theme/theme.ts b/src/theme/theme.ts
new file mode 100644
index 00000000..1fa7aaef
--- /dev/null
+++ b/src/theme/theme.ts
@@ -0,0 +1,132 @@
+export const theme = {
+ fonts: {
+ title: 'var(--title-font-family)',
+ text: 'var(--text-font-family)',
+ },
+
+ titleSizes: {
+ xs: 'var(--title-xs)', // 20px
+ sm: 'var(--title-sm)', // 24px
+ md: 'var(--title-md)', // 32px
+ lg: 'var(--title-lg)', // 40px
+ xl: 'var(--title-xl)', // 48px
+ xxl: 'var(--title-xxl)', // 56px
+ huge: 'var(--title-huge)', // 80px
+ },
+
+ textSizes: {
+ xs: 'var(--text-xs)', // 12px
+ sm: 'var(--text-sm)', // 14px
+ md: 'var(--text-md)', // 16px
+ lg: 'var(--text-lg)', // 18px
+ xl: 'var(--text-xl)', // 20px
+ xxl: 'var(--text-xxl)', // 24px
+ huge: 'var(--text-huge)', // 32px
+ },
+
+ weights: {
+ normal: 'var(--weight-normal)', // 400
+ medium: 'var(--weight-medium)', // 500
+ semibold: 'var(--weight-semibold)', // 600
+ bold: 'var(--weight-bold)', // 700
+ },
+
+ colors: {
+ bg: 'var(--bg-color)',
+ title: 'var(--title-color)',
+ text: 'var(--text-color)',
+ primary: 'var(--title-color)',
+ secondary: 'var(--color-secondary)',
+ tertiary: 'var(--color-tertiary)',
+ icon: 'var(--color-icon)',
+ },
+
+ sections: {
+ hero: {
+ bg: 'var(--section-hero-bg-color)',
+ title: 'var(--section-hero-title-color)',
+ text: 'var(--section-hero-text-color)',
+ },
+ tech: {
+ bg: 'var(--section-tech-bg-color)',
+ title: 'var(--section-tech-title-color)',
+ text: 'var(--section-tech-text-color)',
+ },
+ projects: {
+ bg: 'var(--section-projects-bg-color)',
+ title: 'var(--section-projects-title-color)',
+ text: 'var(--section-projects-text-color)',
+ },
+ articles: {
+ bg: 'var(--section-articles-bg-color)',
+ title: 'var(--section-articles-title-color)',
+ text: 'var(--section-articles-text-color)',
+ },
+ skills: {
+ bg: 'var(--section-skills-bg-color)',
+ title: 'var(--section-skills-title-color)',
+ text: 'var(--section-skills-text-color)',
+ },
+ footer: {
+ bg: 'var(--section-footer-bg-color)',
+ title: 'var(--section-footer-title-color)',
+ text: 'var(--section-footer-text-color)',
+ },
+ cv: {
+ bg: 'var(--section-cv-bg-color)',
+ title: 'var(--section-cv-title-color)',
+ text: 'var(--section-cv-text-color)',
+ },
+ },
+
+ spacing: {
+ xs: 'var(--spacing-xs)', // 4px
+ sm: 'var(--spacing-sm)', // 8px
+ md: 'var(--spacing-md)', // 16px
+ lg: 'var(--spacing-lg)', // 24px
+ xl: 'var(--spacing-xl)', // 32px
+ xxl: 'var(--spacing-xxl)', // 48px
+ huge: 'var(--spacing-huge)', // 124px
+ },
+
+ radius: {
+ sm: 'var(--radius-sm)', // 4px
+ md: 'var(--radius-md)', // 8px
+ lg: 'var(--radius-lg)', // 12px
+ round: 'var(--radius-round)', // 50%
+ },
+
+ breakpoints: {
+ mobile: 'var(--breakpoint-mobile)', // 480px
+ tablet: 'var(--breakpoint-tablet)', // 768px
+ desktop: 'var(--breakpoint-desktop)', // 1024px
+ wide: 'var(--breakpoint-wide)', // 1200px
+ },
+
+
+ media: {
+ mobile: `@media (min-width: 480px)`,
+ tablet: `@media (min-width: 768px)`,
+ desktop: `@media (min-width: 1024px)`,
+ wide: `@media (min-width: 1200px)`,
+
+ maxMobile: `@media (max-width: 479px)`,
+ maxTablet: `@media (max-width: 767px)`,
+ maxDesktop: `@media (max-width: 1023px)`,
+ },
+}
+
+export type Theme = typeof theme
+
+export const getThemeValue = (path: string): string | undefined => {
+ const keys = path.split('.')
+
+ let value: any = theme
+
+ for (const key of keys) {
+ value = value[key]
+ if (value === undefined) return undefined
+ }
+
+ return value
+}
diff --git a/src/views/Home.jsx b/src/views/Home.jsx
new file mode 100644
index 00000000..3e59601d
--- /dev/null
+++ b/src/views/Home.jsx
@@ -0,0 +1,41 @@
+import { SkipLink } from '@/components/SkipLink'
+import { Hero } from '@/components/Hero'
+import { TechStack } from '@/components/TechStack'
+import { Title } from '@/components/Typography'
+import { Projects } from '@/components/Projects'
+import { Articles } from '@/components/Articles'
+import { Skills } from '@/components/Skills'
+import { CV } from '@/components/CV'
+import { Footer } from '@/components/Footer'
+import aboutData from '@/data/about.json'
+import stackData from '@/data/stack.json'
+import projectsData from '@/data/projects.json'
+import articlesData from '@/data/articles.json'
+import skillsData from '@/data/skills.json'
+
+export const Home = () => (
+ <>
+
+
+
+
+
+
+
+
+ >
+)
\ No newline at end of file
diff --git a/src/views/StyleGuide.jsx b/src/views/StyleGuide.jsx
new file mode 100644
index 00000000..baafe277
--- /dev/null
+++ b/src/views/StyleGuide.jsx
@@ -0,0 +1,957 @@
+import { useState } from 'react';
+import { Button } from '@/components/Button';
+import { Card } from '@/components/Card';
+import { Grid, GridItem } from '@/components/Grid';
+import { Tag } from '@/components/Tag';
+import { Icon } from '@/components/Icon';
+import { Image, Avatar } from '@/components/Image';
+import { Text, Title } from '@/components/Typography';
+import { Section } from '@/components/Section';
+import { SkipLink } from '@/components/SkipLink';
+import { ProjectCard } from '@/components/Projects/ProjectCard';
+import { ArticleCard } from '@/components/Articles/ArticleCard';
+import { Hero } from '@/components/Hero';
+import { Footer } from '@/components/Footer';
+import { ScrollIndicator } from '@/components/ScrollIndicator';
+import { Skills } from '@/components/Skills';
+import { CV } from '@/components/CV';
+import { TechStack } from '@/components/TechStack';
+import { ThemeToggle } from '@/components/ThemeToggle';
+import { Articles } from '@/components/Articles';
+import styled from 'styled-components';
+
+const StyleGuideContainer = styled.div`
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: var(--spacing-xl);
+ font-family: var(--text-font-family);
+`;
+
+const Navigation = styled.nav`
+ position: sticky;
+ top: 20px;
+ background: #f8f9fa;
+ border-radius: 12px;
+ padding: 16px;
+ margin: 32px 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ border: 1px solid #e9ecef;
+ z-index: 100;
+`;
+
+const NavItem = styled.a`
+ padding: 8px 12px;
+ background: white;
+ border: 1px solid #dee2e6;
+ border-radius: 6px;
+ text-decoration: none;
+ color: var(--text-color);
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: var(--title-color);
+ color: white;
+ }
+`;
+
+const ComponentSection = styled.section`
+ margin: 48px 0;
+ padding-top: 24px;
+ border-top: 1px solid #e9ecef;
+
+ &:first-of-type {
+ border-top: none;
+ }
+`;
+
+const ExampleGrid = styled.div`
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 32px;
+ margin: 32px 0;
+
+ @media (min-width: 768px) {
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
+ }
+`;
+
+const ExampleCard = styled.div`
+ border: 1px solid #e9ecef;
+ border-radius: 12px;
+ padding: 24px;
+ background: white;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+`;
+
+const Preview = styled.div`
+ background: #f8f9fa;
+ border: 1px solid #e9ecef;
+ border-radius: 8px;
+ padding: 24px;
+ margin: 16px 0;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 16px;
+
+ > * {
+ margin: 0;
+ }
+`;
+
+const CodeBlock = styled.div`
+ position: relative;
+ background: #f1f3f4;
+ border: 1px solid #d0d7de;
+ border-radius: 8px;
+ overflow: hidden;
+
+ pre {
+ margin: 0;
+ padding: 16px;
+ background: none;
+ color: #24292f;
+ font-family: 'SF Mono', Monaco, monospace;
+ font-size: 13px;
+ line-height: 1.5;
+ overflow-x: auto;
+ }
+`;
+
+const CopyButton = styled.button`
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ padding: 4px 8px;
+ background: var(--title-color);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ font-size: 12px;
+ cursor: pointer;
+
+ &:hover {
+ background: #0920d9;
+ }
+
+ &.copied {
+ background: #28a745;
+ }
+`;
+
+const ColorPalette = styled.div`
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+ margin: 24px 0;
+`;
+
+const ColorSwatch = styled.div`
+ background: ${props => props.color};
+ height: 80px;
+ border-radius: 8px;
+ border: 1px solid #e9ecef;
+ display: flex;
+ align-items: flex-end;
+ position: relative;
+
+ &::after {
+ content: '${props => props.color}';
+ background: rgba(0, 0, 0, 0.8);
+ color: white;
+ padding: 8px;
+ font-size: 12px;
+ font-family: 'SF Mono', Monaco, monospace;
+ width: 100%;
+ text-align: center;
+ }
+`;
+
+const TokenTable = styled.table`
+ width: 100%;
+ border-collapse: collapse;
+ margin: 24px 0;
+
+ th, td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid #e9ecef;
+ }
+
+ th {
+ background: #f8f9fa;
+ font-weight: 600;
+ font-size: 14px;
+ }
+
+ td {
+ font-family: 'SF Mono', Monaco, monospace;
+ font-size: 13px;
+ }
+`;
+
+const IconGrid = styled.div`
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
+ gap: 16px;
+ margin: 24px 0;
+`;
+
+const IconCard = styled.div`
+ padding: 16px;
+ border: 1px solid #e9ecef;
+ border-radius: 8px;
+ text-align: center;
+ background: white;
+
+ svg {
+ margin-bottom: 8px;
+ }
+
+ span {
+ font-size: 12px;
+ color: #6c757d;
+ font-family: 'SF Mono', Monaco, monospace;
+ }
+`;
+
+export const StyleGuide = () => {
+ const [copiedCode, setCopiedCode] = useState('');
+
+ const copyToClipboard = (code) => {
+ navigator.clipboard.writeText(code);
+ setCopiedCode(code);
+ setTimeout(() => setCopiedCode(''), 2000);
+ };
+
+ const ComponentShowcase = ({ title, description, children, code }) => (
+
+ {title}
+ {description}
+ {children}
+
+ {code}
+ copyToClipboard(code)}
+ className={copiedCode === code ? 'copied' : ''}
+ >
+ {copiedCode === code ? 'Copied!' : 'Copy'}
+
+
+
+ );
+
+ // Available icons from your icons.json
+ const availableIcons = ['ArrowDown', 'Close', 'Github', 'Globe', 'Doc', 'StackOverflow', 'X', 'LinkedIn', 'Instagram'];
+
+ return (
+
+ {/* Header */}
+
+
+ Daniel's Design System
+
+
+ A comprehensive component library and design system built for modern web applications.
+ Each component follows accessibility best practices and maintains visual consistency.
+
+
+
+ {/* Navigation */}
+
+ Design Tokens
+ Colors
+ Typography
+ Buttons
+ Icons
+ Tags
+ Cards
+ Layout
+ Navigation
+ Images
+ Hero
+ Footer
+ Interactive
+
+
+ {/* DESIGN TOKENS */}
+
+ Design Tokens
+ Foundational design values that ensure consistency across the design system.
+
+ Spacing Scale
+
+
+
+ Token
+ Value
+ Usage
+
+
+
+ --spacing-xs 4px Tight spacing, form elements
+ --spacing-sm 8px Small gaps, button padding
+ --spacing-md 16px Standard spacing, card padding
+ --spacing-lg 24px Section spacing
+ --spacing-xl 32px Large gaps, page margins
+ --spacing-xxl 48px Section separation
+
+
+
+ Border Radius
+
+
+
+ Token
+ Value
+ Usage
+
+
+
+ --radius-sm 4px Small elements, tags
+ --radius-md 8px Buttons, form elements
+ --radius-lg 12px Cards, containers
+ --radius-round 50% Circular elements, avatars
+
+
+
+
+ {/* COLORS */}
+
+ Color Palette
+ Primary colors and semantic color meanings throughout the design system.
+
+
+
+
Primary
+
+
+
+
Secondary
+
+
+
+
Background
+
+
+
+
Text
+
+
+
+
Light Background
+
+
+
+
Dark Background
+
+
+
+
+
+ {/* TYPOGRAPHY */}
+
+ Typography
+ Text styles and hierarchy for clear communication and accessibility.
+
+
+ Huge Title (80px)
+XXL Title (64px)
+XL Title (56px)
+Large Title (40px)
+Medium Title (32px)
+Small Title (24px) `}
+ >
+
+
Huge Title
+ XXL Title
+ XL Title
+ Large Title
+ Medium Title
+ Small Title
+
+
+
+ Huge Bold Text
+XL Semibold Text
+Large Medium Text
+Medium Normal Text
+Small Light Text `}
+ >
+
+ Huge Bold Text
+ XL Semibold Text
+ Large Medium Text
+ Medium Normal Text
+ Small Light Text
+
+
+
+ Page Title (H1)
+Section Title (H2)
+Subsection (H3)
+Component Title (H4) `}
+ >
+
+
Page Title (H1)
+ Section Title (H2)
+ Subsection (H3)
+ Component Title (H4)
+
+
+
+
+
+ {/* BUTTONS */}
+
+ Buttons
+ Interactive elements for triggering actions and navigation.
+
+
+ Primary Action
+Secondary Action
+Outline Button
+Ghost Button `}
+ >
+ Primary Action
+ Secondary Action
+ Outline Button
+ Ghost Button
+
+
+ Small Button
+Medium Button
+Large Button `}
+ >
+ Small Button
+ Medium Button
+ Large Button
+
+
+ Default State
+Disabled State
+Loading State `}
+ >
+ Default State
+ Disabled State
+ Loading State
+
+
+ View on GitHub
+
+
+ `}
+ >
+ View on GitHub
+
+
+
+
+
+
+
+ {/* ICONS */}
+
+ Icons
+ Scalable vector icons for UI elements and visual communication.
+
+ Available Icons
+
+ {availableIcons.map(iconName => (
+
+
+
+ {iconName}
+
+ ))}
+
+
+
+
+
+ `}
+ >
+
+
+
+
+
+
+
+ `}
+ >
+
+
+
+
+
+
+
+ `}
+ >
+
+
+
+
+
+
+
+ {/* TAGS */}
+
+ Tags
+ Small labels for categorization and status indication.
+
+
+ React
+TypeScript
+Styled Components
+JavaScript `}
+ >
+ React
+ TypeScript
+ Styled Components
+ JavaScript
+
+
+ alert('Filter applied!')}>
+ Clickable Filter
+
+Selected Tag
+ {}}>Frontend `}
+ >
+ alert('Filter applied!')}>Clickable Filter
+ Selected Tag
+ {}}>Frontend
+
+
+ Default State
+Selected State
+Disabled State `}
+ >
+ Default State
+ Selected State
+ Disabled State
+
+
+
+
+ {/* CARDS */}
+
+ Cards
+ Containers for displaying related information in a structured layout.
+
+
+ `}
+ >
+
+
+
+ `}
+ >
+
+
+
+
+ Card Title
+ This is a basic card container that can hold any content you need.
+ Learn More
+`}
+ >
+
+ Card Title
+ This is a basic card container that can hold any content you need.
+ Learn More
+
+
+
+
+
+ {/* LAYOUT */}
+
+ Layout Components
+ Structural components for organizing content and creating responsive layouts.
+
+
+
+
+ Grid Item 1
+
+
+ Grid Item 2
+
+
+ Grid Item 3
+
+`}
+ >
+
+
+ Grid Item 1
+
+
+ Grid Item 2
+
+
+ Grid Item 3
+
+
+
+
+
+ Section content goes here...
+`}
+ >
+
+ Section content goes here...
+
+
+
+
+
+ {/* NAVIGATION */}
+
+ Navigation
+ Components for helping users navigate and understand page structure.
+
+
+
+ Skip to main content
+
+
+ Skip to navigation
+ `}
+ >
+
+
+ Press Tab to see skip links (they appear when focused)
+
+ Skip to main content
+ Skip to navigation
+
+
+
+
+
+ {/* HERO */}
+
+ Hero Section
+ Main landing area with eye-catching content and call-to-action.
+
+
+ `}
+ >
+
+
+
+
+
+
+
+ {/* FOOTER */}
+
+
+ {/* INTERACTIVE COMPONENTS */}
+
+ Interactive Components
+ Dynamic components that enhance user experience.
+
+
+ `}
+ >
+
+ Scroll the page to see the indicator in action (typically fixed at top of viewport)
+
+
+
+
+ `}
+ >
+
+
+
+ `}
+ >
+
+
+
+ `}
+ >
+
+
+
+ `}
+ >
+
+
+
+
+
+ `}
+ >
+
+
+
+
+
+ {/* IMAGES */}
+
+ Images
+ Image components with accessibility and performance optimizations.
+
+
+
+ `}
+ >
+
+
+
+
+
+ `}
+ >
+
+
+
+
+
+ {/* USAGE GUIDELINES */}
+
+ Usage Guidelines
+ Best practices for using the design system components.
+
+
+
Accessibility First
+
+ All components follow WCAG 2.1 AA guidelines and include proper ARIA attributes,
+ keyboard navigation support, and screen reader compatibility.
+
+
+ Responsive Design
+
+ Components are built with mobile-first responsive design principles and
+ work seamlessly across all device sizes.
+
+
+ Consistent Styling
+
+ Use design tokens (CSS custom properties) for consistent spacing, colors,
+ and typography throughout your application.
+
+
+ Performance
+
+ Components are optimized for performance with lazy loading, efficient renders,
+ and minimal bundle size impact.
+
+
+
+
+ {/* FOOTER */}
+
+
+ Design System by Daniel Lauding β’ Built with React, TypeScript & Styled Components
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..44a5c9f4
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+
+ /* Path mapping */
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src"]
+}
\ No newline at end of file
diff --git a/vite.config.js b/vite.config.js
index 8b0f57b9..c513b7b5 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,7 +1,62 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+import path from 'path'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ }
+ },
+ build: {
+ // Use esbuild for faster minification (better than terser for most cases)
+ minify: 'esbuild',
+ // Enable source maps for production debugging (can disable for smaller bundles)
+ sourcemap: false,
+ // Optimize chunk size
+ chunkSizeWarningLimit: 1000,
+ // Enable CSS code splitting
+ cssCodeSplit: true,
+ rollupOptions: {
+ output: {
+ // Better chunk splitting strategy
+ manualChunks: (id) => {
+ // Vendor chunks
+ if (id.includes('node_modules')) {
+ if (id.includes('react') || id.includes('react-dom')) {
+ return 'react-vendor'
+ }
+ if (id.includes('react-router')) {
+ return 'router-vendor'
+ }
+ if (id.includes('styled-components')) {
+ return 'styled-vendor'
+ }
+ // Other vendor libraries
+ return 'vendor'
+ }
+ },
+ // Optimize chunk file names
+ chunkFileNames: 'assets/js/[name]-[hash].js',
+ entryFileNames: 'assets/js/[name]-[hash].js',
+ assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
+ },
+ },
+ // Enable tree shaking
+ treeshake: true,
+ // Target modern browsers for smaller bundles
+ target: 'es2015',
+ },
+ optimizeDeps: {
+ include: ['react', 'react-dom', 'react-router-dom', 'styled-components'],
+ // Exclude large dependencies from pre-bundling if not needed
+ exclude: [],
+ },
+ // Enable compression
+ esbuild: {
+ drop: ['console', 'debugger'],
+ legalComments: 'none',
+ },
})