33 - Use theme tokens in primitives and replace template boilerplate
This commit is contained in:
parent
79367b3de0
commit
f76c7b9ecd
8 changed files with 57 additions and 40 deletions
|
|
@ -1,32 +1,38 @@
|
||||||
# React + TypeScript + Vite
|
# Buckets — Frontend
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
|
Standalone React SPA for the Buckets budgeting app. Talks to the Symfony +
|
||||||
|
API Platform backend over a cookie-session API (`/api`).
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
## Stack
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
- Vite + React + TypeScript (strict)
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
- Tailwind 4 (`@tailwindcss/vite`) — dark-only "80s terminal" theme
|
||||||
|
- oxlint (lint) + Prettier (format)
|
||||||
|
|
||||||
## React Compiler
|
## Development
|
||||||
|
|
||||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
The frontend runs as the `frontend` service in the project's compose stack
|
||||||
|
(podman-compose, dev-only — defined in `compose.override.yaml`). Bring the stack
|
||||||
|
up from the repo root (`dev-up`) and the Vite dev server is published on
|
||||||
|
**http://localhost:5173**.
|
||||||
|
|
||||||
## Expanding the Oxlint configuration
|
The dev server proxies `/api` to the backend container (`http://php:80`) so the
|
||||||
|
session cookie flows same-origin — no CORS in dev.
|
||||||
|
|
||||||
If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
|
Run commands inside the container:
|
||||||
|
|
||||||
```json
|
```
|
||||||
{
|
podman exec buckets-budget_frontend_1 npm run build # tsc -b && vite build
|
||||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
podman exec buckets-budget_frontend_1 npm run lint # oxlint
|
||||||
"plugins": ["react", "typescript", "oxc"],
|
podman exec buckets-budget_frontend_1 npm run format # prettier --write
|
||||||
"options": {
|
podman exec buckets-budget_frontend_1 npm run format:check # prettier --check
|
||||||
"typeAware": true
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"react/rules-of-hooks": "error",
|
|
||||||
"react/only-export-components": ["warn", { "allowConstantExport": true }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
|
## Structure
|
||||||
|
|
||||||
|
- `src/theme.css` — Tailwind import + the 80s terminal theme (OKLCH tokens, glow,
|
||||||
|
7-segment font)
|
||||||
|
- `src/components/ui/` — shared primitives (Button, Panel, DigitalProgressBar)
|
||||||
|
- `src/components/layout/` — the app shell (AppLayout)
|
||||||
|
- `src/lib/api.ts` — cookie-session API client (get/post/patch/delete, JSON-LD)
|
||||||
|
- `src/types/api.ts` — Hydra/JSON-LD envelope types
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 242 B |
|
|
@ -3,6 +3,8 @@ import Button from './components/ui/Button'
|
||||||
import DigitalProgressBar from './components/ui/DigitalProgressBar'
|
import DigitalProgressBar from './components/ui/DigitalProgressBar'
|
||||||
import Panel from './components/ui/Panel'
|
import Panel from './components/ui/Panel'
|
||||||
|
|
||||||
|
// Scaffold-only demo composition proving the primitives + theme render.
|
||||||
|
// Replaced by real routed pages in the frontend feature tickets (#52–#56).
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ interface AppLayoutProps {
|
||||||
export default function AppLayout({ children }: AppLayoutProps) {
|
export default function AppLayout({ children }: AppLayoutProps) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-background text-foreground min-h-screen font-mono">
|
<div className="bg-background text-foreground min-h-screen font-mono">
|
||||||
<header className="glow-red border-b-4 border-red-500 px-6 py-4">
|
<header className="glow-red border-primary border-b-4 px-6 py-4">
|
||||||
<h1 className="text-primary text-2xl font-bold uppercase tracking-widest">
|
<h1 className="text-primary text-2xl font-bold uppercase tracking-widest">
|
||||||
BUCKETS
|
BUCKETS
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export default function Button({ glow, className, ...props }: ButtonProps) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-2 border-red-500 bg-black px-4 py-2 font-mono font-bold uppercase tracking-wider text-red-500 transition-colors hover:bg-red-500 hover:text-black',
|
'border-primary text-primary hover:bg-primary hover:text-primary-foreground border-2 bg-background px-4 py-2 font-mono font-bold uppercase tracking-wider transition-colors',
|
||||||
glow && 'glow-red',
|
glow && 'glow-red',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,21 @@ interface DigitalProgressBarProps {
|
||||||
|
|
||||||
const BAR_COUNT = 20
|
const BAR_COUNT = 20
|
||||||
|
|
||||||
// Zone colors are driven by the theme's chart tokens (chart-1 red → chart-2
|
/**
|
||||||
// amber → chart-3 green) so they stay in sync with the palette; the glow is
|
* Color zones by bar index, driven by the theme's chart tokens (chart-1 red →
|
||||||
// derived from the same token via color-mix rather than a duplicated literal.
|
* chart-2 amber → chart-3 green) so they stay in sync with the palette. `token`
|
||||||
function getZoneToken(index: number): string {
|
* feeds the glow via color-mix; `className` is the Tailwind utility for the fill
|
||||||
if (index < 10) return 'var(--chart-1)'
|
* — both reference the same source so they can't drift. `upTo` is the exclusive
|
||||||
if (index < 18) return 'var(--chart-2)'
|
* upper bound of each zone.
|
||||||
return 'var(--chart-3)'
|
*/
|
||||||
}
|
const ZONES = [
|
||||||
|
{ upTo: 10, token: 'var(--chart-1)', className: 'bg-chart-1' },
|
||||||
|
{ upTo: 18, token: 'var(--chart-2)', className: 'bg-chart-2' },
|
||||||
|
{ upTo: BAR_COUNT, token: 'var(--chart-3)', className: 'bg-chart-3' },
|
||||||
|
] as const
|
||||||
|
|
||||||
function getBarColor(index: number): string {
|
function getZone(index: number) {
|
||||||
if (index < 10) return 'bg-chart-1'
|
return ZONES.find((zone) => index < zone.upTo) ?? ZONES[ZONES.length - 1]
|
||||||
if (index < 18) return 'bg-chart-2'
|
|
||||||
return 'bg-chart-3'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DigitalProgressBar({
|
export default function DigitalProgressBar({
|
||||||
|
|
@ -40,6 +42,7 @@ export default function DigitalProgressBar({
|
||||||
>
|
>
|
||||||
{Array.from({ length: BAR_COUNT }, (_, i) => {
|
{Array.from({ length: BAR_COUNT }, (_, i) => {
|
||||||
const filled = i < filledBars
|
const filled = i < filledBars
|
||||||
|
const zone = getZone(i)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
|
|
@ -47,10 +50,10 @@ export default function DigitalProgressBar({
|
||||||
borderRadius: '3px',
|
borderRadius: '3px',
|
||||||
transition: 'background-color 300ms, box-shadow 300ms',
|
transition: 'background-color 300ms, box-shadow 300ms',
|
||||||
boxShadow: filled
|
boxShadow: filled
|
||||||
? `0 0 8px color-mix(in oklch, ${getZoneToken(i)} 60%, transparent)`
|
? `0 0 8px color-mix(in oklch, ${zone.token} 60%, transparent)`
|
||||||
: 'none',
|
: 'none',
|
||||||
}}
|
}}
|
||||||
className={filled ? getBarColor(i) : 'bg-chart-1/10'}
|
className={filled ? zone.className : 'bg-chart-1/10'}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ type PanelProps = HTMLAttributes<HTMLDivElement>
|
||||||
export default function Panel({ className, ...props }: PanelProps) {
|
export default function Panel({ className, ...props }: PanelProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('glow-red border-4 border-red-500 bg-black p-6', className)}
|
className={cn('glow-red border-primary border-4 bg-card p-6', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@
|
||||||
box-shadow: 0 0 25px rgba(239, 68, 68, 0.6);
|
box-shadow: 0 0 25px rgba(239, 68, 68, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Reserved: the app is dark-only (palette lives unconditionally in :root), so
|
||||||
|
* no `dark:` utilities are used yet. Kept for forward-compatibility if a light
|
||||||
|
* mode is ever added. */
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue