diff --git a/CLAUDE.md b/CLAUDE.md index cafd896..2d3111f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,7 +88,12 @@ When adding a feature: create `Features//Endpoints.cs` with a `Map(path, opts)` helper. Stores the JWT in `localStorage` under `token`, attaches `Authorization: Bearer …`, and on 401 it clears the token and `goto('/login')`. diff --git a/src/frontend/STYLE_GUIDE.md b/src/frontend/STYLE_GUIDE.md deleted file mode 100644 index 75e8fbd..0000000 --- a/src/frontend/STYLE_GUIDE.md +++ /dev/null @@ -1,71 +0,0 @@ -# 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 ` elements. Removes native OS appearance + * (which can add a grey gradient even when bg-white is set) and injects a + * consistent chevron so all dropdowns look identical to text inputs. + * Note: @apply cannot compose other component classes in Tailwind v4, so + * the base field styles are duplicated here. + */ + .select { + @apply appearance-none rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm focus:border-primary focus:outline-none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%239ca3af' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 8l4 4 4-4'/%3E%3C/svg%3E"); + background-position: right 0.4rem center; + background-repeat: no-repeat; + background-size: 1.1rem 1.1rem; + padding-right: 1.75rem; + } + + .select-lg { + @apply appearance-none rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-base focus:border-primary focus:outline-none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%239ca3af' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 8l4 4 4-4'/%3E%3C/svg%3E"); + background-position: right 0.4rem center; + background-repeat: no-repeat; + background-size: 1.1rem 1.1rem; + padding-right: 1.75rem; + } + + /* Standard action button — primary green. */ + .btn-primary { + @apply rounded-lg bg-primary px-4 py-2 font-semibold text-white disabled:opacity-50; + } + + /* Outlined button — secondary / cancel. Same height as btn-primary. */ + .btn-secondary { + @apply rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 disabled:opacity-50; + } + + /* Low-prominence destructive link — use at the bottom of detail pages. */ + .btn-danger-link { + @apply text-sm text-danger hover:underline; + } +} diff --git a/src/frontend/src/lib/QuantityInput.svelte b/src/frontend/src/lib/QuantityInput.svelte index 3f684fd..fd3ce6d 100644 --- a/src/frontend/src/lib/QuantityInput.svelte +++ b/src/frontend/src/lib/QuantityInput.svelte @@ -86,13 +86,13 @@ oninput={onQuantityInput} placeholder="Qty" aria-label={ariaLabel} - class="w-20 rounded-lg border border-gray-300 px-2 py-2 text-sm focus:border-primary focus:outline-none" + class="field w-20 px-2" /> {#each stores as store} @@ -103,7 +103,7 @@ diff --git a/src/frontend/src/routes/lists/[id]/+page.svelte b/src/frontend/src/routes/lists/[id]/+page.svelte index 74a4662..1088846 100644 --- a/src/frontend/src/routes/lists/[id]/+page.svelte +++ b/src/frontend/src/routes/lists/[id]/+page.svelte @@ -278,7 +278,7 @@ bind:value={newItemName} placeholder="Add an item..." ariaLabel="Item name" - inputClass="w-full rounded-lg border border-gray-300 px-3 py-2.5 text-base focus:border-primary focus:outline-none" + inputClass="field-lg w-full" onsubmit={addItem} onProductChange={onItemProductChange} /> @@ -288,7 +288,7 @@ type="text" bind:value={newItemQuantityNote} placeholder="e.g. to taste" - class="w-36 rounded-lg border border-gray-300 px-2 py-2 text-sm focus:border-primary focus:outline-none" + class="field w-36 px-2" /> {:else} 0} setItemSection(item.id, (e.currentTarget.value === '' ? null : Number(e.currentTarget.value)))} - 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" + class="select shrink-0 rounded-md border-gray-200 px-1 py-0.5 text-xs text-gray-500" aria-label="Section for {item.name}" > diff --git a/src/frontend/src/routes/recipes/[id]/+page.svelte b/src/frontend/src/routes/recipes/[id]/+page.svelte index 6de2d5e..0b7f065 100644 --- a/src/frontend/src/routes/recipes/[id]/+page.svelte +++ b/src/frontend/src/routes/recipes/[id]/+page.svelte @@ -97,13 +97,13 @@
@@ -160,7 +160,7 @@
diff --git a/src/frontend/src/routes/recipes/[id]/edit/+page.svelte b/src/frontend/src/routes/recipes/[id]/edit/+page.svelte index a75a8fe..bff1b96 100644 --- a/src/frontend/src/routes/recipes/[id]/edit/+page.svelte +++ b/src/frontend/src/routes/recipes/[id]/edit/+page.svelte @@ -209,14 +209,14 @@ bind:value={title} placeholder="Recipe title" required - class="w-full rounded-lg border border-gray-300 px-3 py-2.5 text-lg focus:border-primary focus:outline-none" + class="field-lg w-full" />
@@ -227,7 +227,7 @@ placeholder="Short description (optional)" bind:value={servings} min={1} placeholder="e.g. 4" - class="mt-1 block w-24 rounded-lg border border-gray-300 px-3 py-2 focus:border-primary focus:outline-none" + class="field mt-1 block w-24" />
@@ -242,7 +242,7 @@ placeholder="Short description (optional)" type="text" bind:value={ingredient.quantityNote} placeholder="e.g. to taste" - class="w-44 rounded-lg border border-gray-300 px-2 py-2 text-sm focus:border-primary focus:outline-none" + class="field w-44 px-2" /> {:else} onIngredientProductChange(idx, p)} />
@@ -293,7 +293,7 @@ placeholder="Short description (optional)" bind:value={instructions} placeholder="Instructions (optional)" rows={6} - class="w-full rounded-lg border border-gray-300 px-3 py-2.5 focus:border-primary focus:outline-none" + class="field-lg w-full" > {#if saveError} @@ -305,14 +305,14 @@ placeholder="Short description (optional)" type="button" onclick={() => goto(`/recipes/${recipeId}`)} disabled={saving} - class="flex-1 rounded-lg border border-gray-300 py-3 font-semibold text-gray-700 disabled:opacity-50" + class="btn-secondary flex-1 py-3" > Cancel diff --git a/src/frontend/src/routes/recipes/new/+page.svelte b/src/frontend/src/routes/recipes/new/+page.svelte index b43c9dd..fb02661 100644 --- a/src/frontend/src/routes/recipes/new/+page.svelte +++ b/src/frontend/src/routes/recipes/new/+page.svelte @@ -112,14 +112,14 @@ bind:value={title} placeholder="Recipe title" required - class="w-full rounded-lg border border-gray-300 px-3 py-2.5 text-lg focus:border-primary focus:outline-none" + class="field-lg w-full" />
@@ -130,7 +130,7 @@ bind:value={servings} min={1} placeholder="e.g. 4" - class="mt-1 block w-24 rounded-lg border border-gray-300 px-3 py-2 focus:border-primary focus:outline-none" + class="field mt-1 block w-24" />
@@ -145,7 +145,7 @@ type="text" bind:value={ingredient.quantityNote} placeholder="e.g. to taste" - class="w-44 rounded-lg border border-gray-300 px-2 py-2 text-sm focus:border-primary focus:outline-none" + class="field w-44 px-2" /> {:else} onIngredientProductChange(idx, p)} />
@@ -195,13 +195,13 @@ bind:value={instructions} placeholder="Instructions (optional)" rows={6} - class="w-full rounded-lg border border-gray-300 px-3 py-2.5 focus:border-primary focus:outline-none" + class="field-lg w-full" > diff --git a/src/frontend/src/routes/stores/+page.svelte b/src/frontend/src/routes/stores/+page.svelte index 9ef17a0..1fdc510 100644 --- a/src/frontend/src/routes/stores/+page.svelte +++ b/src/frontend/src/routes/stores/+page.svelte @@ -160,9 +160,9 @@ bind:value={newName} placeholder="New store name" required - class="flex-1 rounded-lg border border-gray-300 px-3 py-2.5 focus:border-primary focus:outline-none" + class="field flex-1 py-2.5" /> -