Replace ad-hoc Tailwind strings with design tokens
Define .field, .field-lg, .select, .select-lg, .btn-primary, .btn-secondary, and .btn-danger-link in app.css as @layer components. The .select tokens use appearance-none + an inline SVG chevron so all dropdowns look identical to text inputs (no OS-level gray gradient). Apply tokens across every in-app form: QuantityInput, shopping list add form and item rows, recipe new/edit pages, recipe detail, stores, and lists create form. Drop the STYLE_GUIDE.md doc in favour of the tokens themselves as the source of truth, and update CLAUDE.md to document the token names and usage rules. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -88,7 +88,12 @@ When adding a feature: create `Features/<Name>/<Name>Endpoints.cs` with a `Map<N
|
|||||||
|
|
||||||
### Frontend — SvelteKit with runes
|
### 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.
|
**UI tokens:** form controls, buttons, and select elements use named CSS component classes defined in `src/frontend/src/app.css`. Always use these instead of raw Tailwind strings:
|
||||||
|
- Inputs / textareas: `.field` (standard, `py-2 text-sm`) or `.field-lg` (prominent, `py-2.5 text-base`)
|
||||||
|
- Selects: `.select` or `.select-lg` — these add `appearance-none` + a consistent chevron so dropdowns look identical to text inputs
|
||||||
|
- Buttons: `.btn-primary`, `.btn-secondary`, `.btn-danger-link`
|
||||||
|
- Width, margin, and flex utilities are added alongside the token class (e.g. `class="field w-full"`)
|
||||||
|
- Never hardcode the border, background, padding, or focus ring on a new control — always start from a token
|
||||||
|
|
||||||
- `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.
|
- `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/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')`.
|
||||||
|
|||||||
@@ -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 `<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).
|
|
||||||
@@ -21,3 +21,57 @@ body {
|
|||||||
button {
|
button {
|
||||||
@apply cursor-pointer;
|
@apply cursor-pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Design tokens ── */
|
||||||
|
|
||||||
|
/* Use .field for standard single-line text inputs and textareas inside the app shell. */
|
||||||
|
@layer components {
|
||||||
|
.field {
|
||||||
|
@apply rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm focus:border-primary focus:outline-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Larger variant for prominent standalone inputs (recipe title, item name search bar). */
|
||||||
|
.field-lg {
|
||||||
|
@apply rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-base focus:border-primary focus:outline-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Use .select / .select-lg for <select> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -86,13 +86,13 @@
|
|||||||
oninput={onQuantityInput}
|
oninput={onQuantityInput}
|
||||||
placeholder="Qty"
|
placeholder="Qty"
|
||||||
aria-label={ariaLabel}
|
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"
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
value={composite}
|
value={composite}
|
||||||
onchange={onUnitChange}
|
onchange={onUnitChange}
|
||||||
aria-label="Unit"
|
aria-label="Unit"
|
||||||
class="w-24 rounded-lg border border-gray-300 bg-white px-1 py-2 text-sm focus:border-primary focus:outline-none"
|
class="select w-24 px-1"
|
||||||
>
|
>
|
||||||
<option value="">Unit</option>
|
<option value="">Unit</option>
|
||||||
{#each visible as unit}
|
{#each visible as unit}
|
||||||
|
|||||||
@@ -89,12 +89,12 @@
|
|||||||
bind:value={newName}
|
bind:value={newName}
|
||||||
placeholder="List name"
|
placeholder="List name"
|
||||||
required
|
required
|
||||||
class="mb-3 w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary focus:outline-none"
|
class="field mb-3 w-full"
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
bind:value={newStoreId}
|
bind:value={newStoreId}
|
||||||
required
|
required
|
||||||
class="mb-3 w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary focus:outline-none"
|
class="select mb-3 w-full"
|
||||||
>
|
>
|
||||||
<option value={null} disabled>Select store</option>
|
<option value={null} disabled>Select store</option>
|
||||||
{#each stores as store}
|
{#each stores as store}
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="w-full rounded-lg bg-primary py-2 font-semibold text-white"
|
class="btn-primary w-full"
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -278,7 +278,7 @@
|
|||||||
bind:value={newItemName}
|
bind:value={newItemName}
|
||||||
placeholder="Add an item..."
|
placeholder="Add an item..."
|
||||||
ariaLabel="Item name"
|
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}
|
onsubmit={addItem}
|
||||||
onProductChange={onItemProductChange}
|
onProductChange={onItemProductChange}
|
||||||
/>
|
/>
|
||||||
@@ -288,7 +288,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
bind:value={newItemQuantityNote}
|
bind:value={newItemQuantityNote}
|
||||||
placeholder="e.g. to taste"
|
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}
|
{:else}
|
||||||
<QuantityInput
|
<QuantityInput
|
||||||
@@ -299,7 +299,7 @@
|
|||||||
{#if sections.length > 0}
|
{#if sections.length > 0}
|
||||||
<select
|
<select
|
||||||
bind:value={newItemSectionId}
|
bind:value={newItemSectionId}
|
||||||
class="max-w-36 rounded-lg border border-gray-300 bg-white px-2 py-2 text-sm focus:border-primary focus:outline-none"
|
class="select max-w-36 px-2"
|
||||||
aria-label="Section"
|
aria-label="Section"
|
||||||
>
|
>
|
||||||
<option value={null}>No section</option>
|
<option value={null}>No section</option>
|
||||||
@@ -310,7 +310,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="rounded-lg bg-primary px-4 py-2 font-semibold text-white"
|
class="btn-primary"
|
||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
@@ -353,7 +353,7 @@
|
|||||||
<select
|
<select
|
||||||
value={item.sectionId ?? ''}
|
value={item.sectionId ?? ''}
|
||||||
onchange={(e) => setItemSection(item.id, (e.currentTarget.value === '' ? null : Number(e.currentTarget.value)))}
|
onchange={(e) => 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}"
|
aria-label="Section for {item.name}"
|
||||||
>
|
>
|
||||||
<option value="">No section</option>
|
<option value="">No section</option>
|
||||||
|
|||||||
@@ -97,13 +97,13 @@
|
|||||||
<div class="mb-4 flex gap-2">
|
<div class="mb-4 flex gap-2">
|
||||||
<button
|
<button
|
||||||
onclick={() => (showAddToList = !showAddToList)}
|
onclick={() => (showAddToList = !showAddToList)}
|
||||||
class="flex-1 rounded-lg bg-primary py-2.5 font-semibold text-white"
|
class="btn-primary flex-1 py-2.5"
|
||||||
>
|
>
|
||||||
Add to list
|
Add to list
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => goto(`/recipes/${recipeId}/edit`)}
|
onclick={() => goto(`/recipes/${recipeId}/edit`)}
|
||||||
class="rounded-lg border border-gray-300 px-4 py-2.5 text-sm text-gray-700"
|
class="btn-secondary py-2.5"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
<div class="mt-10 border-t border-gray-100 pt-4">
|
<div class="mt-10 border-t border-gray-100 pt-4">
|
||||||
<button
|
<button
|
||||||
onclick={deleteRecipe}
|
onclick={deleteRecipe}
|
||||||
class="text-sm text-danger hover:underline"
|
class="btn-danger-link"
|
||||||
>
|
>
|
||||||
Delete this recipe
|
Delete this recipe
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -209,14 +209,14 @@
|
|||||||
bind:value={title}
|
bind:value={title}
|
||||||
placeholder="Recipe title"
|
placeholder="Recipe title"
|
||||||
required
|
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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={description}
|
bind:value={description}
|
||||||
placeholder="Short description (optional)"
|
placeholder="Short description (optional)"
|
||||||
rows={2}
|
rows={2}
|
||||||
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"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -227,7 +227,7 @@ placeholder="Short description (optional)"
|
|||||||
bind:value={servings}
|
bind:value={servings}
|
||||||
min={1}
|
min={1}
|
||||||
placeholder="e.g. 4"
|
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"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -242,7 +242,7 @@ placeholder="Short description (optional)"
|
|||||||
type="text"
|
type="text"
|
||||||
bind:value={ingredient.quantityNote}
|
bind:value={ingredient.quantityNote}
|
||||||
placeholder="e.g. to taste"
|
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}
|
{:else}
|
||||||
<QuantityInput
|
<QuantityInput
|
||||||
@@ -256,7 +256,7 @@ placeholder="Short description (optional)"
|
|||||||
initialSelectedName={ingredient.initialName}
|
initialSelectedName={ingredient.initialName}
|
||||||
placeholder="Ingredient name"
|
placeholder="Ingredient name"
|
||||||
ariaLabel="Ingredient name"
|
ariaLabel="Ingredient name"
|
||||||
inputClass="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:outline-none"
|
inputClass="field w-full"
|
||||||
onProductChange={(p) => onIngredientProductChange(idx, p)}
|
onProductChange={(p) => onIngredientProductChange(idx, p)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -293,7 +293,7 @@ placeholder="Short description (optional)"
|
|||||||
bind:value={instructions}
|
bind:value={instructions}
|
||||||
placeholder="Instructions (optional)"
|
placeholder="Instructions (optional)"
|
||||||
rows={6}
|
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"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
{#if saveError}
|
{#if saveError}
|
||||||
@@ -305,14 +305,14 @@ placeholder="Short description (optional)"
|
|||||||
type="button"
|
type="button"
|
||||||
onclick={() => goto(`/recipes/${recipeId}`)}
|
onclick={() => goto(`/recipes/${recipeId}`)}
|
||||||
disabled={saving}
|
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
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
class="flex-[2] rounded-lg bg-primary py-3 font-semibold text-white disabled:opacity-50"
|
class="btn-primary flex-[2] py-3"
|
||||||
>
|
>
|
||||||
{saving ? 'Saving...' : 'Save Changes'}
|
{saving ? 'Saving...' : 'Save Changes'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -112,14 +112,14 @@
|
|||||||
bind:value={title}
|
bind:value={title}
|
||||||
placeholder="Recipe title"
|
placeholder="Recipe title"
|
||||||
required
|
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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={description}
|
bind:value={description}
|
||||||
placeholder="Short description (optional)"
|
placeholder="Short description (optional)"
|
||||||
rows={2}
|
rows={2}
|
||||||
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"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
bind:value={servings}
|
bind:value={servings}
|
||||||
min={1}
|
min={1}
|
||||||
placeholder="e.g. 4"
|
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"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
bind:value={ingredient.quantityNote}
|
bind:value={ingredient.quantityNote}
|
||||||
placeholder="e.g. to taste"
|
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}
|
{:else}
|
||||||
<QuantityInput
|
<QuantityInput
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
bind:value={ingredient.name}
|
bind:value={ingredient.name}
|
||||||
placeholder="Ingredient name"
|
placeholder="Ingredient name"
|
||||||
ariaLabel="Ingredient name"
|
ariaLabel="Ingredient name"
|
||||||
inputClass="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:outline-none"
|
inputClass="field w-full"
|
||||||
onProductChange={(p) => onIngredientProductChange(idx, p)}
|
onProductChange={(p) => onIngredientProductChange(idx, p)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,13 +195,13 @@
|
|||||||
bind:value={instructions}
|
bind:value={instructions}
|
||||||
placeholder="Instructions (optional)"
|
placeholder="Instructions (optional)"
|
||||||
rows={6}
|
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"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
class="w-full rounded-lg bg-primary py-3 font-semibold text-white disabled:opacity-50"
|
class="btn-primary w-full py-3"
|
||||||
>
|
>
|
||||||
{saving ? 'Saving...' : 'Save Recipe'}
|
{saving ? 'Saving...' : 'Save Recipe'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -160,9 +160,9 @@
|
|||||||
bind:value={newName}
|
bind:value={newName}
|
||||||
placeholder="New store name"
|
placeholder="New store name"
|
||||||
required
|
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"
|
||||||
/>
|
/>
|
||||||
<button type="submit" class="rounded-lg bg-primary px-4 py-2.5 font-semibold text-white">
|
<button type="submit" class="btn-primary py-2.5">
|
||||||
Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user