feat(03-01): bootstrap React+TS frontend with ClickHouse design system
- Vite 5 + React 18 + TypeScript 5 + Tailwind 3 scaffold in web/ - ClickHouse design tokens (volt, forest, canvas, charcoal, near-black) in tailwind.config.ts - TanStack Router v1 with placeholder routes for /, /item/$id, /intake, /scan - TanStack Query v5 QueryClientProvider + Zustand uiStore - shadcn/ui Button (neon/forest/secondary/outline/ghost), Card, Badge with ClickHouse variants - Vite proxy: /api -> http://localhost:8080 - Makefile: added frontend and dev-frontend targets - Fixed: @typescript-eslint v8 for ESLint v9 compatibility; @types/node for vite.config.ts
This commit is contained in:
parent
206d6886aa
commit
d38f93dd67
23 changed files with 5141 additions and 19 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -2,3 +2,12 @@
|
|||
# ai_config.json is committed as a template (placeholder keys only).
|
||||
# Create ai_config.local.json for real API keys — never commit it.
|
||||
ai_config.local.json
|
||||
|
||||
# Frontend build artifacts
|
||||
web/node_modules/
|
||||
web/dist/assets/
|
||||
web/*.tsbuildinfo
|
||||
|
||||
# Go build output
|
||||
bin/
|
||||
hwlab
|
||||
|
|
|
|||
8
Makefile
8
Makefile
|
|
@ -1,4 +1,4 @@
|
|||
.PHONY: build dev test clean
|
||||
.PHONY: build dev dev-frontend frontend test clean
|
||||
|
||||
build:
|
||||
go build -o bin/hwlab ./cmd/hwlab/...
|
||||
|
|
@ -6,6 +6,12 @@ build:
|
|||
dev:
|
||||
air -c .air.toml
|
||||
|
||||
frontend:
|
||||
cd web && npm run build
|
||||
|
||||
dev-frontend:
|
||||
cd web && npm run dev
|
||||
|
||||
test:
|
||||
go test ./... -v
|
||||
|
||||
|
|
|
|||
21
web/components.json
Normal file
21
web/components.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": false,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
33
web/dist/index.html
vendored
33
web/dist/index.html
vendored
|
|
@ -1,19 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>HWLab</title>
|
||||
<style>
|
||||
body { background: #000; color: #faff69; font-family: monospace; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
||||
h1 { font-size: 2rem; }
|
||||
p { color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<h1>HWLab</h1>
|
||||
<p>Backend is running. UI coming in Phase 3.</p>
|
||||
</div>
|
||||
</body>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@600&family=Inter:wght@400;500;600;700;900&display=swap" rel="stylesheet" />
|
||||
<script type="module" crossorigin src="/assets/index-DZYNd_MO.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CyeDzuEC.css">
|
||||
</head>
|
||||
<body class="bg-canvas text-white antialiased">
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
17
web/index.html
Normal file
17
web/index.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>HWLab</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@600&family=Inter:wght@400;500;600;700;900&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="bg-canvas text-white antialiased">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4580
web/package-lock.json
generated
Normal file
4580
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
42
web/package.json
Normal file
42
web/package.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "hwlab",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@tanstack/react-query": "^5.51.0",
|
||||
"@tanstack/react-router": "^1.48.0",
|
||||
"@tanstack/router-devtools": "^1.48.0",
|
||||
"@zxing/browser": "^0.1.5",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.417.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"zustand": "^4.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^9.7.0",
|
||||
"postcss": "^8.4.40",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.5"
|
||||
}
|
||||
}
|
||||
6
web/postcss.config.cjs
Normal file
6
web/postcss.config.cjs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
26
web/src/App.tsx
Normal file
26
web/src/App.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { RouterProvider } from '@tanstack/react-router'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { queryClient } from '@/lib/queryClient'
|
||||
import { router } from '@/router'
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster
|
||||
position="bottom-right"
|
||||
toastOptions={{
|
||||
style: {
|
||||
background: '#141414',
|
||||
color: '#ffffff',
|
||||
border: '1px solid rgba(65,65,65,0.8)',
|
||||
},
|
||||
success: {
|
||||
iconTheme: { primary: '#faff69', secondary: '#000000' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
33
web/src/components/ui/badge.tsx
Normal file
33
web/src/components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'border-volt/40 bg-volt/10 text-volt',
|
||||
indexed: 'border-green-500/40 bg-green-500/10 text-green-400',
|
||||
draft: 'border-charcoal/80 bg-charcoal/20 text-gray-400',
|
||||
needs_research: 'border-yellow-500/40 bg-yellow-500/10 text-yellow-400',
|
||||
researched: 'border-blue-500/40 bg-blue-500/10 text-blue-400',
|
||||
complete: 'border-forest/40 bg-forest/20 text-green-300',
|
||||
destructive: 'border-red-500/40 bg-red-500/10 text-red-400',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
59
web/src/components/ui/button.tsx
Normal file
59
web/src/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-volt disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
// Neon primary — #faff69 background, near-black text
|
||||
default: 'bg-volt text-near-black rounded-sharp border border-volt hover:bg-near-black hover:text-white active:text-volt-pale',
|
||||
// Forest green — primary action
|
||||
forest: 'bg-forest text-white rounded-sharp border border-near-black hover:bg-near-black active:text-volt-pale',
|
||||
// Dark solid
|
||||
secondary: 'bg-near-black text-white rounded-sharp border border-near-black hover:bg-hover-gray active:text-volt-pale',
|
||||
// Ghost / outlined
|
||||
outline: 'bg-transparent text-white rounded-sharp border border-border-olive hover:bg-near-black active:text-volt-pale',
|
||||
// Destructive
|
||||
destructive: 'bg-red-900 text-white rounded-sharp hover:bg-red-800',
|
||||
// Invisible — just text
|
||||
ghost: 'bg-transparent text-white hover:text-volt',
|
||||
link: 'text-volt underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 px-3 text-xs',
|
||||
lg: 'h-11 px-8',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export { Button, buttonVariants }
|
||||
44
web/src/components/ui/card.tsx
Normal file
44
web/src/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-card border border-charcoal/80 bg-near-black text-white shadow-sm transition-colors',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
)
|
||||
Card.displayName = 'Card'
|
||||
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-4', className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardHeader.displayName = 'CardHeader'
|
||||
|
||||
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h3 ref={ref} className={cn('font-semibold leading-none tracking-tight', className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardTitle.displayName = 'CardTitle'
|
||||
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => <div ref={ref} className={cn('p-4 pt-0', className)} {...props} />,
|
||||
)
|
||||
CardContent.displayName = 'CardContent'
|
||||
|
||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('flex items-center p-4 pt-0', className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardFooter.displayName = 'CardFooter'
|
||||
|
||||
export { Card, CardHeader, CardTitle, CardContent, CardFooter }
|
||||
11
web/src/lib/queryClient.ts
Normal file
11
web/src/lib/queryClient.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { QueryClient } from '@tanstack/react-query'
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 30_000, // 30s — inventory data doesn't change rapidly
|
||||
retry: 2,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
6
web/src/lib/utils.ts
Normal file
6
web/src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
10
web/src/main.tsx
Normal file
10
web/src/main.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import './styles/globals.css'
|
||||
import { App } from './App'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
67
web/src/router.tsx
Normal file
67
web/src/router.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { createRouter, createRoute, createRootRoute, Outlet } from '@tanstack/react-router'
|
||||
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
|
||||
|
||||
// Root layout — wraps all routes with the app shell
|
||||
const rootRoute = createRootRoute({
|
||||
component: () => (
|
||||
<>
|
||||
<Outlet />
|
||||
{import.meta.env.DEV && <TanStackRouterDevtools />}
|
||||
</>
|
||||
),
|
||||
})
|
||||
|
||||
// Routes — components are lazy-imported in Plan 03 and 04; stubs for now
|
||||
const indexRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/',
|
||||
component: () => (
|
||||
<div className="min-h-screen bg-canvas flex items-center justify-center">
|
||||
<p className="text-volt font-display font-black text-2xl">HWLab — Dashboard loading…</p>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
|
||||
const itemRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/item/$id',
|
||||
component: () => (
|
||||
<div className="min-h-screen bg-canvas flex items-center justify-center">
|
||||
<p className="text-volt font-display font-black text-2xl">Item detail loading…</p>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
|
||||
const intakeRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/intake',
|
||||
component: () => (
|
||||
<div className="min-h-screen bg-canvas flex items-center justify-center">
|
||||
<p className="text-volt font-display font-black text-2xl">Intake wizard loading…</p>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
|
||||
const scanRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/scan',
|
||||
component: () => (
|
||||
<div className="min-h-screen bg-canvas flex items-center justify-center">
|
||||
<p className="text-volt font-display font-black text-2xl">QR Scanner loading…</p>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
|
||||
const routeTree = rootRoute.addChildren([indexRoute, itemRoute, intakeRoute, scanRoute])
|
||||
|
||||
export const router = createRouter({
|
||||
routeTree,
|
||||
defaultPreload: 'intent',
|
||||
})
|
||||
|
||||
// TypeScript type augmentation for TanStack Router
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router
|
||||
}
|
||||
}
|
||||
21
web/src/store/ui.ts
Normal file
21
web/src/store/ui.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { create } from 'zustand'
|
||||
|
||||
type ViewMode = 'grid' | 'list'
|
||||
|
||||
interface UIStore {
|
||||
viewMode: ViewMode
|
||||
setViewMode: (mode: ViewMode) => void
|
||||
scannerActive: boolean
|
||||
setScannerActive: (active: boolean) => void
|
||||
intakeStep: number
|
||||
setIntakeStep: (step: number) => void
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIStore>((set) => ({
|
||||
viewMode: 'grid',
|
||||
setViewMode: (mode) => set({ viewMode: mode }),
|
||||
scannerActive: false,
|
||||
setScannerActive: (active) => set({ scannerActive: active }),
|
||||
intakeStep: 0,
|
||||
setIntakeStep: (step) => set({ intakeStep: step }),
|
||||
}))
|
||||
49
web/src/styles/globals.css
Normal file
49
web/src/styles/globals.css
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 0%;
|
||||
--foreground: 0 0% 100%;
|
||||
--card: 0 0% 8%;
|
||||
--card-foreground: 0 0% 100%;
|
||||
--border: 0 0% 25%;
|
||||
--ring: 64 100% 71%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
* {
|
||||
border-color: rgba(65, 65, 65, 0.8);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Charcoal border utility */
|
||||
.border-charcoal-80 {
|
||||
border-color: rgba(65, 65, 65, 0.8);
|
||||
}
|
||||
|
||||
/* ClickHouse card elevation */
|
||||
.card-elevated {
|
||||
box-shadow:
|
||||
rgba(0,0,0,0.1) 0px 10px 15px -3px,
|
||||
rgba(0,0,0,0.1) 0px 4px 6px -4px;
|
||||
}
|
||||
|
||||
/* Neon accent glow (use sparingly) */
|
||||
.ring-volt {
|
||||
box-shadow: 0 0 0 2px #faff69;
|
||||
}
|
||||
|
||||
/* Uppercase tracked label */
|
||||
.label-upper {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.4px;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
43
web/tailwind.config.ts
Normal file
43
web/tailwind.config.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config: Config = {
|
||||
darkMode: ['class'],
|
||||
content: ['./index.html', './src/**/*.{ts,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// ClickHouse design system tokens
|
||||
'volt': '#faff69', // neon yellow-green — primary CTA accent
|
||||
'volt-pale': '#f4f692', // active/pressed state text
|
||||
'forest': '#166534', // primary action button (Get Started)
|
||||
'forest-dark': '#14572f', // forest border variant
|
||||
'olive-dark': '#161600', // subtle brand text
|
||||
'border-olive': '#4f5100', // ghost button borders
|
||||
// Surfaces
|
||||
'canvas': '#000000', // pure black background
|
||||
'near-black': '#141414', // button bg, elevated surfaces
|
||||
'hover-gray': '#3a3a3a', // button hover bg
|
||||
'charcoal': '#414141', // primary border (used at 80% opacity in CSS)
|
||||
'deep-charcoal': '#343434', // subtle dividers
|
||||
},
|
||||
fontFamily: {
|
||||
display: ['Inter', 'system-ui', 'sans-serif'],
|
||||
body: ['Inter', 'system-ui', 'sans-serif'],
|
||||
code: ['Inconsolata', 'monospace'],
|
||||
},
|
||||
fontWeight: {
|
||||
black: '900',
|
||||
},
|
||||
borderRadius: {
|
||||
sharp: '4px',
|
||||
card: '8px',
|
||||
},
|
||||
letterSpacing: {
|
||||
widest: '1.4px',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
export default config
|
||||
25
web/tsconfig.app.json
Normal file
25
web/tsconfig.app.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"types": ["vite/client"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
web/tsconfig.json
Normal file
7
web/tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
16
web/tsconfig.node.json
Normal file
16
web/tsconfig.node.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"types": ["node"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
25
web/vite.config.ts
Normal file
25
web/vite.config.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue