Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .cursor/rules/no-default-export-typescript.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
description: Named exports only in TypeScript; no default exports
globs:
- "**/*.ts"
- "**/*.tsx"
alwaysApply: false
---

# No default exports (TypeScript)

- Prefer **named exports** only: `export const Component = …`, `export { foo }`, and similar.
- Do **not** add `export default`, including `export { Name as default }`, unless the file is an allowed tooling entrypoint (see below).

**Exception:** `apps/web/vite.config.ts` and `apps/web/openapi-ts.config.ts` may use `export default` because Vite and `@hey-api/openapi-ts` load the config via the module’s default export. Biome disables `style/noDefaultExport` for those paths in `apps/web/biome.json`.

Where this repository runs Biome with `style.noDefaultExport`, that config applies to the linted tree (see `apps/web/biome.json`).
2 changes: 1 addition & 1 deletion PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Tasks and subtasks for building the bread-recipes app (SolidJS + Python REST + O

- [x] **4.1** Scaffold SolidJS + Vite + TypeScript in `apps/web`; **Biome** (lint + format + import organise); Vitest configured; exact dependency versions only; `README.md` for the app.
- [x] **4.2** Generate or synchronise typed API usage from the OpenAPI spec (client/types) so API calls stay strictly typed.
- [ ] **4.3** App shell: router, layout, and global styles (clean, minimalist, bread-appropriate palette, responsive).
- [x] **4.3** App shell: router, layout, and global styles (clean, minimalist, bread-appropriate palette, responsive).
- [ ] **4.4** Home page: fetch and list bread recipes with overview + thumbnail; navigate to detail on click.
- [ ] **4.5** Recipe page: full recipe content and larger image; deep-linkable route (e.g. by id).
- [ ] **4.6** MSW for tests; knip configured; Vitest coverage at 100% with a CI gate.
Expand Down
4 changes: 4 additions & 0 deletions apps/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ Run from **`apps/web`** (or via **`pnpm --filter web <script>`** from the root):

Configuration: **`biome.json`** (Biome **2.4.x**), **`vite.config.ts`** (includes Vitest), **`tsconfig.*.json`**.

## App shell (PLAN §4.3)

Routing uses [**`@solidjs/router`**](https://github.com/solidjs/solid-router): **`Router`** with a shared **`AppShell`** layout (header, main outlet, footer). Routes include **`/`** (home) and **`/recipes/:id`** (detail placeholder until §4.5). Global styles live in **`src/index.css`** (warm bread surfaces, cool complementary accents); layout CSS in **`src/layout/AppShell.css`**.

## OpenAPI client

The UI uses a **full fetch-based client** generated from **`../../packages/openapi/openapi.yaml`** via [**`@hey-api/openapi-ts`**](https://github.com/hey-api/openapi-ts). Generated output lives under **`src/api/generated/`** (do not edit by hand; regenerate after spec changes).
Expand Down
23 changes: 22 additions & 1 deletion apps/web/biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,30 @@
"linter": {
"enabled": true,
"rules": {
"recommended": true
"recommended": true,
"complexity": {
"useArrowFunction": "error"
},
"nursery": {
"useExplicitType": "error"
},
"style": {
"noDefaultExport": "error"
}
}
},
"overrides": [
{
"includes": ["vite.config.ts", "openapi-ts.config.ts"],
"linter": {
"rules": {
"style": {
"noDefaultExport": "off"
}
}
}
}
],
"javascript": {
"formatter": {
"quoteStyle": "single",
Expand Down
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"openapi:validate": "pnpm --filter @solid-pact/openapi run lint"
},
"dependencies": {
"@solidjs/router": "0.16.1",
"solid-js": "1.9.12"
},
"devDependencies": {
Expand Down
22 changes: 0 additions & 22 deletions apps/web/src/App.css

This file was deleted.

2 changes: 1 addition & 1 deletion apps/web/src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { render, screen } from '@solidjs/testing-library';
import { describe, expect, it } from 'vitest';
import App from './App';
import { App } from './App';

describe('App', () => {
it('renders the main heading', () => {
Expand Down
35 changes: 11 additions & 24 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
import { createSignal } from 'solid-js';
import './App.css';
import { Route, Router } from '@solidjs/router';
import type { JSX } from 'solid-js';
import { AppShell } from './layout/AppShell';
import { Home } from './pages/Home';
import { RecipePage } from './pages/RecipePage';

function App() {
const [count, setCount] = createSignal(0);

return (
<main class="app">
<h1>Bread Recipes</h1>
<p class="lede">
SolidJS + Vite scaffold (PLAN §4.1). Replace this with the real UI in
later tasks.
</p>
<button
type="button"
class="counter"
onClick={() => setCount((c) => c + 1)}
>
Count is {count()}
</button>
</main>
);
}

export default App;
export const App = (): JSX.Element => (
<Router root={AppShell}>
<Route path="/" component={Home} />
<Route path="/recipes/:id" component={RecipePage} />
</Router>
);
3 changes: 2 additions & 1 deletion apps/web/src/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
type Client,
type ClientOptions,
createClient,
createConfig,
Expand All @@ -16,6 +17,6 @@ function resolveBaseUrl(): string {
}

/** Shared fetch client for generated SDK calls (base URL from `VITE_API_BASE_URL`, or local API in dev). */
export const apiClient = createClient(
export const apiClient: Client = createClient(
createConfig<ClientOptions>({ baseUrl: resolveBaseUrl() }),
);
14 changes: 6 additions & 8 deletions apps/web/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,20 @@ import type { GetRecipeByIdData, ListRecipesData } from './generated/types.gen';

export { apiClient } from './client';

export function listRecipes<ThrowOnError extends boolean = false>(
export const listRecipes = <ThrowOnError extends boolean = false>(
options?: Options<ListRecipesData, ThrowOnError>,
) {
return listRecipesSdk({
): ReturnType<typeof listRecipesSdk<ThrowOnError>> =>
listRecipesSdk({
...options,
client: options?.client ?? apiClient,
});
}

export function getRecipeById<ThrowOnError extends boolean = false>(
export const getRecipeById = <ThrowOnError extends boolean = false>(
options: Options<GetRecipeByIdData, ThrowOnError>,
) {
return getRecipeByIdSdk({
): ReturnType<typeof getRecipeByIdSdk<ThrowOnError>> =>
getRecipeByIdSdk({
...options,
client: options?.client ?? apiClient,
});
}

export type * from './generated/types.gen';
113 changes: 58 additions & 55 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
:root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
/*
* Surfaces: warm flour / crust / wheat (bread).
* Accents: cool slate–teal — complementary to golden bread on the wheel.
*/
--text: #5c5348;
--text-h: #2a241c;
--text-muted: #7a7167;
--bg: #faf7f2;
--header-bg: rgba(255, 252, 248, 0.92);
--footer-bg: rgba(244, 239, 230, 0.65);
--border: #e8e4dc;
--code-bg: #e9f0f3;
--accent: #3a6f82;
--accent-hover: #2d5a6a;
--accent-bg: rgba(58, 111, 130, 0.12);
Comment on lines +14 to +16
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--accent-hover is defined here (and in the dark-scheme override) but isn’t used anywhere in the current styles. Consider either using it for hover states (e.g., link hover colors) or removing it to avoid carrying unused design tokens.

Copilot uses AI. Check for mistakes.
--accent-border: rgba(58, 111, 130, 0.42);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
rgba(45, 70, 85, 0.07) 0 10px 24px -6px,
rgba(40, 55, 65, 0.05) 0 4px 8px -4px;

--sans: system-ui, "Segoe UI", Roboto, sans-serif;
--heading: system-ui, "Segoe UI", Roboto, sans-serif;
--sans: system-ui, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
--heading: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
--mono: ui-monospace, Consolas, monospace;

font: 18px / 145% var(--sans);
letter-spacing: 0.18px;
font: 17px / 1.5 var(--sans);
letter-spacing: 0.01em;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
Expand All @@ -32,17 +41,21 @@

@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--text: #c4bbb0;
--text-h: #f2ebe3;
--text-muted: #9a9188;
--bg: #1a1816;
--header-bg: rgba(28, 26, 23, 0.94);
--footer-bg: rgba(22, 20, 18, 0.85);
--border: #3d3834;
--code-bg: #1e2a2e;
--accent: #7eb8c8;
--accent-hover: #a3d4e0;
--accent-bg: rgba(126, 184, 200, 0.14);
--accent-border: rgba(126, 184, 200, 0.45);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
rgba(0, 0, 0, 0.35) 0 12px 28px -8px, rgba(0, 0, 0, 0.2) 0 4px 10px -4px;
}

#social .button-icon {
Expand All @@ -55,57 +68,47 @@ body {
}

#root {
width: 1126px;
max-width: 100%;
width: 100%;
max-width: 720px;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
min-height: 100dvh;
display: flex;
flex-direction: column;
box-sizing: border-box;
border-inline: 1px solid var(--border);
padding-inline: max(0px, env(safe-area-inset-left))
max(0px, env(safe-area-inset-right));
padding-bottom: env(safe-area-inset-bottom);
}

@media (min-width: 768px) {
#root {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
min-height: calc(100svh - 1rem);
border-radius: 12px;
overflow: clip;
box-shadow: var(--shadow);
}
}

h1,
h2 {
h2,
h3 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}

h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}

code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}

code {
font-family: var(--mono);
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
border-radius: 4px;
color: var(--text-h);
}
7 changes: 4 additions & 3 deletions apps/web/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/* @refresh reload */
import type { JSX } from 'solid-js';
import { render } from 'solid-js/web';
import './index.css';
import App from './App.tsx';
import { App } from './App.tsx';

const root = document.getElementById('root');
const root: HTMLElement | null = document.getElementById('root');
if (!root) {
throw new Error('Missing #root element');
}

render(() => <App />, root);
render((): JSX.Element => <App />, root);
Loading
Loading