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)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
- Vite + React + TypeScript (strict)
|
||||
- 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
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["react", "typescript", "oxc"],
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"rules": {
|
||||
"react/rules-of-hooks": "error",
|
||||
"react/only-export-components": ["warn", { "allowConstantExport": true }]
|
||||
}
|
||||
}
|
||||
```
|
||||
podman exec buckets-budget_frontend_1 npm run build # tsc -b && vite build
|
||||
podman exec buckets-budget_frontend_1 npm run lint # oxlint
|
||||
podman exec buckets-budget_frontend_1 npm run format # prettier --write
|
||||
podman exec buckets-budget_frontend_1 npm run format:check # prettier --check
|
||||
```
|
||||
|
||||
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 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() {
|
||||
return (
|
||||
<AppLayout>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ interface AppLayoutProps {
|
|||
export default function AppLayout({ children }: AppLayoutProps) {
|
||||
return (
|
||||
<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">
|
||||
BUCKETS
|
||||
</h1>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default function Button({ glow, className, ...props }: ButtonProps) {
|
|||
return (
|
||||
<button
|
||||
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',
|
||||
className,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -5,19 +5,21 @@ interface DigitalProgressBarProps {
|
|||
|
||||
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
|
||||
// derived from the same token via color-mix rather than a duplicated literal.
|
||||
function getZoneToken(index: number): string {
|
||||
if (index < 10) return 'var(--chart-1)'
|
||||
if (index < 18) return 'var(--chart-2)'
|
||||
return 'var(--chart-3)'
|
||||
}
|
||||
/**
|
||||
* Color zones by bar index, driven by the theme's chart tokens (chart-1 red →
|
||||
* chart-2 amber → chart-3 green) so they stay in sync with the palette. `token`
|
||||
* feeds the glow via color-mix; `className` is the Tailwind utility for the fill
|
||||
* — both reference the same source so they can't drift. `upTo` is the exclusive
|
||||
* upper bound of each zone.
|
||||
*/
|
||||
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 {
|
||||
if (index < 10) return 'bg-chart-1'
|
||||
if (index < 18) return 'bg-chart-2'
|
||||
return 'bg-chart-3'
|
||||
function getZone(index: number) {
|
||||
return ZONES.find((zone) => index < zone.upTo) ?? ZONES[ZONES.length - 1]
|
||||
}
|
||||
|
||||
export default function DigitalProgressBar({
|
||||
|
|
@ -40,6 +42,7 @@ export default function DigitalProgressBar({
|
|||
>
|
||||
{Array.from({ length: BAR_COUNT }, (_, i) => {
|
||||
const filled = i < filledBars
|
||||
const zone = getZone(i)
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
|
|
@ -47,10 +50,10 @@ export default function DigitalProgressBar({
|
|||
borderRadius: '3px',
|
||||
transition: 'background-color 300ms, box-shadow 300ms',
|
||||
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',
|
||||
}}
|
||||
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) {
|
||||
return (
|
||||
<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}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@
|
|||
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 *));
|
||||
|
||||
@theme {
|
||||
|
|
|
|||
Loading…
Reference in a new issue