Cleanup: Remove old modes, add Svelte 5 reference library, update orchestrator
- Delete Mode1Test, Mode2Test, Mode3Test, Mode5Test (keep only Mode6Test) - Delete old documentation, Docker files, and k8s configs - Add comprehensive Svelte 5 reference library (svelte-reference/) - Update ModeSwitch orchestrator: - Fix port to 3005 (never use 3000/3001) - Add API endpoints for mode switching - Add bun-types and proper tsconfig - Clean up mode folder mappings
This commit is contained in:
@@ -0,0 +1,733 @@
|
||||
# Svelte Styling & Tailwind Integration
|
||||
|
||||
## Overview
|
||||
|
||||
Svelte provides scoped CSS by default and integrates seamlessly with Tailwind CSS. This guide covers both approaches and best practices.
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Scoped CSS (Default)
|
||||
|
||||
### Basic Scoped Styles
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
let { variant = 'primary' }: { variant?: 'primary' | 'secondary' } = $props();
|
||||
</script>
|
||||
|
||||
<button class={variant}>
|
||||
<slot />
|
||||
</button>
|
||||
|
||||
<style>
|
||||
/* These styles are automatically scoped to this component */
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.primary:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
border: 2px solid currentColor;
|
||||
}
|
||||
|
||||
.secondary:hover {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### Global Styles with :global()
|
||||
|
||||
```svelte
|
||||
<style>
|
||||
/* Scoped to this component */
|
||||
.container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Applies globally - use sparingly */
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
font-family: system-ui;
|
||||
}
|
||||
|
||||
/* Global styles WITHIN a scoped selector */
|
||||
.container :global(a) {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
/* Global class that can be used anywhere */
|
||||
:global(.sr-only) {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### CSS Custom Properties (Variables)
|
||||
|
||||
```svelte
|
||||
<!-- Define in root layout or app.css -->
|
||||
<style>
|
||||
:root {
|
||||
/* Colors */
|
||||
--color-primary: #3b82f6;
|
||||
--color-primary-dark: #2563eb;
|
||||
--color-secondary: #6b7280;
|
||||
--color-success: #10b981;
|
||||
--color-warning: #f59e0b;
|
||||
--color-error: #ef4444;
|
||||
|
||||
/* Surfaces */
|
||||
--color-background: #ffffff;
|
||||
--color-surface: #f9fafb;
|
||||
--color-border: #e5e7eb;
|
||||
|
||||
/* Text */
|
||||
--color-text: #111827;
|
||||
--color-text-muted: #6b7280;
|
||||
|
||||
/* Spacing */
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-4: 1rem;
|
||||
--space-6: 1.5rem;
|
||||
--space-8: 2rem;
|
||||
|
||||
/* Typography */
|
||||
--font-sans: system-ui, -apple-system, sans-serif;
|
||||
--font-mono: ui-monospace, monospace;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
||||
|
||||
/* Borders */
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.375rem;
|
||||
--radius-lg: 0.5rem;
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
:root[data-theme="dark"] {
|
||||
--color-background: #111827;
|
||||
--color-surface: #1f2937;
|
||||
--color-border: #374151;
|
||||
--color-text: #f9fafb;
|
||||
--color-text-muted: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💨 Tailwind CSS Integration
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Install Tailwind
|
||||
npm install -D tailwindcss postcss autoprefixer
|
||||
npx tailwindcss init -p
|
||||
```
|
||||
|
||||
### tailwind.config.js
|
||||
|
||||
```javascript
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
darkMode: 'class', // or 'media'
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
200: '#bfdbfe',
|
||||
300: '#93c5fd',
|
||||
400: '#60a5fa',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8',
|
||||
800: '#1e40af',
|
||||
900: '#1e3a8a',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter var', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/typography'),
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### postcss.config.js
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### app.css
|
||||
|
||||
```css
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Custom base styles */
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-white dark:bg-gray-900 text-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
@apply font-bold tracking-tight;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom component classes */
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center px-4 py-2
|
||||
font-medium rounded-lg transition-colors
|
||||
focus:outline-none focus:ring-2 focus:ring-offset-2;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply bg-primary-600 text-white hover:bg-primary-700
|
||||
focus:ring-primary-500;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-gray-100 text-gray-700 hover:bg-gray-200
|
||||
dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700;
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply block w-full rounded-lg border border-gray-300
|
||||
px-3 py-2 shadow-sm
|
||||
focus:border-primary-500 focus:ring-1 focus:ring-primary-500
|
||||
dark:border-gray-600 dark:bg-gray-800;
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply rounded-xl bg-white dark:bg-gray-800
|
||||
border border-gray-200 dark:border-gray-700
|
||||
shadow-sm overflow-hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom utilities */
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Component Styling Patterns
|
||||
|
||||
### Pattern 1: Tailwind Component
|
||||
|
||||
```svelte
|
||||
<!-- Button.svelte -->
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLButtonAttributes } from 'svelte/elements';
|
||||
|
||||
interface Props extends HTMLButtonAttributes {
|
||||
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
loading?: boolean;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
loading = false,
|
||||
disabled,
|
||||
class: className = '',
|
||||
children,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
|
||||
const baseClasses = `
|
||||
inline-flex items-center justify-center font-medium rounded-lg
|
||||
transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
`;
|
||||
|
||||
const variantClasses = {
|
||||
primary: 'bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500',
|
||||
secondary: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300',
|
||||
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
|
||||
ghost: 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'px-3 py-1.5 text-sm',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-6 py-3 text-base'
|
||||
};
|
||||
|
||||
let classes = $derived(
|
||||
`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`.trim()
|
||||
);
|
||||
</script>
|
||||
|
||||
<button class={classes} disabled={disabled || loading} {...rest}>
|
||||
{#if loading}
|
||||
<svg class="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
{/if}
|
||||
{@render children()}
|
||||
</button>
|
||||
```
|
||||
|
||||
### Pattern 2: Class Merging with clsx/tailwind-merge
|
||||
|
||||
```bash
|
||||
npm install clsx tailwind-merge
|
||||
```
|
||||
|
||||
```typescript
|
||||
// src/lib/utils/cn.ts
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- Card.svelte -->
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils/cn';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
variant?: 'default' | 'bordered' | 'elevated';
|
||||
padding?: 'none' | 'sm' | 'md' | 'lg';
|
||||
class?: string;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
variant = 'default',
|
||||
padding = 'md',
|
||||
class: className,
|
||||
children
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
'rounded-xl bg-white dark:bg-gray-800',
|
||||
{
|
||||
'border border-gray-200 dark:border-gray-700': variant === 'default' || variant === 'bordered',
|
||||
'shadow-lg': variant === 'elevated',
|
||||
'p-0': padding === 'none',
|
||||
'p-4': padding === 'sm',
|
||||
'p-6': padding === 'md',
|
||||
'p-8': padding === 'lg',
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{@render children()}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Pattern 3: CVA (Class Variance Authority)
|
||||
|
||||
```bash
|
||||
npm install class-variance-authority
|
||||
```
|
||||
|
||||
```typescript
|
||||
// src/lib/components/Button/variants.ts
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
export const buttonVariants = cva(
|
||||
// Base classes
|
||||
'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: 'bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500',
|
||||
secondary: 'bg-gray-100 text-gray-700 hover:bg-gray-200',
|
||||
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
|
||||
ghost: 'text-gray-600 hover:bg-gray-100',
|
||||
link: 'text-primary-600 underline-offset-4 hover:underline'
|
||||
},
|
||||
size: {
|
||||
sm: 'h-8 px-3 text-sm',
|
||||
md: 'h-10 px-4 text-sm',
|
||||
lg: 'h-12 px-6 text-base',
|
||||
icon: 'h-10 w-10'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
size: 'md'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>;
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- Button.svelte -->
|
||||
<script lang="ts">
|
||||
import { buttonVariants, type ButtonVariants } from './variants';
|
||||
import { cn } from '$lib/utils/cn';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLButtonAttributes } from 'svelte/elements';
|
||||
|
||||
interface Props extends ButtonVariants, HTMLButtonAttributes {
|
||||
class?: string;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { variant, size, class: className, children, ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<button class={cn(buttonVariants({ variant, size }), className)} {...rest}>
|
||||
{@render children()}
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌓 Dark Mode
|
||||
|
||||
### With Tailwind
|
||||
|
||||
```svelte
|
||||
<!-- ThemeToggle.svelte -->
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
type Theme = 'light' | 'dark' | 'system';
|
||||
|
||||
let theme = $state<Theme>('system');
|
||||
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
|
||||
const saved = localStorage.getItem('theme') as Theme | null;
|
||||
if (saved) {
|
||||
theme = saved;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
|
||||
const root = document.documentElement;
|
||||
|
||||
if (theme === 'dark') {
|
||||
root.classList.add('dark');
|
||||
} else if (theme === 'light') {
|
||||
root.classList.remove('dark');
|
||||
} else {
|
||||
// System preference
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem('theme', theme);
|
||||
});
|
||||
|
||||
function cycle() {
|
||||
const themes: Theme[] = ['light', 'dark', 'system'];
|
||||
const current = themes.indexOf(theme);
|
||||
theme = themes[(current + 1) % themes.length];
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
onclick={cycle}
|
||||
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{#if theme === 'light'}
|
||||
☀️
|
||||
{:else if theme === 'dark'}
|
||||
🌙
|
||||
{:else}
|
||||
💻
|
||||
{/if}
|
||||
</button>
|
||||
```
|
||||
|
||||
### Prevent Flash of Wrong Theme
|
||||
|
||||
```html
|
||||
<!-- app.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script>
|
||||
// Prevent flash of wrong theme
|
||||
(function() {
|
||||
const theme = localStorage.getItem('theme');
|
||||
if (theme === 'dark' ||
|
||||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📐 Responsive Design
|
||||
|
||||
### Tailwind Breakpoints
|
||||
|
||||
```svelte
|
||||
<div class="
|
||||
grid grid-cols-1
|
||||
sm:grid-cols-2
|
||||
md:grid-cols-3
|
||||
lg:grid-cols-4
|
||||
xl:grid-cols-5
|
||||
gap-4
|
||||
">
|
||||
{#each items as item}
|
||||
<div class="p-4 bg-white rounded-lg shadow">
|
||||
{item.name}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Responsive typography -->
|
||||
<h1 class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold">
|
||||
Responsive Heading
|
||||
</h1>
|
||||
|
||||
<!-- Hide/show based on screen size -->
|
||||
<nav class="hidden md:flex gap-4">
|
||||
<!-- Desktop navigation -->
|
||||
</nav>
|
||||
|
||||
<button class="md:hidden">
|
||||
<!-- Mobile menu button -->
|
||||
</button>
|
||||
```
|
||||
|
||||
### Container Queries (Tailwind v3.4+)
|
||||
|
||||
```svelte
|
||||
<div class="@container">
|
||||
<div class="@md:flex @md:gap-4">
|
||||
<img class="@md:w-48" src="/image.jpg" alt="" />
|
||||
<div>
|
||||
<h2 class="@md:text-xl">Title</h2>
|
||||
<p class="@md:text-base text-sm">Description</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✨ Animations
|
||||
|
||||
### Tailwind Animations
|
||||
|
||||
```svelte
|
||||
<!-- Built-in animations -->
|
||||
<div class="animate-spin">Spinning</div>
|
||||
<div class="animate-pulse">Pulsing</div>
|
||||
<div class="animate-bounce">Bouncing</div>
|
||||
|
||||
<!-- Custom animation in tailwind.config.js -->
|
||||
<div class="animate-fade-in">Fading in</div>
|
||||
```
|
||||
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
export default {
|
||||
theme: {
|
||||
extend: {
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.3s ease-in-out',
|
||||
'slide-up': 'slideUp 0.3s ease-out',
|
||||
'slide-down': 'slideDown 0.3s ease-out',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
},
|
||||
slideDown: {
|
||||
'0%': { transform: 'translateY(-10px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Svelte Transitions
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { fade, fly, slide, scale, blur } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
|
||||
let visible = $state(true);
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
<!-- Fade -->
|
||||
<div transition:fade={{ duration: 300 }}>
|
||||
Fading content
|
||||
</div>
|
||||
|
||||
<!-- Fly -->
|
||||
<div transition:fly={{ y: 20, duration: 300 }}>
|
||||
Flying content
|
||||
</div>
|
||||
|
||||
<!-- Slide -->
|
||||
<div transition:slide={{ duration: 300 }}>
|
||||
Sliding content
|
||||
</div>
|
||||
|
||||
<!-- Scale -->
|
||||
<div transition:scale={{ start: 0.95, duration: 300 }}>
|
||||
Scaling content
|
||||
</div>
|
||||
|
||||
<!-- Combined with easing -->
|
||||
<div transition:fly={{ y: 20, duration: 400, easing: quintOut }}>
|
||||
Smooth flying content
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- In/out separately -->
|
||||
{#if visible}
|
||||
<div
|
||||
in:fly={{ y: 20, duration: 300 }}
|
||||
out:fade={{ duration: 200 }}
|
||||
>
|
||||
Different in/out
|
||||
</div>
|
||||
{/if}
|
||||
```
|
||||
|
||||
### Custom Transition
|
||||
|
||||
```typescript
|
||||
// src/lib/transitions/index.ts
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
|
||||
export function expandHeight(
|
||||
node: Element,
|
||||
{ duration = 300, easing = cubicOut } = {}
|
||||
) {
|
||||
const style = getComputedStyle(node);
|
||||
const height = parseFloat(style.height);
|
||||
const paddingTop = parseFloat(style.paddingTop);
|
||||
const paddingBottom = parseFloat(style.paddingBottom);
|
||||
|
||||
return {
|
||||
duration,
|
||||
easing,
|
||||
css: (t: number) => `
|
||||
height: ${t * height}px;
|
||||
padding-top: ${t * paddingTop}px;
|
||||
padding-bottom: ${t * paddingBottom}px;
|
||||
overflow: hidden;
|
||||
`
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Best Practices
|
||||
|
||||
### ✅ Do:
|
||||
- Use Tailwind for utility-first styling
|
||||
- Create reusable component classes with `@layer components`
|
||||
- Use CSS variables for design tokens
|
||||
- Prefer `class:` directive for conditional classes
|
||||
- Use `cn()` utility for merging classes
|
||||
- Keep animations subtle and purposeful
|
||||
|
||||
### ❌ Don't:
|
||||
- Mix inline styles with Tailwind randomly
|
||||
- Override Tailwind classes with `!important`
|
||||
- Create deeply nested CSS selectors
|
||||
- Use `@apply` for everything (defeats the purpose)
|
||||
- Forget dark mode variants
|
||||
- Ignore accessibility (focus states, contrast)
|
||||
Reference in New Issue
Block a user