Fix form control consistency; add frontend style guide

- QuantityInput unit select: add bg-white (browser defaults to gray
  without it) and capitalize default option to "Unit"
- Shopping list section dropdowns: rename "Uncategorized" → "No section"
  for consistent title-case phrasing across all default options
- src/frontend/STYLE_GUIDE.md: documents form control classes, button
  variants, text casing rules, icon usage, color tokens, and spacing
  rhythm so all future UI work stays consistent
- CLAUDE.md: link to the style guide so it is always consulted

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Josh Rogers
2026-05-14 20:13:36 -05:00
parent 32ce4d1a6b
commit a398f8cf44
4 changed files with 77 additions and 4 deletions
+2
View File
@@ -88,6 +88,8 @@ When adding a feature: create `Features/<Name>/<Name>Endpoints.cs` with a `Map<N
### Frontend — SvelteKit with runes
**UI consistency:** all form controls, buttons, colors, icons, spacing, and text casing are governed by `src/frontend/STYLE_GUIDE.md`. Read it before adding or modifying any UI component — deviating without a documented reason is a bug.
- `svelte.config.js` forces `runes: true` for all non-`node_modules` files. Use Svelte 5 runes (`$state`, `$derived`, `$effect`); do not use legacy reactive `$:` syntax.
- `src/lib/api.ts``api<T>(path, opts)` helper. Stores the JWT in `localStorage` under `token`, attaches `Authorization: Bearer …`, and on 401 it clears the token and `goto('/login')`.
- `src/lib/auth.svelte.ts` — runes-based auth state.
+71
View File
@@ -0,0 +1,71 @@
# Frontend Style Guide
Rules that apply to every component. Deviating from these requires an explicit reason.
## Form controls
All interactive inputs, selects, and textareas share one baseline class set so they look identical regardless of type:
```
rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm
focus:border-primary focus:outline-none
```
| Variant | When to use | Size note |
|---|---|---|
| **Standard** (`px-3 py-2`) | Inline rows (qty, unit, section dropdowns, filters) | `text-sm` |
| **Large** (`px-3 py-2.5 text-base`) | Full-width, standalone inputs (item name, recipe title) | `text-base` |
**Rules:**
- `bg-white` is **always explicit** on `<select>` — browsers default to gray without it.
- Never mix `py-2` and `py-2.5` controls in the same flex row; they will be different heights.
- Fixed-width selects (unit dropdown: `w-24`, qty: `w-20`). Unconstrained selects get `max-w-36` so long option text doesn't push other controls off screen.
## Buttons
| Style | Class | Use |
|---|---|---|
| Primary | `rounded-lg bg-primary px-4 py-2 font-semibold text-white` | Main action (Add, Save Changes) |
| Secondary | `rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-700` | Peer to primary (Cancel, Edit) |
| Danger (inline) | `rounded-lg border border-danger px-4 py-2 text-sm text-danger` | Destructive, same visual weight as secondary |
| Danger (footer) | `text-sm text-danger hover:underline` | Low-prominence delete at the bottom of a detail page |
| Ghost | `text-sm text-gray-500` | Back links, minor navigation |
Primary and secondary buttons that appear in the same row must use the same `py-*` value.
## Text casing
| Context | Rule | Example |
|---|---|---|
| Button labels | Title Case | `Save Changes`, `Add`, `Cancel` |
| Placeholder text | Sentence case | `Add an item...`, `Recipe title` |
| Select default option | Title Case | `Unit`, `No section` |
| Section/group headings | UPPERCASE via `uppercase` CSS class | `UNCATEGORIZED` |
| Navigation labels | Title Case | `Lists`, `Recipes` |
| Error / hint text | Sentence case | `~ approximate (e.g. to taste)` |
Never use ALL CAPS in raw text — apply the `uppercase` Tailwind class to let CSS handle it.
## Icons
- Use Heroicons outline style, 16×16 (`h-4 w-4`), `stroke-width="1.75"`.
- Always include `aria-hidden="true"` on decorative SVGs; the wrapping button carries the `aria-label`.
- Trash icon for destructive remove actions. No other icon for that purpose.
## Colors
Defined as CSS custom properties in `app.css`. Never hard-code hex values in component classes.
| Token | Use |
|---|---|
| `primary` | Brand green — CTAs, active states, quantity highlights |
| `danger` | Destructive actions, error text |
| `gray-300` | Default border on all form controls |
| `gray-400` | Secondary / hint text |
| `gray-500` | Ghost button text, tertiary labels |
## Spacing rhythm
- Gap between inline form controls in a row: `gap-2`.
- Gap between stacked form sections: `space-y-2` or `space-y-4`.
- Card / list item padding: `px-3 py-3` (shopping list) or `px-3 py-2` (compact).
+2 -2
View File
@@ -92,9 +92,9 @@
value={composite}
onchange={onUnitChange}
aria-label="Unit"
class="w-24 rounded-lg border border-gray-300 px-1 py-2 text-sm focus:border-primary focus:outline-none"
class="w-24 rounded-lg border border-gray-300 bg-white px-1 py-2 text-sm focus:border-primary focus:outline-none"
>
<option value="">unit</option>
<option value="">Unit</option>
{#each visible as unit}
<option value="{unit.kind}:{unit.id}">{unit.abbreviation}</option>
{/each}
@@ -302,7 +302,7 @@
class="max-w-36 rounded-lg border border-gray-300 bg-white px-2 py-2 text-sm focus:border-primary focus:outline-none"
aria-label="Section"
>
<option value={null}>Uncategorized</option>
<option value={null}>No section</option>
{#each sections as section (section.id)}
<option value={section.id}>{section.name}</option>
{/each}
@@ -356,7 +356,7 @@
class="shrink-0 rounded border border-gray-200 bg-white px-1 py-0.5 text-xs text-gray-500 focus:border-primary focus:outline-none"
aria-label="Section for {item.name}"
>
<option value="">Uncategorized</option>
<option value="">No section</option>
{#each sections as section (section.id)}
<option value={section.id}>{section.name}</option>
{/each}