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
5 changes: 3 additions & 2 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ Tasks and subtasks for building the bread-recipes app (SolidJS + Python REST + O
- [x] **4.2** Generate or synchronise typed API usage from the OpenAPI spec (client/types) so API calls stay strictly typed.
- [x] **4.3** App shell: router, layout, and global styles (clean, minimalist, bread-appropriate palette, responsive).
- [x] **4.4** Home page: fetch and list bread recipes with overview + thumbnail; navigate to detail on click.
- [ ] **4.5** Component library: evaluate options for SolidJS (e.g. **shadcn-solid** with Tailwind and Kobalte primitives vs smaller headless stacks); record the decision; add the chosen tooling and migrate or adopt components on at least one real screen so the pattern is established.
- [x] **4.5** Component library: evaluate options for SolidJS (e.g. **shadcn-solid** with Tailwind vs smaller stacks); record the decision; add the chosen tooling and migrate or adopt components on at least one real screen so the pattern is established.
- [x] **4.6** Recipe page: full recipe content and larger image; deep-linkable route (e.g. by id).
- [ ] **4.7** MSW for tests; knip configured; Vitest coverage at 100% with a CI gate.
- [ ] **4.7** **shadcn-solid adoption (full):** migrate remaining UI (app shell, home, recipe cards and detail sections, and any shared layout) to registry components and Tailwind utilities where it replaces bespoke CSS; align tokens with **`COMPONENT_LIBRARY.md`**; no orphaned hand-rolled controls that duplicate registry patterns. Update **`COMPONENT_LIBRARY.md`** when scope is complete.
- [ ] **4.8** MSW for tests; knip configured; Vitest coverage at 100% with a CI gate.

## 5. Contract testing (Pact)

Expand Down
33 changes: 33 additions & 0 deletions apps/web/COMPONENT_LIBRARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Component library (PLAN §4.5)

## Decision: [shadcn-solid](https://shadcn-solid.com/)

We standardise on **shadcn-solid** — the SolidJS port of **[shadcn/ui](https://ui.shadcn.com/)**: copy-in components under **`src/components/ui/`**, **Tailwind CSS**, **class-variance-authority**, **`cn()`** (`clsx` + `tailwind-merge`), and the **registry CLI** to add or refresh blocks.

Implementation details (Kobalte, CVA, etc.) are whatever the **published registry** uses for each component; we do **not** pick a separate primitive stack as the product decision — **shadcn-solid** is the choice.

## Options considered (summary)

| Option | Notes |
| ------ | ----- |
| **shadcn-solid** | Same mental model as shadcn/ui: own the source, consistent variants, CLI adds components. |
| **Ad hoc Tailwind only** | Fast for one-offs; diverges from a shared design system. |
| **A full component npm package** | Version churn; less control than vendored registry files. |

## Tooling in this repo

- **Tailwind CSS v4** via **`@tailwindcss/vite`** (`vite.config.ts`).
- **`tailwindcss-animate`** (`@plugin` in `src/index.css`).
- **Path alias `@/`** for imports (aligned with [shadcn-solid manual install](https://shadcn-solid.com/docs/installation/manual)).
- **`src/lib/utils.ts`** — **`cn()`** helper as in the docs.

## Adding or updating UI

1. From **`apps/web`**, run **`pnpm dlx shadcn-solid@latest init`** once if **`components.json`** is missing (needs registry access).
2. Add components: **`pnpm dlx shadcn-solid@latest add <name>`** (e.g. **`button`**, **`card`**). Use **`--overwrite`** when refreshing an existing file.
3. Prefer **`@/`** imports; use **`./`** for same-folder modules where Biome allows (see project rules).

## Adoption so far

- **`src/components/ui/button.tsx`** — **shadcn-solid-style** `Button` + **`buttonVariants`** (see [Button](https://shadcn-solid.com/docs/components/button)). For router links, the docs recommend **`buttonVariants` on `<A>`** — used on **recipe detail** for “← All recipes” (`RecipePage`).
- Existing bread **CSS variables** in **`src/index.css`** remain the source of theme tokens; utility classes reference them where the default shadcn HSL tokens are not wired yet.
6 changes: 5 additions & 1 deletion apps/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ Configuration: **`biome.json`** (Biome **2.4.x**), **`vite.config.ts`** (include

## 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`**.
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). Global styles live in **`src/index.css`** (warm bread surfaces, cool complementary accents); layout CSS in **`src/layout/AppShell.css`**.

## Component library (PLAN §4.5)

**[shadcn-solid](https://shadcn-solid.com/)** — registry components under **`src/components/ui/`**, Tailwind v4, and **`cn()`**. Rationale and workflow: **[`COMPONENT_LIBRARY.md`](./COMPONENT_LIBRARY.md)**.

## OpenAPI client

Expand Down
5 changes: 5 additions & 0 deletions apps/web/biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
}
}
],
"css": {
"parser": {
"tailwindDirectives": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
Expand Down
9 changes: 8 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,22 @@
"openapi:validate": "pnpm --filter @solid-pact/openapi run lint"
},
"dependencies": {
"@kobalte/core": "0.13.11",
"@solidjs/router": "0.16.1",
"solid-js": "1.9.12"
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"solid-js": "1.9.12",
"tailwind-merge": "3.5.0"
},
"devDependencies": {
"@biomejs/biome": "2.4.10",
"@hey-api/openapi-ts": "0.95.0",
"@solidjs/testing-library": "0.8.10",
"@tailwindcss/vite": "4.2.2",
"@types/node": "24.12.0",
"jsdom": "29.0.1",
"tailwindcss": "4.2.2",
"tailwindcss-animate": "1.0.7",
"typescript": "5.9.3",
"vite": "8.0.3",
"vite-plugin-solid": "2.11.11",
Expand Down
62 changes: 62 additions & 0 deletions apps/web/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* shadcn-solid Button — this file *is* the shadcn component (vendored registry source, not an npm import).
* There is no `from "shadcn-solid"` package for UI: the CLI copies components into `components/ui/`.
*
* The upstream registry implementation wraps Kobalte’s primitive — so `@kobalte/core` appears here by design.
* @see https://shadcn-solid.com/docs/components/button
*
* Refresh from registry: `pnpm dlx shadcn-solid@latest add button --overwrite` (after `init`).
*/
import { Button as KobalteButton } from '@kobalte/core/button';
import { cva, type VariantProps } from 'class-variance-authority';
import type { ComponentProps, JSX } from 'solid-js';
import { splitProps } from 'solid-js';
import { cn } from '@/lib/utils';

// biome-ignore lint/nursery/useExplicitType: explicit CVA output type is verbose and harms inference
export const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg)] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default:
'bg-[var(--accent)] text-white shadow hover:bg-[var(--accent-hover)]',
destructive:
'bg-red-600 text-white shadow-sm hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800',
outline:
'border border-[var(--border)] bg-transparent shadow-sm hover:bg-[var(--accent-bg)] hover:text-[var(--text-h)]',
secondary:
'bg-[var(--border)] text-[var(--text-h)] shadow-sm hover:bg-[var(--text-muted)]/25',
ghost: 'hover:bg-[var(--accent-bg)] hover:text-[var(--text-h)]',
link: 'text-[var(--accent)] underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
link: 'h-auto p-0 text-[0.95rem]',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);

export type ButtonProps = ComponentProps<typeof KobalteButton> &
VariantProps<typeof buttonVariants>;

export const Button = (props: ButtonProps): JSX.Element => {
const [local, rest] = splitProps(props, ['class', 'variant', 'size']);
return (
<KobalteButton
class={cn(
buttonVariants({ variant: local.variant, size: local.size }),
local.class,
)}
{...rest}
/>
);
};
3 changes: 3 additions & 0 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
@import "tailwindcss";
@plugin "tailwindcss-animate";

:root {
/*
* Surfaces: warm flour / crust / wheat (bread).
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

/** Merge Tailwind classes; later wins on conflicts. */
export const cn = (...inputs: ClassValue[]): string => twMerge(clsx(inputs));
11 changes: 0 additions & 11 deletions apps/web/src/pages/Page.css
Original file line number Diff line number Diff line change
Expand Up @@ -162,17 +162,6 @@
margin: 0 0 1rem;
}

.recipe-detail-back-link {
font-size: 0.95rem;
color: var(--accent);
text-decoration: none;
}

.recipe-detail-back-link:hover {
color: var(--accent-hover);
text-decoration: underline;
}

.recipe-detail-header {
margin-bottom: 1rem;
}
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/pages/RecipePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createResource, Show } from 'solid-js';
import type { RecipeDetail } from '@/api';
import { getRecipeById } from '@/api';
import { RecipeDetailBody } from '@/components/recipe-detail/RecipeDetailBody';
import { buttonVariants } from '@/components/ui/button';
import './Page.css';

export const RecipePage = (): JSX.Element => {
Expand All @@ -25,7 +26,7 @@ export const RecipePage = (): JSX.Element => {
return (
<article class="page recipe-detail" aria-labelledby="recipe-title">
<p class="recipe-detail-back">
<A href="/" class="recipe-detail-back-link">
<A class={buttonVariants({ variant: 'link', size: 'link' })} href="/">
← All recipes
</A>
</p>
Expand Down
3 changes: 2 additions & 1 deletion apps/web/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// <reference types="vitest/config" />
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
import solid from 'vite-plugin-solid';

Expand All @@ -12,7 +13,7 @@ export default defineConfig({
'@': path.resolve(__dirname, 'src'),
},
},
plugins: [solid()],
plugins: [tailwindcss(), solid()],
test: {
environment: 'jsdom',
globals: true,
Expand Down
Loading
Loading