ในโลกของการพัฒนาเว็บแอปพลิเคชัน การเลือก Color Scheme ที่เหมาะสมไม่เพียงแต่ช่วยให้แอปพลิเคชันดูสวยงาม แต่ยังส่งผลต่อประสบการณ์ผู้ใช้และความสบายตาในการใช้งานเป็นเวลานาน Gruvbox เป็นหนึ่งใน Color Scheme ที่ได้รับความนิยมอย่างมากในหมู่นักพัฒนา ด้วยโทนสีที่อบอุ่น สบายตา และรองรับทั้ง Dark Mode และ Light Mode
บทความนี้จะพาคุณไปสร้าง Gruvbox Theme ที่พร้อมใช้งานสำหรับ Qwik Framework ร่วมกับ Tailwind CSS v4 ซึ่งมีการเปลี่ยนแปลงวิธีการ Configuration ใหม่ทั้งหมด
Gruvbox เป็น Color Scheme ที่ถูกออกแบบโดย Pavel Pertsev สำหรับ Vim Editor แต่ด้วยความสวยงามและความสบายตา ทำให้ถูกนำไปใช้กับ Terminal, IDE และเว็บแอปพลิเคชันมากมาย
Tailwind CSS v4 มาพร้อมกับการเปลี่ยนแปลงครั้งใหญ่ที่น่าตื่นเต้น:
| Tailwind v3 | Tailwind v4 |
|---|---|
ใช้ tailwind.config.js |
ใช้ @theme directive ใน CSS |
@tailwind base/components/utilities |
@import "tailwindcss" |
| JavaScript-based Config | CSS-based Config |
การเปลี่ยนมาใช้ CSS-based Configuration ทำให้:
ก่อนเริ่มต้น ตรวจสอบให้แน่ใจว่าคุณมีสิ่งเหล่านี้ติดตั้งอยู่ในเครื่อง:
ตรวจสอบเวอร์ชัน Node.js:
node --version
# ควรแสดง v18.x.x หรือสูงกว่า
เปิด Terminal และรันคำสั่งต่อไปนี้เพื่อสร้างโปรเจค Qwik ใหม่:
# ใช้ npm
npm create qwik@latest
# หรือใช้ pnpm
pnpm create qwik@latest
# หรือใช้ yarn
yarn create qwik
# หรือใช้ bun
bun create qwik@latest
เมื่อรันคำสั่งข้างต้น ระบบจะถามคำถามหลายข้อ:
┌ Let's create a Qwik App ✨
│
◇ Where would you like to create your new project?
│ ./my-qwik-app
│
◇ Select a starter
│ ● Basic App (Qwik City)
│ ○ Empty App
│ ○ Library
│
◇ Would you like to install npm dependencies?
│ Yes
│
◇ Initialize a new git repository?
│ Yes
│
└ Successfully created my-qwik-app! 🎉
คำแนะนำในการเลือก:
my-qwik-appcd my-qwik-app
# ใช้ npm
npm run dev
# หรือใช้ pnpm
pnpm dev
# หรือใช้ yarn
yarn dev
# หรือใช้ bun
bun dev
เปิด Browser และไปที่ http://localhost:5173 จะเห็นหน้า Welcome ของ Qwik
my-qwik-app/
├── public/ # Static assets
├── src/
│ ├── components/ # Reusable components
│ ├── routes/ # File-based routing
│ │ ├── index.tsx # หน้าแรก (/)
│ │ └── layout.tsx # Layout หลัก
│ ├── entry.ssr.tsx # Server-side entry
│ └── root.tsx # Root component
├── package.json
├── tsconfig.json
└── vite.config.ts
Qwik มี Integration สำหรับ Tailwind CSS ที่ติดตั้งได้ง่ายด้วยคำสั่ง qwik add
รันคำสั่งต่อไปนี้ในโฟลเดอร์โปรเจค:
# ใช้ npm
npm run qwik add tailwind
# หรือใช้ pnpm
pnpm qwik add tailwind
# หรือใช้ yarn
yarn qwik add tailwind
# หรือใช้ bun
bun run qwik add tailwind
เมื่อรันคำสั่ง ระบบจะแสดงข้อความดังนี้:
🦋 Add Integration
◇ What integration would you like to add?
│ Tailwind CSS
│
◇ Ready to add tailwind to your app?
│ Yes
│
◇ Installing tailwindcss...
│
◇ Updating files...
│ - vite.config.ts
│ - src/global.css (created)
│ - postcss.config.js (created)
│
└ ✅ Added tailwind!
หลังจากติดตั้ง Tailwind สำเร็จ จะมีไฟล์ที่ถูกสร้างและแก้ไขดังนี้:
ไฟล์ที่สร้างใหม่:
@import "tailwindcss";
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
ไฟล์ที่แก้ไข:
vite.config.ts - เพิ่ม Tailwind plugin
package.json - เพิ่ม dependencies
{
"devDependencies": {
"tailwindcss": "^4.x.x",
"autoprefixer": "^10.x.x"
}
}
ตรวจสอบไฟล์ src/root.tsx ว่ามีการ import global.css:
// src/root.tsx
import './global.css';
export default component$(() => {
return (
<QwikCityProvider>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="manifest" href="/manifest.json" />
</head>
<body lang="en">
<RouterOutlet />
<ServiceWorkerRegister />
</body>
</QwikCityProvider>
);
});
แก้ไขไฟล์ src/routes/index.tsx เพื่อทดสอบ:
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return (
<div class="min-h-screen bg-gray-900 text-white flex items-center justify-center">
<div class="text-center">
<h1 class="text-4xl font-bold text-blue-400 mb-4">
Tailwind CSS ทำงานแล้ว! 🎉
</h1>
<p class="text-gray-400">
พร้อมสำหรับ Gruvbox Theme
</p>
</div>
</div>
);
});
รัน npm run dev และดูผลลัพธ์ที่ http://localhost:5173
src/
├── global.css # Theme หลัก + Components
├── hooks/
│ └── useTheme.ts # Hook สำหรับ Toggle Theme
└── components/
└── gruvbox-showcase.tsx # ตัวอย่าง Component
แก้ไขไฟล์ src/global.css โดยลบเนื้อหาเดิมและใส่โค้ดต่อไปนี้:
@import "tailwindcss";
@theme {
/* Gruvbox Dark Background */
--color-gb-bg: #282828;
--color-gb-bg-h: #1d2021;
--color-gb-bg-s: #32302f;
--color-gb-bg-0: #282828;
--color-gb-bg-1: #3c3836;
--color-gb-bg-2: #504945;
--color-gb-bg-3: #665c54;
--color-gb-bg-4: #7c6f64;
/* Gruvbox Light Background */
--color-gb-bg-light: #fbf1c7;
--color-gb-bg-light-h: #f9f5d7;
--color-gb-bg-light-s: #f2e5bc;
--color-gb-bg-light-0: #fbf1c7;
--color-gb-bg-light-1: #ebdbb2;
--color-gb-bg-light-2: #d5c4a1;
--color-gb-bg-light-3: #bdae93;
--color-gb-bg-light-4: #a89984;
/* Gruvbox Dark Foreground */
--color-gb-fg: #ebdbb2;
--color-gb-fg-0: #fbf1c7;
--color-gb-fg-1: #ebdbb2;
--color-gb-fg-2: #d5c4a1;
--color-gb-fg-3: #bdae93;
--color-gb-fg-4: #a89984;
/* Gruvbox Light Foreground */
--color-gb-fg-light: #3c3836;
--color-gb-fg-light-0: #282828;
--color-gb-fg-light-1: #3c3836;
--color-gb-fg-light-2: #504945;
--color-gb-fg-light-3: #665c54;
--color-gb-fg-light-4: #7c6f64;
/* Red */
--color-gb-red: #cc241d;
--color-gb-red-bright: #fb4934;
--color-gb-red-dim: #9d0006;
/* Green */
--color-gb-green: #98971a;
--color-gb-green-bright: #b8bb26;
--color-gb-green-dim: #79740e;
/* Yellow */
--color-gb-yellow: #d79921;
--color-gb-yellow-bright: #fabd2f;
--color-gb-yellow-dim: #b57614;
/* Blue */
--color-gb-blue: #458588;
--color-gb-blue-bright: #83a598;
--color-gb-blue-dim: #076678;
/* Purple */
--color-gb-purple: #b16286;
--color-gb-purple-bright: #d3869b;
--color-gb-purple-dim: #8f3f71;
/* Aqua */
--color-gb-aqua: #689d6a;
--color-gb-aqua-bright: #8ec07c;
--color-gb-aqua-dim: #427b58;
/* Orange */
--color-gb-orange: #d65d0e;
--color-gb-orange-bright: #fe8019;
--color-gb-orange-dim: #af3a03;
/* Gray */
--color-gb-gray: #928374;
--color-gb-gray-bright: #a89984;
--color-gb-gray-dim: #7c6f64;
/* Fonts */
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
--font-sans: 'Inter', system-ui, sans-serif;
/* Box Shadows */
--shadow-gb: 0 4px 6px -1px rgba(40, 40, 40, 0.3);
--shadow-gb-lg: 0 10px 15px -3px rgba(40, 40, 40, 0.4);
}
เมื่อกำหนดสีใน @theme แล้ว Tailwind จะสร้าง Utility Classes ให้อัตโนมัติ เช่น bg-gb-bg, text-gb-red-bright, border-gb-blue เป็นต้น
| Class | Hex Code | การใช้งาน |
|---|---|---|
bg-gb-bg-h |
#1d2021 |
Hard Background (มืดที่สุด) |
bg-gb-bg |
#282828 |
Default Background |
bg-gb-bg-s |
#32302f |
Soft Background |
bg-gb-bg-1 |
#3c3836 |
Surface Level 1 (Cards) |
bg-gb-bg-2 |
#504945 |
Surface Level 2 (Elevated) |
bg-gb-bg-3 |
#665c54 |
Surface Level 3 |
bg-gb-bg-4 |
#7c6f64 |
Surface Level 4 |
| สี | Normal | Bright | Dim | การใช้งานแนะนำ |
|---|---|---|---|---|
| Red | #cc241d |
#fb4934 |
#9d0006 |
Errors, Warnings |
| Green | #98971a |
#b8bb26 |
#79740e |
Success, Strings |
| Yellow | #d79921 |
#fabd2f |
#b57614 |
Warnings, Highlights |
| Blue | #458588 |
#83a598 |
#076678 |
Links, Primary Actions |
| Purple | #b16286 |
#d3869b |
#8f3f71 |
Keywords, Special |
| Aqua | #689d6a |
#8ec07c |
#427b58 |
Types, Secondary |
| Orange | #d65d0e |
#fe8019 |
#af3a03 |
Constants, Headings |
เพิ่มโค้ดต่อไปนี้ใน global.css ต่อจาก @theme:
/* ============================================
CSS Variables for Runtime Theme Switching
============================================ */
:root {
--gb-bg: #282828;
--gb-bg-h: #1d2021;
--gb-bg-s: #32302f;
--gb-bg-1: #3c3836;
--gb-bg-2: #504945;
--gb-bg-3: #665c54;
--gb-bg-4: #7c6f64;
--gb-fg: #ebdbb2;
--gb-fg-0: #fbf1c7;
--gb-fg-1: #ebdbb2;
--gb-fg-2: #d5c4a1;
--gb-fg-3: #bdae93;
--gb-fg-4: #a89984;
--gb-red: #fb4934;
--gb-green: #b8bb26;
--gb-yellow: #fabd2f;
--gb-blue: #83a598;
--gb-purple: #d3869b;
--gb-aqua: #8ec07c;
--gb-orange: #fe8019;
--gb-gray: #928374;
}
.light {
--gb-bg: #fbf1c7;
--gb-bg-h: #f9f5d7;
--gb-bg-s: #f2e5bc;
--gb-bg-1: #ebdbb2;
--gb-bg-2: #d5c4a1;
--gb-bg-3: #bdae93;
--gb-bg-4: #a89984;
--gb-fg: #3c3836;
--gb-fg-0: #282828;
--gb-fg-1: #3c3836;
--gb-fg-2: #504945;
--gb-fg-3: #665c54;
--gb-fg-4: #7c6f64;
--gb-red: #cc241d;
--gb-green: #98971a;
--gb-yellow: #d79921;
--gb-blue: #458588;
--gb-purple: #b16286;
--gb-aqua: #689d6a;
--gb-orange: #d65d0e;
--gb-gray: #928374;
}
/* ============================================
Base Styles
============================================ */
@layer base {
html {
scroll-behavior: smooth;
}
body {
background-color: var(--gb-bg);
color: var(--gb-fg);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Headings */
h1 {
font-size: 2.25rem;
line-height: 2.5rem;
font-weight: 700;
color: var(--color-gb-orange-bright);
}
h2 {
font-size: 1.875rem;
line-height: 2.25rem;
font-weight: 700;
color: var(--color-gb-yellow-bright);
}
h3 {
font-size: 1.5rem;
line-height: 2rem;
font-weight: 700;
color: var(--color-gb-aqua-bright);
}
h4 {
font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 700;
color: var(--color-gb-blue-bright);
}
/* Links */
a {
color: var(--color-gb-blue-bright);
transition: color 0.2s;
}
a:hover {
color: var(--color-gb-aqua-bright);
}
/* Code */
code {
font-family: var(--font-mono);
color: var(--color-gb-orange-bright);
background-color: var(--color-gb-bg-1);
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
}
pre {
font-family: var(--font-mono);
background-color: var(--color-gb-bg-h);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
}
pre code {
background-color: transparent;
padding: 0;
}
/* Selection */
::selection {
background-color: var(--color-gb-blue);
color: var(--color-gb-bg);
}
/* Scrollbar */
::-webkit-scrollbar {
width: 0.5rem;
height: 0.5rem;
}
::-webkit-scrollbar-track {
background-color: var(--color-gb-bg-1);
}
::-webkit-scrollbar-thumb {
background-color: var(--color-gb-bg-3);
border-radius: 9999px;
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--color-gb-bg-4);
}
}
@layer components {
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.2s;
cursor: pointer;
border: none;
}
.btn:focus {
outline: none;
box-shadow: 0 0 0 2px var(--color-gb-bg), 0 0 0 4px var(--color-gb-blue);
}
.btn-primary {
background-color: var(--color-gb-blue);
color: var(--color-gb-bg);
}
.btn-primary:hover {
background-color: var(--color-gb-blue-bright);
}
.btn-secondary {
background-color: var(--color-gb-bg-2);
color: var(--color-gb-fg);
}
.btn-secondary:hover {
background-color: var(--color-gb-bg-3);
}
.btn-success {
background-color: var(--color-gb-green);
color: var(--color-gb-bg);
}
.btn-success:hover {
background-color: var(--color-gb-green-bright);
}
.btn-danger {
background-color: var(--color-gb-red);
color: var(--color-gb-bg);
}
.btn-danger:hover {
background-color: var(--color-gb-red-bright);
}
.btn-warning {
background-color: var(--color-gb-yellow);
color: var(--color-gb-bg);
}
.btn-warning:hover {
background-color: var(--color-gb-yellow-bright);
}
.btn-ghost {
background-color: transparent;
color: var(--color-gb-fg);
}
.btn-ghost:hover {
background-color: var(--color-gb-bg-1);
}
}
@layer components {
/* Cards */
.card {
background-color: var(--color-gb-bg-1);
border-radius: 0.75rem;
padding: 1.5rem;
box-shadow: var(--shadow-gb);
}
.card-hover {
background-color: var(--color-gb-bg-1);
border-radius: 0.75rem;
padding: 1.5rem;
box-shadow: var(--shadow-gb);
transition: background-color 0.2s;
cursor: pointer;
}
.card-hover:hover {
background-color: var(--color-gb-bg-2);
}
.card-bordered {
background-color: var(--color-gb-bg-1);
border-radius: 0.75rem;
padding: 1.5rem;
box-shadow: var(--shadow-gb);
border: 1px solid var(--color-gb-bg-3);
}
}
@layer components {
/* Inputs */
.input {
width: 100%;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: var(--color-gb-bg-h);
border: 1px solid var(--color-gb-bg-3);
color: var(--color-gb-fg);
transition: all 0.2s;
}
.input::placeholder {
color: var(--color-gb-fg-4);
}
.input:focus {
outline: none;
border-color: var(--color-gb-blue);
box-shadow: 0 0 0 1px var(--color-gb-blue);
}
.input-error {
width: 100%;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: var(--color-gb-bg-h);
border: 1px solid var(--color-gb-red);
color: var(--color-gb-fg);
transition: all 0.2s;
}
.input-error:focus {
outline: none;
border-color: var(--color-gb-red);
box-shadow: 0 0 0 1px var(--color-gb-red);
}
/* Labels */
.label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-gb-fg-3);
margin-bottom: 0.25rem;
}
}
@layer components {
/* Badges */
.badge {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.625rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
.badge-red {
background-color: color-mix(in srgb, var(--color-gb-red) 20%, transparent);
color: var(--color-gb-red-bright);
}
.badge-green {
background-color: color-mix(in srgb, var(--color-gb-green) 20%, transparent);
color: var(--color-gb-green-bright);
}
.badge-yellow {
background-color: color-mix(in srgb, var(--color-gb-yellow) 20%, transparent);
color: var(--color-gb-yellow-bright);
}
.badge-blue {
background-color: color-mix(in srgb, var(--color-gb-blue) 20%, transparent);
color: var(--color-gb-blue-bright);
}
.badge-purple {
background-color: color-mix(in srgb, var(--color-gb-purple) 20%, transparent);
color: var(--color-gb-purple-bright);
}
.badge-aqua {
background-color: color-mix(in srgb, var(--color-gb-aqua) 20%, transparent);
color: var(--color-gb-aqua-bright);
}
.badge-orange {
background-color: color-mix(in srgb, var(--color-gb-orange) 20%, transparent);
color: var(--color-gb-orange-bright);
}
}
@layer components {
/* Alerts */
.alert {
padding: 1rem;
border-radius: 0.5rem;
border-left: 4px solid;
}
.alert-info {
background-color: color-mix(in srgb, var(--color-gb-blue) 10%, transparent);
border-color: var(--color-gb-blue);
color: var(--color-gb-blue-bright);
}
.alert-success {
background-color: color-mix(in srgb, var(--color-gb-green) 10%, transparent);
border-color: var(--color-gb-green);
color: var(--color-gb-green-bright);
}
.alert-warning {
background-color: color-mix(in srgb, var(--color-gb-yellow) 10%, transparent);
border-color: var(--color-gb-yellow);
color: var(--color-gb-yellow-bright);
}
.alert-error {
background-color: color-mix(in srgb, var(--color-gb-red) 10%, transparent);
border-color: var(--color-gb-red);
color: var(--color-gb-red-bright);
}
}
สร้างไฟล์ src/hooks/useTheme.ts:
import { $, useOnDocument, useSignal, useVisibleTask$ } from '@builder.io/qwik';
type Theme = 'dark' | 'light' | 'system';
/**
* useTheme - Hook สำหรับจัดการ Gruvbox Theme (Tailwind v4)
*/
export const useTheme = () => {
const theme = useSignal<Theme>('dark');
const resolvedTheme = useSignal<'dark' | 'light'>('dark');
// eslint-disable-next-line qwik/no-use-visible-task
useVisibleTask$(() => {
const stored = localStorage.getItem('gruvbox-theme') as Theme | null;
if (stored) {
theme.value = stored;
updateTheme(stored);
} else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
theme.value = 'system';
updateTheme('system');
}
});
useOnDocument(
'visibilitychange',
$(() => {
if (theme.value === 'system') {
updateTheme('system');
}
})
);
const updateTheme = (newTheme: Theme) => {
let resolved: 'dark' | 'light' = 'dark';
if (newTheme === 'system') {
resolved = window.matchMedia('(prefers-color-scheme: light)').matches
? 'light'
: 'dark';
} else {
resolved = newTheme;
}
resolvedTheme.value = resolved;
// Tailwind v4: ใช้ class 'light' สำหรับ light mode (default = dark)
document.documentElement.classList.remove('light');
if (resolved === 'light') {
document.documentElement.classList.add('light');
}
// Update meta theme-color for mobile browsers
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
if (metaThemeColor) {
metaThemeColor.setAttribute(
'content',
resolved === 'dark' ? '#282828' : '#fbf1c7'
);
}
};
const setTheme = $((newTheme: Theme) => {
theme.value = newTheme;
localStorage.setItem('gruvbox-theme', newTheme);
updateTheme(newTheme);
});
const toggleTheme = $(() => {
const newTheme = resolvedTheme.value === 'dark' ? 'light' : 'dark';
setTheme(newTheme);
});
const cycleTheme = $(() => {
const themes: Theme[] = ['dark', 'light', 'system'];
const currentIndex = themes.indexOf(theme.value);
const nextIndex = (currentIndex + 1) % themes.length;
setTheme(themes[nextIndex]);
});
return {
theme,
resolvedTheme,
setTheme,
toggleTheme,
cycleTheme,
isDark: resolvedTheme.value === 'dark',
isLight: resolvedTheme.value === 'light',
isSystem: theme.value === 'system',
};
};
export default useTheme;
import { component$ } from '@builder.io/qwik';
import { useTheme } from '../hooks/useTheme';
export const ThemeToggle = component$(() => {
const { toggleTheme, resolvedTheme } = useTheme();
return (
<button onClick$={toggleTheme} class="btn btn-ghost">
{resolvedTheme.value === 'dark' ? '🌙 Dark' : '☀️ Light'}
</button>
);
});
// เพิ่ม class "light" ที่ <html> สำหรับ light mode
document.documentElement.classList.add('light');
// ลบ class "light" สำหรับ dark mode (default)
document.documentElement.classList.remove('light');
import { component$ } from '@builder.io/qwik';
import { useTheme } from '../hooks/useTheme';
export default component$(() => {
const { toggleTheme, resolvedTheme } = useTheme();
return (
<div class="min-h-screen">
{/* Header */}
<header class="bg-gb-bg-1 p-4 flex justify-between items-center">
<h1>My Gruvbox App</h1>
<div class="flex items-center gap-4">
<nav class="flex gap-4">
<a href="/" class="nav-link">Home</a>
<a href="/about" class="nav-link">About</a>
<a href="/contact" class="nav-link">Contact</a>
</nav>
<button onClick$={toggleTheme} class="btn btn-ghost">
{resolvedTheme.value === 'dark' ? '🌙' : '☀️'}
</button>
</div>
</header>
{/* Main Content */}
<main class="container mx-auto p-8">
<div class="grid md:grid-cols-2 gap-6">
<div class="card">
<h2 class="mb-4">Welcome!</h2>
<p class="text-gb-fg-3 mb-4">
สวัสดี! นี่คือ Gruvbox Theme สำหรับ Qwik + Tailwind v4
</p>
<div class="flex gap-2">
<span class="badge badge-green">Active</span>
<span class="badge badge-blue">New</span>
</div>
</div>
<div class="card">
<h3 class="text-gb-aqua-bright mb-4">Quick Actions</h3>
<div class="flex flex-col gap-2">
<button class="btn btn-primary">Get Started</button>
<button class="btn btn-secondary">Learn More</button>
</div>
</div>
</div>
{/* Alerts Section */}
<section class="mt-8 space-y-4">
<div class="alert alert-info">
<strong>Info:</strong> Welcome to Gruvbox Theme!
</div>
<div class="alert alert-success">
<strong>Success:</strong> Theme loaded successfully.
</div>
</section>
</main>
{/* Footer */}
<footer class="bg-gb-bg-1 p-4 text-center text-gb-fg-4 mt-auto">
© 2024 My Gruvbox App
</footer>
</div>
);
});
import { component$, useSignal } from '@builder.io/qwik';
export const LoginForm = component$(() => {
const email = useSignal('');
const password = useSignal('');
return (
<div class="card max-w-md mx-auto">
<h2 class="mb-6">Login</h2>
<div class="space-y-4">
<div>
<label class="label">Email</label>
<input
type="email"
class="input"
placeholder="you@example.com"
value={email.value}
onInput$={(e) => email.value = (e.target as HTMLInputElement).value}
/>
</div>
<div>
<label class="label">Password</label>
<input
type="password"
class="input"
placeholder="••••••••"
value={password.value}
onInput$={(e) => password.value = (e.target as HTMLInputElement).value}
/>
</div>
<button type="submit" class="btn btn-primary w-full">
Sign In
</button>
</div>
<p class="text-gb-fg-4 text-sm mt-4 text-center">
Don't have an account?
<a href="/register" class="text-gb-blue-bright ml-1">Sign up</a>
</p>
</div>
);
});
@layer utilities {
.gb-gradient-primary {
background: linear-gradient(to right, var(--color-gb-blue), var(--color-gb-aqua));
}
.gb-gradient-warm {
background: linear-gradient(to right, var(--color-gb-orange), var(--color-gb-yellow));
}
.gb-gradient-cool {
background: linear-gradient(to right, var(--color-gb-purple), var(--color-gb-blue));
}
}
@layer utilities {
.gb-glass {
background-color: color-mix(in srgb, var(--color-gb-bg) 80%, transparent);
backdrop-filter: blur(12px);
}
}
@layer utilities {
.glow-blue {
box-shadow: 0 0 20px color-mix(in srgb, var(--color-gb-blue-bright) 30%, transparent);
}
.glow-orange {
box-shadow: 0 0 20px color-mix(in srgb, var(--color-gb-orange-bright) 30%, transparent);
}
.glow-aqua {
box-shadow: 0 0 20px color-mix(in srgb, var(--color-gb-aqua-bright) 30%, transparent);
}
}
การสร้าง Gruvbox Theme สำหรับ Qwik + Tailwind CSS v4 นั้นต้องคำนึงถึงการเปลี่ยนแปลงใน Tailwind v4 ที่ใช้ CSS-based Configuration แทน JavaScript Config File เดิม
npm create qwik@latest สร้างโปรเจคใหม่npm run qwik add tailwind ติดตั้งได้ง่ายบทความนี้เขียนขึ้นเพื่อเป็นแนวทางในการสร้าง Custom Theme สำหรับ Modern Web Framework โดยใช้ Gruvbox Color Scheme ที่เป็นที่นิยมในหมู่นักพัฒนา