@ssejal8/recipe-ui-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +10 -0
- package/.npmr +1 -0
- package/README.md +354 -0
- package/apps/recipe-web/package.json +31 -0
- package/apps/recipe-web/src/app.css +418 -0
- package/apps/recipe-web/src/app.d.ts +32 -0
- package/apps/recipe-web/src/app.html +25 -0
- package/apps/recipe-web/src/lib/api/mealdb.ts +240 -0
- package/apps/recipe-web/src/lib/components/AppHeader.svelte +290 -0
- package/apps/recipe-web/src/lib/components/ConfirmDialog.svelte +46 -0
- package/apps/recipe-web/src/lib/components/Modal.svelte +33 -0
- package/apps/recipe-web/src/lib/components/RecipeCard.svelte +100 -0
- package/apps/recipe-web/src/lib/components/RecipeForm.svelte +560 -0
- package/apps/recipe-web/src/lib/components/RecipeGrid.svelte +77 -0
- package/apps/recipe-web/src/lib/components/RecipePickerDialog.svelte +185 -0
- package/apps/recipe-web/src/lib/components/Toaster.svelte +111 -0
- package/apps/recipe-web/src/lib/domain/recipe.ts +130 -0
- package/apps/recipe-web/src/lib/domain/validation.ts +189 -0
- package/apps/recipe-web/src/lib/stencil/WebComponent.svelte +145 -0
- package/apps/recipe-web/src/lib/stencil/register.ts +28 -0
- package/apps/recipe-web/src/lib/stores/favorites.svelte.ts +81 -0
- package/apps/recipe-web/src/lib/stores/myRecipes.svelte.ts +98 -0
- package/apps/recipe-web/src/lib/stores/persisted.svelte.ts +73 -0
- package/apps/recipe-web/src/lib/stores/planner.svelte.ts +275 -0
- package/apps/recipe-web/src/lib/stores/theme.svelte.ts +67 -0
- package/apps/recipe-web/src/lib/stores/toast.svelte.ts +66 -0
- package/apps/recipe-web/src/routes/+error.svelte +36 -0
- package/apps/recipe-web/src/routes/+layout.svelte +62 -0
- package/apps/recipe-web/src/routes/+page.svelte +352 -0
- package/apps/recipe-web/src/routes/+page.ts +107 -0
- package/apps/recipe-web/src/routes/favorites/+page.svelte +170 -0
- package/apps/recipe-web/src/routes/my-recipes/+layout.ts +55 -0
- package/apps/recipe-web/src/routes/my-recipes/+page.svelte +180 -0
- package/apps/recipe-web/src/routes/my-recipes/[id]/edit/+page.svelte +104 -0
- package/apps/recipe-web/src/routes/my-recipes/new/+page.svelte +67 -0
- package/apps/recipe-web/src/routes/planner/+page.svelte +422 -0
- package/apps/recipe-web/src/routes/recipes/[id]/+page.svelte +538 -0
- package/apps/recipe-web/src/routes/recipes/[id]/+page.ts +24 -0
- package/apps/recipe-web/static/favicon.svg +8 -0
- package/apps/recipe-web/static/robots.txt +2 -0
- package/apps/recipe-web/svelte.config.js +35 -0
- package/apps/recipe-web/tsconfig.json +14 -0
- package/apps/recipe-web/vite.config.ts +23 -0
- package/package.json +29 -0
- package/packages/recipe-kit-stencil/LICENSE +21 -0
- package/packages/recipe-kit-stencil/README.md +185 -0
- package/packages/recipe-kit-stencil/package-lock.json +4772 -0
- package/packages/recipe-kit-stencil/package.json +91 -0
- package/packages/recipe-kit-stencil/src/components/rk-badge/readme.md +38 -0
- package/packages/recipe-kit-stencil/src/components/rk-badge/rk-badge.css +102 -0
- package/packages/recipe-kit-stencil/src/components/rk-badge/rk-badge.tsx +31 -0
- package/packages/recipe-kit-stencil/src/components/rk-chip/readme.md +52 -0
- package/packages/recipe-kit-stencil/src/components/rk-chip/rk-chip.css +155 -0
- package/packages/recipe-kit-stencil/src/components/rk-chip/rk-chip.tsx +105 -0
- package/packages/recipe-kit-stencil/src/components/rk-empty-state/readme.md +44 -0
- package/packages/recipe-kit-stencil/src/components/rk-empty-state/rk-empty-state.css +64 -0
- package/packages/recipe-kit-stencil/src/components/rk-empty-state/rk-empty-state.tsx +50 -0
- package/packages/recipe-kit-stencil/src/components/rk-meal-slot/readme.md +58 -0
- package/packages/recipe-kit-stencil/src/components/rk-meal-slot/rk-meal-slot.css +225 -0
- package/packages/recipe-kit-stencil/src/components/rk-meal-slot/rk-meal-slot.tsx +182 -0
- package/packages/recipe-kit-stencil/src/components/rk-modal/readme.md +57 -0
- package/packages/recipe-kit-stencil/src/components/rk-modal/rk-modal.css +163 -0
- package/packages/recipe-kit-stencil/src/components/rk-modal/rk-modal.tsx +176 -0
- package/packages/recipe-kit-stencil/src/components/rk-rating/readme.md +42 -0
- package/packages/recipe-kit-stencil/src/components/rk-rating/rk-rating.css +71 -0
- package/packages/recipe-kit-stencil/src/components/rk-rating/rk-rating.tsx +114 -0
- package/packages/recipe-kit-stencil/src/components/rk-recipe-card/readme.md +66 -0
- package/packages/recipe-kit-stencil/src/components/rk-recipe-card/rk-recipe-card.css +280 -0
- package/packages/recipe-kit-stencil/src/components/rk-recipe-card/rk-recipe-card.tsx +179 -0
- package/packages/recipe-kit-stencil/src/components/rk-recipe-card/test/rk-recipe-card.spec.tsx +70 -0
- package/packages/recipe-kit-stencil/src/components/rk-search-bar/readme.md +67 -0
- package/packages/recipe-kit-stencil/src/components/rk-search-bar/rk-search-bar.css +141 -0
- package/packages/recipe-kit-stencil/src/components/rk-search-bar/rk-search-bar.tsx +152 -0
- package/packages/recipe-kit-stencil/src/components/rk-search-bar/test/rk-search-bar.spec.tsx +93 -0
- package/packages/recipe-kit-stencil/src/components/rk-segmented-control/readme.md +41 -0
- package/packages/recipe-kit-stencil/src/components/rk-segmented-control/rk-segmented-control.css +91 -0
- package/packages/recipe-kit-stencil/src/components/rk-segmented-control/rk-segmented-control.tsx +115 -0
- package/packages/recipe-kit-stencil/src/components/rk-tag-input/readme.md +47 -0
- package/packages/recipe-kit-stencil/src/components/rk-tag-input/rk-tag-input.css +146 -0
- package/packages/recipe-kit-stencil/src/components/rk-tag-input/rk-tag-input.tsx +181 -0
- package/packages/recipe-kit-stencil/src/components/rk-tag-input/test/rk-tag-input.spec.tsx +76 -0
- package/packages/recipe-kit-stencil/src/components.d.ts +1192 -0
- package/packages/recipe-kit-stencil/src/global/rk-global.css +104 -0
- package/packages/recipe-kit-stencil/src/index.html +259 -0
- package/packages/recipe-kit-stencil/src/index.ts +6 -0
- package/packages/recipe-kit-stencil/stencil.config.ts +41 -0
- package/packages/recipe-kit-stencil/tsconfig.json +24 -0
package/.npmr
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
registry=https://registry.npmjs.org/
|
package/README.md
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# Recipe Finder & Meal Planner
|
|
2
|
+
|
|
3
|
+
A recipe discovery and weekly meal-planning application built with **Svelte 5 + SvelteKit**, using a **StencilJS** web component library that is published to npm and consumed as a package dependency.
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| **Live application** | _fill in after deploying — see [Deployment](#deployment)_ |
|
|
8
|
+
| **npm package** | https://www.npmjs.com/package/recipe-kit-stencil |
|
|
9
|
+
| **GitHub repository** | _fill in after pushing — see [Repository](#repository)_ |
|
|
10
|
+
| **Recipe data** | [TheMealDB](https://www.themealdb.com/api.php) (public test key, no signup) |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Contents
|
|
15
|
+
|
|
16
|
+
- [What it does](#what-it-does)
|
|
17
|
+
- [Repository layout](#repository-layout)
|
|
18
|
+
- [Setup](#setup)
|
|
19
|
+
- [Starting the development server](#starting-the-development-server)
|
|
20
|
+
- [The Stencil component library](#the-stencil-component-library)
|
|
21
|
+
- [Publishing to npm](#publishing-to-npm)
|
|
22
|
+
- [How SvelteKit consumes the package](#how-sveltekit-consumes-the-package)
|
|
23
|
+
- [Architecture](#architecture)
|
|
24
|
+
- [Deployment](#deployment)
|
|
25
|
+
- [Repository](#repository)
|
|
26
|
+
- [Assumptions and decisions](#assumptions-and-decisions)
|
|
27
|
+
- [Known limitations](#known-limitations)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## What it does
|
|
32
|
+
|
|
33
|
+
**Recipe discovery** — Search the catalogue by name, browse by category chip, and narrow results by category, cuisine and main ingredient. Every filter lives in the URL, so a search is shareable and survives a reload. Results can be sorted, and are paged client-side.
|
|
34
|
+
|
|
35
|
+
**Recipe details** — A dedicated page per recipe with the full ingredient list (as a tickable prep checklist with progress), numbered method steps, cuisine metadata, and links to the original source and video where the API provides them.
|
|
36
|
+
|
|
37
|
+
**Recipe management** — Create, edit and delete your own recipes through a validated form. Your recipes are searchable alongside the catalogue, appear first in results, and are marked with a badge. Deletes are undoable.
|
|
38
|
+
|
|
39
|
+
**Favorites** — Favorite from any card or from the detail page, filter your saved list by text and category, and clear the list (also undoable).
|
|
40
|
+
|
|
41
|
+
**Weekly meal planner** — A 7-day × 3-meal grid for any week, with previous/next week navigation, "repeat last week", drag-and-drop between slots, a recipe picker that searches your favorites / your own recipes / the whole catalogue, and a per-week recipe summary.
|
|
42
|
+
|
|
43
|
+
Favorites, your recipes and meal plans persist in `localStorage` and stay in sync across duplicate browser tabs.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Repository layout
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
recipe-ui/ npm workspaces root
|
|
51
|
+
├── packages/
|
|
52
|
+
│ └── recipe-kit-stencil/ StencilJS component library → published to npm
|
|
53
|
+
│ ├── src/components/ 10 web components (.tsx + .css + specs)
|
|
54
|
+
│ ├── src/global/ design tokens (--rk-* custom properties)
|
|
55
|
+
│ ├── src/index.html component gallery / dev harness
|
|
56
|
+
│ └── stencil.config.ts
|
|
57
|
+
└── apps/
|
|
58
|
+
└── recipe-web/ SvelteKit 5 application
|
|
59
|
+
├── src/lib/api/ typed TheMealDB client
|
|
60
|
+
├── src/lib/domain/ recipe model + validation rules
|
|
61
|
+
├── src/lib/stencil/ the SvelteKit ↔ Stencil integration layer
|
|
62
|
+
├── src/lib/stores/ Svelte 5 rune stores, localStorage-backed
|
|
63
|
+
├── src/lib/components/ app-level Svelte components
|
|
64
|
+
└── src/routes/ pages
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Setup
|
|
70
|
+
|
|
71
|
+
**Requirements:** Node.js ≥ 20 and npm ≥ 10.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
git clone <your-repo-url> recipe-ui
|
|
75
|
+
cd recipe-ui
|
|
76
|
+
npm install
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A single `npm install` at the root installs both workspaces. The component library's `prepare` script builds it automatically, so the app has something to import straight away.
|
|
80
|
+
|
|
81
|
+
No environment variables and no API key are needed — TheMealDB's public test key is used.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Starting the development server
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm run dev
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Builds the component library, then starts the app on **http://localhost:5173**.
|
|
92
|
+
|
|
93
|
+
Other useful commands, all from the repository root:
|
|
94
|
+
|
|
95
|
+
| Command | What it does |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| `npm run dev` | Build the library, then run the app dev server |
|
|
98
|
+
| `npm run dev:app` | App dev server only (library already built) |
|
|
99
|
+
| `npm run dev:kit` | Library in watch mode + component gallery |
|
|
100
|
+
| `npm run build` | Production build of the library, then the app |
|
|
101
|
+
| `npm run check` | `svelte-check` type and template checking |
|
|
102
|
+
| `npm test` | Component library spec tests |
|
|
103
|
+
| `npm run publish:kit` | Publish the library to npm |
|
|
104
|
+
|
|
105
|
+
Working on a component and the app at the same time? Run `npm run dev:kit` and `npm run dev:app` in two terminals — Stencil rebuilds `dist/` on save and Vite picks it up.
|
|
106
|
+
|
|
107
|
+
To verify a real production server locally:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm run build:kit
|
|
111
|
+
npm run preview:node --workspace recipe-web # builds with adapter-node and serves it
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## The Stencil component library
|
|
117
|
+
|
|
118
|
+
`recipe-kit-stencil` ships 10 framework-agnostic custom elements. Full prop / event / slot tables are in [`packages/recipe-kit-stencil/README.md`](./packages/recipe-kit-stencil/README.md) and in the generated docs beside each component.
|
|
119
|
+
|
|
120
|
+
| Tag | Role in the app | Events consumed by SvelteKit |
|
|
121
|
+
| --- | --- | --- |
|
|
122
|
+
| `rk-recipe-card` | Every recipe tile, in all grids | `rkCardSelect`, `rkFavoriteToggle` |
|
|
123
|
+
| `rk-search-bar` | Discover, favorites, my recipes, picker | `rkSearch`, `rkClear` |
|
|
124
|
+
| `rk-chip` | Category rail, active-filter pills, favorites facets | `rkChipToggle`, `rkChipDismiss` |
|
|
125
|
+
| `rk-segmented-control` | Sort controls, picker source switch | `rkSegmentChange` |
|
|
126
|
+
| `rk-meal-slot` | All 21 cells of the planner grid | `rkMealAdd`, `rkMealSwap`, `rkMealRemove`, `rkMealOpen`, `rkMealDrop` |
|
|
127
|
+
| `rk-modal` | Picker, add-to-plan, all confirmations | `rkModalClose` |
|
|
128
|
+
| `rk-tag-input` | Tag field in the recipe form | `rkTagsChange` |
|
|
129
|
+
| `rk-badge` | Metadata badges throughout | — |
|
|
130
|
+
| `rk-rating` | Star rating display | `rkRatingChange` |
|
|
131
|
+
| `rk-empty-state` | Every empty and error state | — |
|
|
132
|
+
|
|
133
|
+
Run the gallery to see and exercise all of them, with a live event log:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npm run dev:kit # → http://localhost:3333
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Design principles
|
|
140
|
+
|
|
141
|
+
- **Primitive props over object props.** `rk-recipe-card` takes `name`, `image`, `category` … as separate primitives rather than one `recipe` object, so it works from plain HTML attributes in any framework.
|
|
142
|
+
- **Components are controlled.** They emit intent and never mutate their own state. `rkFavoriteToggle` reports the state to move *to*; the app persists it and passes `favorite` back down.
|
|
143
|
+
- **Shadow DOM everywhere.** Styling hooks are deliberate: `--rk-*` custom properties and `part="…"` attributes.
|
|
144
|
+
- **Complex props have a string fallback.** `options` and `values` are arrays, assigned as DOM properties on the normal path, but both also parse a JSON string attribute.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Publishing to npm
|
|
149
|
+
|
|
150
|
+
The library is configured to publish to the public registry regardless of any local registry setting, via `publishConfig` in its `package.json`.
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# 1. Log in to the public npm registry (once per machine)
|
|
154
|
+
npm login --registry=https://registry.npmjs.org/
|
|
155
|
+
|
|
156
|
+
# 2. Publish from the repository root
|
|
157
|
+
npm run publish:kit
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`prepublishOnly` runs a clean production build first, so a published tarball can never contain stale output. `files` is restricted to `dist/`, `loader/`, `README.md` and `LICENSE` — no source, no tests, no config.
|
|
161
|
+
|
|
162
|
+
### Versioning
|
|
163
|
+
|
|
164
|
+
[Semantic versioning](https://semver.org/), from the library directory:
|
|
165
|
+
|
|
166
|
+
| Change | Command |
|
|
167
|
+
| --- | --- |
|
|
168
|
+
| Bug fix, styling correction, no API change | `npm run release:patch` |
|
|
169
|
+
| New component, new optional prop, new event | `npm run release:minor` |
|
|
170
|
+
| Renamed/removed prop, event or tag; changed payload | `npm run release:major` |
|
|
171
|
+
|
|
172
|
+
Each script bumps the version, then publishes. After a bump, raise the range in `apps/recipe-web/package.json` to match.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## How SvelteKit consumes the package
|
|
177
|
+
|
|
178
|
+
The app depends on the library **by package name**, exactly as any external consumer would:
|
|
179
|
+
|
|
180
|
+
```jsonc
|
|
181
|
+
// apps/recipe-web/package.json
|
|
182
|
+
"dependencies": {
|
|
183
|
+
"recipe-kit-stencil": "^0.1.0"
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Nothing is imported from `packages/recipe-kit-stencil/src`. All imports resolve through the package's `exports` map to its built `dist/` and `loader/` output:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import 'recipe-kit-stencil/dist/recipe-kit/recipe-kit.css'; // design tokens
|
|
191
|
+
const { defineCustomElements } = await import('recipe-kit-stencil/loader');
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Because both directories are npm workspaces, `npm install` satisfies `^0.1.0` from the local package during development — one install, no `npm link`, no manual rebuild step. Outside this repository the identical range resolves from the registry.
|
|
195
|
+
|
|
196
|
+
To prove the registry path end-to-end after publishing:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
cd /tmp && mkdir kit-check && cd kit-check && npm init -y
|
|
200
|
+
npm install recipe-kit-stencil --registry=https://registry.npmjs.org/
|
|
201
|
+
ls node_modules/recipe-kit-stencil/loader
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Passing data down: properties, not attributes
|
|
205
|
+
|
|
206
|
+
Attributes can only carry strings, so array props such as `rk-segmented-control.options` must be assigned as DOM properties. `src/lib/stencil/WebComponent.svelte` is a small bridge that handles this for the whole app:
|
|
207
|
+
|
|
208
|
+
```svelte
|
|
209
|
+
<WebComponent
|
|
210
|
+
tag="rk-recipe-card"
|
|
211
|
+
props={{ recipeId: recipe.id, name: recipe.name, favorite: isFavorite }}
|
|
212
|
+
events={{ rkFavoriteToggle: handleToggle }}
|
|
213
|
+
>
|
|
214
|
+
<rk-badge tone="accent" small>30 min</rk-badge>
|
|
215
|
+
{#snippet actions()}
|
|
216
|
+
<button slot="actions" class="btn btn-sm">Add to plan</button>
|
|
217
|
+
{/snippet}
|
|
218
|
+
</WebComponent>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
It does three things that matter:
|
|
222
|
+
|
|
223
|
+
1. **Defers property writes until the element upgrades.** Assigning a property before `customElements.define` runs would shadow the generated accessor, so writes wait on `customElements.whenDefined(tag)`.
|
|
224
|
+
2. **Registers listeners with `addEventListener`.** The library emits `rkFavoriteToggle`, not `rkfavoritetoggle`; `addEventListener` preserves case exactly, keeping the app's contract identical to the library's documented one.
|
|
225
|
+
3. **Mirrors props as kebab-case attributes for SSR.** Property assignment cannot happen on the server, which would leave every card an empty shell until hydration. Stencil observes attributes, so elements render with correct content in the server response — before hydration, and with JavaScript disabled.
|
|
226
|
+
|
|
227
|
+
### Handling events coming back up
|
|
228
|
+
|
|
229
|
+
Custom events are handled as ordinary app logic. The store is treated as the source of truth rather than the event payload, so a change made in another tab cannot be clobbered:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
function handleFavoriteToggle(event: CustomEvent<{ favorite: boolean }>) {
|
|
233
|
+
const nowFavorite = favorites.toggle(recipe);
|
|
234
|
+
if (nowFavorite !== event.detail.favorite) return;
|
|
235
|
+
toasts.push(nowFavorite ? `Saved “${recipe.name}”.` : `Removed “${recipe.name}”.`);
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Slots
|
|
240
|
+
|
|
241
|
+
- **Default slot** — badges under a card title (`rk-recipe-card`), call-to-action buttons (`rk-empty-state`), dialog body (`rk-modal`).
|
|
242
|
+
- **Named slots** — `actions` on `rk-recipe-card` for per-card buttons, `actions` on `rk-search-bar` for the result counter, `footer` on `rk-modal` for every confirmation's button row.
|
|
243
|
+
|
|
244
|
+
Slotted content is passed with Svelte 5 snippets. Any element needing a `slot="…"` attribute must sit inside a `{#snippet}` block, because Svelte 5 treats `slot` on a direct child of a *component* as the deprecated slot syntax.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Architecture
|
|
249
|
+
|
|
250
|
+
### Data flow
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
TheMealDB ──► src/lib/api/mealdb.ts ──► +page.ts load ──► page component ──► rk-* elements
|
|
254
|
+
(typed, normalised) (SSR + CSR) │ │
|
|
255
|
+
│ props down │ events up
|
|
256
|
+
localStorage ──► rune stores ─────────────────────────────────┘◄───────────────────┘
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**One recipe model.** TheMealDB returns 20 flat `strIngredient1…20` / `strMeasure1…20` column pairs, inconsistent instruction formatting, and comma-joined tags. `src/lib/api/mealdb.ts` normalises all of it into a single `Recipe` type that user-authored recipes also satisfy, so no component branches on data origin — only on whether the user *owns* a recipe.
|
|
260
|
+
|
|
261
|
+
**Filters live in the URL.** Search state is read from `url.searchParams` inside `load`, which means SvelteKit re-runs the fetch on change and no fetch logic is duplicated in the component.
|
|
262
|
+
|
|
263
|
+
**Combined filters via set intersection.** The API only supports one filter dimension per request, so each active filter becomes one `filter.php` call and the resulting id sets are intersected. That is what makes "Seafood + Italian" work.
|
|
264
|
+
|
|
265
|
+
**Stores are classes with explicit persistence.** Stores are created at module scope, outside any component, where `$effect` has no owner — so each mutation calls `save()` rather than relying on an effect. A `storage` listener keeps duplicate tabs in sync.
|
|
266
|
+
|
|
267
|
+
**Referential integrity is owned by the store, not the pages.** Favorites and planner slots hold a `RecipeSummary` snapshot so those views render without a network round-trip per recipe. `myRecipes.update()` and `.remove()` therefore propagate renames and deletions into favorites and every planned slot, so no caller can forget to.
|
|
268
|
+
|
|
269
|
+
### Validation
|
|
270
|
+
|
|
271
|
+
`src/lib/domain/validation.ts` is pure and Svelte-free, so the same rules could run on a server later. It enforces: required name (3–120 chars), required category and cuisine, valid `http(s)` image URL if given, ≥ 2 ingredients with no case-insensitive duplicates, ≥ 1 instruction step, optional whole-number time and servings within range, ≤ 6 tags, and a notes length cap. Duplicate recipe titles are checked separately, in the page, because that rule depends on store contents rather than input shape.
|
|
272
|
+
|
|
273
|
+
Errors surface only after the first submit attempt, then live on every keystroke, with a count banner and focus moved to the first invalid field.
|
|
274
|
+
|
|
275
|
+
### Rendering
|
|
276
|
+
|
|
277
|
+
Discover, recipe details and the facet lists are server-rendered. Anything backed by `localStorage` — favorites, your recipes, the planner — is client-only by nature, and each page handles the pre-hydration window explicitly rather than flashing an incorrect empty state.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Deployment
|
|
282
|
+
|
|
283
|
+
`adapter-auto` is the default: on Vercel, Netlify or Cloudflare Pages it installs the right adapter with no configuration.
|
|
284
|
+
|
|
285
|
+
### Vercel or Netlify (recommended)
|
|
286
|
+
|
|
287
|
+
Import the repository, then set:
|
|
288
|
+
|
|
289
|
+
| Setting | Value |
|
|
290
|
+
| --- | --- |
|
|
291
|
+
| Root directory / base | `apps/recipe-web` |
|
|
292
|
+
| Install command | `npm install` |
|
|
293
|
+
| Build command | `npm run build` |
|
|
294
|
+
|
|
295
|
+
npm workspaces are detected from the repository root, so the install reaches both packages. Once the library is published, the app installs it from the registry and no library build step is needed at all.
|
|
296
|
+
|
|
297
|
+
### Self-hosted Node
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
npm run build:kit
|
|
301
|
+
ADAPTER=node npm run build --workspace recipe-web
|
|
302
|
+
node apps/recipe-web/build/index.js # PORT and HOST are respected
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Verified locally: the Node build boots and serves every route.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Repository
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
git remote add origin https://github.com/<your-username>/recipe-ui.git
|
|
313
|
+
git push -u origin main
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Then fill in the repository and live-application links in the table at the top of this file.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Assumptions and decisions
|
|
321
|
+
|
|
322
|
+
**TheMealDB was chosen over Spoonacular or Edamam** because it needs no API key or signup, which keeps the project clone-and-run. Consequences: it has no ratings, no cook times and no nutrition data, and no write endpoints.
|
|
323
|
+
|
|
324
|
+
**No backend, and user data is per-browser.** The assignment scopes the deliverable to the frontend, so favorites, user recipes and meal plans are stored in `localStorage`. They are therefore per-browser and per-device: they do not sync, and clearing site data clears them. A URL to a user-authored recipe only resolves in the browser that created it, which the detail page states explicitly rather than showing a bare 404.
|
|
325
|
+
|
|
326
|
+
**Cook time and servings are only shown when they are real.** Rather than estimating a time for API recipes from instruction length, those fields are simply omitted for catalogue recipes and shown only for user-authored ones. Cards show ingredient count instead, which is genuine data.
|
|
327
|
+
|
|
328
|
+
**The default Discover view is a small featured set.** `search.php` with an empty query returns 25 recipes, not the full catalogue. Rather than firing 14 parallel category requests on every page load, the default view is labelled "Featured recipes" and a category chip rail gives one-tap access to each full category list.
|
|
329
|
+
|
|
330
|
+
**The planner is week-addressable.** Plans are keyed by the Monday of their week, so past and future weeks are all navigable and "repeat last week" is possible, rather than there being a single perpetual week.
|
|
331
|
+
|
|
332
|
+
**Ratings are display-only.** `rk-rating` is fully interactive, but the app never persists a user rating: TheMealDB has no rating data to seed from, and a rating that exists only in one browser would be misleading. The component is in the library for completeness and is exercised in the gallery.
|
|
333
|
+
|
|
334
|
+
**The prep checklist is intentionally not persisted.** Ticking ingredients on the detail page is a cooking aid for the current session, not part of the recipe.
|
|
335
|
+
|
|
336
|
+
**`recipe-kit-stencil` is unscoped.** The name was verified available on npm. Publishing under a scope instead only needs the `name` field changed in the library's `package.json` and the dependency in the app's.
|
|
337
|
+
|
|
338
|
+
**Node ≥ 20** is assumed, matching the SvelteKit 2 and Vite 8 baseline.
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Known limitations
|
|
343
|
+
|
|
344
|
+
- **Deploy and publish are not done.** Both need credentials this environment does not have. The npm package is built, packed and verified (`npm pack` produces a valid tarball); the production Node build is verified booting and serving. The two links at the top of this file are the remaining steps.
|
|
345
|
+
- **Browser interaction was not verified end-to-end.** Every route was exercised over HTTP and asserted on its server-rendered output (card counts, filter intersection, planner grid, 404 handling), `svelte-check` reports zero errors, and 16 component spec tests pass. Click-through of the drag-and-drop planner and the create/edit form was not machine-verified.
|
|
346
|
+
- **Stencil's bundled test runner is deprecated** as of Stencil 4 and will be removed in v5. The spec tests will need migrating to `@stencil/vitest`.
|
|
347
|
+
- **Ingredient filtering of your own recipes is a substring match**, while the API's ingredient filter is an exact match on its own ingredient vocabulary. Results from the two sources can differ in strictness for the same query.
|
|
348
|
+
- **No pagination against the API.** TheMealDB returns whole result sets, so paging is client-side over the full list.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## License
|
|
353
|
+
|
|
354
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "recipe-web",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Recipe Finder & Meal Planner — SvelteKit 5 application.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite dev",
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"build:node": "ADAPTER=node vite build",
|
|
11
|
+
"preview": "vite preview",
|
|
12
|
+
"preview:node": "ADAPTER=node vite build && node build/index.js",
|
|
13
|
+
"start": "node build/index.js",
|
|
14
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
15
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
16
|
+
"prepare": "svelte-kit sync"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"recipe-kit-stencil": "^0.1.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@sveltejs/adapter-auto": "^7.0.1",
|
|
23
|
+
"@sveltejs/adapter-node": "^5.5.7",
|
|
24
|
+
"@sveltejs/kit": "^2.70.3",
|
|
25
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
26
|
+
"svelte": "^5.56.10",
|
|
27
|
+
"svelte-check": "^4.7.6",
|
|
28
|
+
"typescript": "^5.9.3",
|
|
29
|
+
"vite": "^8.2.2"
|
|
30
|
+
}
|
|
31
|
+
}
|