Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:25:32 +08:00
commit 6c7fa6696d
14 changed files with 2262 additions and 0 deletions

20
templates/components.json Normal file
View File

@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

140
templates/index.css Normal file
View File

@@ -0,0 +1,140 @@
@import "tailwindcss";
/*
Tailwind v4 + shadcn/ui Dark Mode Pattern
Based on: https://ui.shadcn.com/docs/tailwind-v4
Key Pattern:
1. Define CSS variables at root level (NOT in @layer base)
2. Use .dark for dark mode overrides (NOT in @theme)
3. Use @theme inline to map variables to Tailwind utilities
4. All color values must use hsl() wrapper
*/
/* Light mode colors - Define at root level */
:root {
--background: hsl(0 0% 100%);
--foreground: hsl(222.2 84% 4.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(222.2 84% 4.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(222.2 84% 4.9%);
--primary: hsl(221.2 83.2% 53.3%);
--primary-foreground: hsl(210 40% 98%);
--secondary: hsl(210 40% 96.1%);
--secondary-foreground: hsl(222.2 47.4% 11.2%);
--muted: hsl(210 40% 96.1%);
--muted-foreground: hsl(215.4 16.3% 46.9%);
--accent: hsl(210 40% 96.1%);
--accent-foreground: hsl(222.2 47.4% 11.2%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(210 40% 98%);
--success: hsl(142.1 76.2% 36.3%);
--success-foreground: hsl(210 40% 98%);
--warning: hsl(38 92% 50%);
--warning-foreground: hsl(222.2 47.4% 11.2%);
--info: hsl(221.2 83.2% 53.3%);
--info-foreground: hsl(210 40% 98%);
--border: hsl(214.3 31.8% 91.4%);
--input: hsl(214.3 31.8% 91.4%);
--ring: hsl(221.2 83.2% 53.3%);
--radius: 0.5rem;
/* Chart colors */
--chart-1: hsl(12 76% 61%);
--chart-2: hsl(173 58% 39%);
--chart-3: hsl(197 37% 24%);
--chart-4: hsl(43 74% 66%);
--chart-5: hsl(27 87% 67%);
}
/* Dark mode colors - Plain CSS overrides (NOT in @theme) */
.dark {
--background: hsl(222.2 84% 4.9%);
--foreground: hsl(210 40% 98%);
--card: hsl(222.2 84% 4.9%);
--card-foreground: hsl(210 40% 98%);
--popover: hsl(222.2 84% 4.9%);
--popover-foreground: hsl(210 40% 98%);
--primary: hsl(217.2 91.2% 59.8%);
--primary-foreground: hsl(222.2 47.4% 11.2%);
--secondary: hsl(217.2 32.6% 17.5%);
--secondary-foreground: hsl(210 40% 98%);
--muted: hsl(217.2 32.6% 17.5%);
--muted-foreground: hsl(215 20.2% 65.1%);
--accent: hsl(217.2 32.6% 17.5%);
--accent-foreground: hsl(210 40% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(210 40% 98%);
--success: hsl(142.1 70.6% 45.3%);
--success-foreground: hsl(222.2 47.4% 11.2%);
--warning: hsl(38 92% 55%);
--warning-foreground: hsl(222.2 47.4% 11.2%);
--info: hsl(217.2 91.2% 59.8%);
--info-foreground: hsl(222.2 47.4% 11.2%);
--border: hsl(217.2 32.6% 17.5%);
--input: hsl(217.2 32.6% 17.5%);
--ring: hsl(224.3 76.3% 48%);
/* Chart colors for dark mode */
--chart-1: hsl(220 70% 50%);
--chart-2: hsl(160 60% 45%);
--chart-3: hsl(30 80% 55%);
--chart-4: hsl(280 65% 60%);
--chart-5: hsl(340 75% 55%);
}
/* Map CSS variables to Tailwind theme with @theme inline */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
/* Border radius tokens */
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
}
/* Base styles in @layer base */
@layer base {
* {
border-color: var(--border);
}
body {
margin: 0;
background-color: var(--background);
color: var(--foreground);
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}

View File

@@ -0,0 +1,92 @@
import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
type Theme = 'dark' | 'light' | 'system'
type ThemeProviderProps = {
children: ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: 'system',
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'vite-ui-theme',
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(() => {
// Try localStorage first, fall back to sessionStorage, then default
try {
return (localStorage.getItem(storageKey) as Theme) ||
(sessionStorage.getItem(storageKey) as Theme) ||
defaultTheme
} catch (e) {
// Storage unavailable (incognito/privacy mode) - use default
return defaultTheme
}
})
useEffect(() => {
const root = window.document.documentElement
root.classList.remove('light', 'dark')
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
.matches
? 'dark'
: 'light'
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
// Try to persist to localStorage, fall back to sessionStorage
try {
localStorage.setItem(storageKey, theme)
} catch (e) {
// localStorage unavailable (incognito) - use sessionStorage
try {
sessionStorage.setItem(storageKey, theme)
} catch (err) {
// Both unavailable - just update state without persistence
console.warn('Storage unavailable, theme preference will not persist')
}
}
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error('useTheme must be used within a ThemeProvider')
return context
}

View File

@@ -0,0 +1,31 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Path aliases */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

6
templates/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

17
templates/vite.config.ts Normal file
View File

@@ -0,0 +1,17 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})