Phase 0: project scaffold
- Go module + chi server with embedded SPA serving and /api/health - internal/config env loader (local-dev defaults; auth/copyleaks deferred) - React 19 + Vite 6 + Tailwind v4 frontend with full Petal design tokens - Frontend embedded into the binary via web/embed.go (go:embed all:dist) - README dev workflow, .env.example, BUILD_PLAN progress tracker - Verified end-to-end: binary serves health, embedded SPA, and SPA fallback
This commit is contained in:
0
web/dist/.gitkeep
vendored
Normal file
0
web/dist/.gitkeep
vendored
Normal file
12
web/embed.go
Normal file
12
web/embed.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// Package web embeds the built frontend assets so the Go binary can serve them
|
||||
// directly (single-binary deployment). The embed path is relative to this file,
|
||||
// so it must live alongside the dist/ directory.
|
||||
package web
|
||||
|
||||
import "embed"
|
||||
|
||||
// DistFS holds the built frontend (web/dist). `all:` includes the committed
|
||||
// .gitkeep placeholder, so this compiles even before `npm run build` runs.
|
||||
//
|
||||
//go:embed all:dist
|
||||
var DistFS embed.FS
|
||||
19
web/index.html
Normal file
19
web/index.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><text y='14' font-size='14'>🌸</text></svg>" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Petal</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=Nunito:wght@400;600;700;800&family=Lora:ital,wght@0,400;0,500;1,400&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
3260
web/package-lock.json
generated
Normal file
3260
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
web/package.json
Normal file
31
web/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "petal-web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"@tiptap/react": "^2.11.5",
|
||||
"@tiptap/pm": "^2.11.5",
|
||||
"@tiptap/starter-kit": "^2.11.5",
|
||||
"@tiptap/extension-underline": "^2.11.5",
|
||||
"@tiptap/extension-text-align": "^2.11.5",
|
||||
"@tiptap/extension-placeholder": "^2.11.5",
|
||||
"@tiptap/extension-character-count": "^2.11.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.1.0"
|
||||
}
|
||||
}
|
||||
46
web/src/App.tsx
Normal file
46
web/src/App.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// Phase 0 placeholder: proves the stack end-to-end — React + Tailwind v4 tokens
|
||||
// rendering, and the Go backend reachable via the /api proxy. Real UI lands in
|
||||
// later phases (DocList, Editor, StatusBar).
|
||||
export default function App() {
|
||||
const [api, setApi] = useState<'checking' | 'ok' | 'down'>('checking')
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/health')
|
||||
.then((r) => setApi(r.ok ? 'ok' : 'down'))
|
||||
.catch(() => setApi('down'))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-6 px-6 text-center">
|
||||
<div
|
||||
className="flex flex-col items-center gap-3 bg-surface px-12 py-10"
|
||||
style={{ borderRadius: 'var(--radius-card)', boxShadow: 'var(--shadow-soft)' }}
|
||||
>
|
||||
<span className="text-5xl">🌸</span>
|
||||
<h1 className="text-3xl font-extrabold text-plum">Petal</h1>
|
||||
<p className="max-w-xs text-muted" style={{ fontFamily: 'var(--font-body)' }}>
|
||||
A cozy place to write.
|
||||
</p>
|
||||
<span
|
||||
className="mt-2 inline-flex items-center gap-2 px-4 py-1.5 text-sm font-semibold"
|
||||
style={{ borderRadius: 'var(--radius-pill)', background: 'var(--color-surface-alt)' }}
|
||||
>
|
||||
<span
|
||||
className="inline-block h-2 w-2 rounded-full"
|
||||
style={{
|
||||
background:
|
||||
api === 'ok'
|
||||
? 'var(--color-success)'
|
||||
: api === 'down'
|
||||
? 'var(--color-accent)'
|
||||
: 'var(--color-muted)',
|
||||
}}
|
||||
/>
|
||||
{api === 'checking' ? 'Checking backend…' : api === 'ok' ? 'Backend connected' : 'Backend offline'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
55
web/src/index.css
Normal file
55
web/src/index.css
Normal file
@@ -0,0 +1,55 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Petal design tokens — soft, warm, bubbly stationery aesthetic.
|
||||
Defined in @theme so they generate Tailwind utilities (bg-bg, text-plum,
|
||||
rounded-card, font-ui, etc.). See petal-spec.md → Design System. */
|
||||
@theme {
|
||||
/* Palette */
|
||||
--color-bg: #FDF6F0; /* warm cream canvas */
|
||||
--color-surface: #FFFFFF; /* card/panel surfaces */
|
||||
--color-surface-alt: #FFF0F5; /* rose-tinted alt surface */
|
||||
--color-border: #F0E0EB; /* soft pink-grey border */
|
||||
--color-plum: #3D2E39; /* warm dark plum text (not harsh black) */
|
||||
--color-muted: #9B8CA3; /* muted lavender-grey */
|
||||
--color-accent: #E8A0BF; /* soft rose — primary interactive */
|
||||
--color-accent-hover: #D98AAF;/* deeper rose on hover */
|
||||
|
||||
/* Suggestion type colors */
|
||||
--color-mint: #A8D8C8; /* grammar */
|
||||
--color-peach: #F4B8A0; /* phrasing */
|
||||
--color-lavender: #C5B4E8; /* idiom */
|
||||
--color-sky: #A8CCE8; /* clarity */
|
||||
--color-honey: #CE9B4F; /* voice */
|
||||
--color-success: #8FCFA8; /* saved / accepted */
|
||||
|
||||
/* Typography */
|
||||
--font-ui: "Nunito", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
|
||||
--font-body: "Lora", Georgia, serif;
|
||||
--font-mono: "JetBrains Mono", ui-monospace, monospace;
|
||||
|
||||
/* Shape language */
|
||||
--radius-card: 16px;
|
||||
--radius-modal: 24px;
|
||||
--radius-pill: 999px;
|
||||
--radius-input: 12px;
|
||||
|
||||
/* Soft, warm, lifted shadow */
|
||||
--shadow-soft: 0 4px 20px rgba(180, 130, 160, 0.12);
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-plum);
|
||||
font-family: var(--font-ui);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* Gentle, consistent motion across interactive elements */
|
||||
button, a, input {
|
||||
transition: all 200ms ease;
|
||||
}
|
||||
10
web/src/main.tsx
Normal file
10
web/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
20
web/tsconfig.json
Normal file
20
web/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "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
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
21
web/vite.config.ts
Normal file
21
web/vite.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// Dev: Vite serves the UI on :5173 and proxies /api to the Go backend on :8080.
|
||||
// Prod: `vite build` emits web/dist, which the Go binary embeds and serves itself.
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': 'http://localhost:8080',
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
// Keep false so the committed dist/.gitkeep placeholder survives builds —
|
||||
// it's what lets `go build` compile on a fresh clone before any frontend build.
|
||||
emptyOutDir: false,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user