@gurpinderjitsingh/recipe-ui 0.11.0 → 0.13.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.
Files changed (34) hide show
  1. package/dist/cjs/app-modal_7.cjs.entry.js +151 -48
  2. package/dist/cjs/app-toast.cjs.entry.js +1 -1
  3. package/dist/collection/components/app-toast/app-toast.js +1 -1
  4. package/dist/collection/components/recipe-card/recipe-card.css +3 -3
  5. package/dist/collection/components/recipe-card/recipe-card.js +1 -1
  6. package/dist/collection/components/recipe-filter-bar/recipe-filter-bar.js +1 -1
  7. package/dist/collection/components/recipe-form/recipe-form.css +5 -1
  8. package/dist/collection/components/recipe-form/recipe-form.js +144 -41
  9. package/dist/collection/components/recipe-rating/recipe-rating.js +1 -1
  10. package/dist/collection/components/recipe-search-bar/recipe-search-bar.js +2 -2
  11. package/dist/components/app-toast.js +1 -1
  12. package/dist/components/index.js +1 -1
  13. package/dist/components/p-2Oa3Ks4e.js +1 -0
  14. package/dist/components/{p-DElrp5qn.js → p-BWjrK8cx.js} +1 -1
  15. package/dist/components/{p-BBMv9wjr.js → p-BjJGcNvA.js} +1 -1
  16. package/dist/components/p-DPXKwfGM.js +1 -0
  17. package/dist/components/{p-B_OSW9IJ.js → p-HEDJ6pyO.js} +1 -1
  18. package/dist/components/{p-BDov60lH.js → p-ltkIKdRy.js} +1 -1
  19. package/dist/components/recipe-card.js +1 -1
  20. package/dist/components/recipe-filter-bar.js +1 -1
  21. package/dist/components/recipe-form.js +1 -1
  22. package/dist/components/recipe-rating.js +1 -1
  23. package/dist/components/recipe-search-bar.js +1 -1
  24. package/dist/esm/app-modal_7.entry.js +151 -48
  25. package/dist/esm/app-toast.entry.js +1 -1
  26. package/dist/recipe-ui/p-7e9d3224.entry.js +1 -0
  27. package/dist/recipe-ui/p-dca78e59.entry.js +1 -0
  28. package/dist/recipe-ui/recipe-ui.esm.js +1 -1
  29. package/dist/types/components/recipe-form/recipe-form.d.ts +15 -3
  30. package/package.json +1 -1
  31. package/dist/components/p-8ogxleRP.js +0 -1
  32. package/dist/components/p-DsZoQlCx.js +0 -1
  33. package/dist/recipe-ui/p-32969b40.entry.js +0 -1
  34. package/dist/recipe-ui/p-9c0be643.entry.js +0 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  var index = require('./index-CRcwY0Fb.js');
4
4
 
5
- const recipeCardCss = () => `:host{display:grid;font-family:system-ui, sans-serif}.card{border:1px solid #e5e5e5;border-radius:12px;overflow:hidden;cursor:pointer;background:#fff;transition:box-shadow 0.15s ease, transform 0.15s ease}.card:hover{box-shadow:0 4px 16px rgba(0, 0, 0, 0.08);transform:translateY(-2px)}.image-wrap{position:relative;aspect-ratio:4 / 3;background:#f2f2f2}.image-wrap img{width:100%;height:100%;object-fit:cover;display:block}.placeholder{width:100%;height:100%;background:linear-gradient(135deg, #eee, #ddd)}.fav-btn{position:absolute;top:8px;right:8px;border:none;background:rgba(255, 255, 255, 0.9);border-radius:50%;width:32px;height:32px;font-size:16px;cursor:pointer;color:#f5a623}.fav-btn.active{color:rgb(186, 76, 37)}.body{padding:12px 14px}h3{margin:0 0 4px;font-size:15px;line-height:1.3}.types{margin:15px 0;display:flex;gap:9px}.category,.cuisine{font-size:12px;color:#777;text-transform:uppercase;letter-spacing:0.03em;border:1px solid;border-color:rgb(186, 76, 37);color:rgb(186, 76, 37);font-weight:500;border-radius:15px;padding:3px 8px}.actions{margin-top:8px}`;
5
+ const recipeCardCss = () => `:host{display:grid;font-family:system-ui, sans-serif}.card{border:1px solid #e5e5e5;border-radius:12px;overflow:hidden;cursor:pointer;background:#fff;transition:box-shadow 0.15s ease, transform 0.15s ease}.card:hover{box-shadow:0 4px 16px rgba(0, 0, 0, 0.08);transform:translateY(-2px)}.image-wrap{position:relative;aspect-ratio:4 / 3;background:#f2f2f2}.image-wrap img{width:100%;height:100%;object-fit:cover;display:block}.placeholder{width:100%;height:100%;background:linear-gradient(135deg, #eee, #ddd)}.fav-btn{position:absolute;top:8px;right:8px;border:none;background:rgba(255, 255, 255, 0.9);border-radius:50%;width:32px;height:32px;font-size:16px;cursor:pointer;color:#f5a623}.fav-btn.active{color:rgb(235, 98, 48)}.body{padding:12px 14px}h3{margin:0 0 4px;font-size:15px;line-height:1.3}.types{margin:15px 0;display:flex;gap:9px}.category,.cuisine{font-size:12px;color:#777;text-transform:uppercase;letter-spacing:0.03em;border:1px solid;border-color:rgb(235, 98, 48);color:rgb(235, 98, 48);font-weight:500;border-radius:15px;padding:3px 8px}.actions{margin-top:8px}`;
6
6
 
7
7
  const RecipeCard = class {
8
8
  constructor(hostRef) {
@@ -21,7 +21,7 @@ const RecipeCard = class {
21
21
  };
22
22
  }
23
23
  render() {
24
- return (index.h("div", { key: '64d7f6fb92a01c650f3972c4c0f0164f1904d959', class: "card", onClick: () => this.cardClick.emit() }, index.h("div", { key: 'b9d014e3fc77346eaa76e6feb15369e9fb622624', class: "image-wrap" }, this.image ? index.h("img", { src: this.image, alt: this.recipeTitle }) : index.h("div", { class: "placeholder" }), index.h("button", { key: '1690299a371310ad5e6b0e0cc238853fb3e9f439', class: { 'fav-btn': true, active: this.favorite }, onClick: this.onFavoriteClick, "aria-label": "Toggle favorite" }, this.favorite ? '★' : '☆')), index.h("div", { key: '52e02f3b16c2ab51209c6812aa94b4642504836a', class: "body" }, index.h("h3", { key: '6df2f0f3146dd8ef5f91cce8e29f0dca00ffbd0a' }, this.recipeTitle), index.h("span", { key: '072d88d380bf578e5a7a967c55d257aa604de23a', class: "types" }, this.category && index.h("span", { key: '636f5c1a350c93c6111415ac8a25b05301a60899', class: "category" }, this.category), this.cuisine && index.h("span", { key: '149cc29a60528ecd2734650b52857a00a685cfd6', class: "cuisine" }, this.cuisine)), index.h("div", { key: '2a75de4cbb0eb4a4e37e35a29c48131c3e88080c', class: "actions" }, index.h("slot", { key: '3c249490cef1005c546c6ee139bb70acabc41535', name: "actions" })))));
24
+ return (index.h("div", { key: '57023f5420716527cf5bb0a6f775bb62a19f70b6', class: "card", onClick: () => this.cardClick.emit() }, index.h("div", { key: '137fab4bde9ee40e26217280ba5736b51b96767f', class: "image-wrap" }, this.image ? index.h("img", { src: this.image, alt: this.recipeTitle }) : index.h("div", { class: "placeholder" }), index.h("button", { key: '99b992c4c50082ec42c4b9473b8418d11fe02e10', class: { 'fav-btn': true, active: this.favorite }, onClick: this.onFavoriteClick, "aria-label": "Toggle favorite" }, this.favorite ? '★' : '☆')), index.h("div", { key: '34a0670cb5e1716e6d7b17d4b1ad3aa5e19b614f', class: "body" }, index.h("h3", { key: 'c2f5ed25dc9435c10a0b476cebf10e2a7fcfcbd4' }, this.recipeTitle), index.h("span", { key: 'e4ee0142c7290110c4cb61bd9a3cae4f2503ca69', class: "types" }, this.category && index.h("span", { key: '0faf05e5d79a532dd5947f4f51fc44ca75f51844', class: "category" }, this.category), this.cuisine && index.h("span", { key: 'e8aee0b08bed65c359fc589412d89a14ed81f091', class: "cuisine" }, this.cuisine)), index.h("div", { key: '7c000476afc7687f4318abf6df26ba83bac131e2', class: "actions" }, index.h("slot", { key: 'e613619696cb0f3967426e1dbb245d23a8913d3d', name: "actions" })))));
25
25
  }
26
26
  };
27
27
  RecipeCard.style = recipeCardCss();
@@ -58,8 +58,8 @@ const RecipeSearchBar = class {
58
58
  this.internalValue = newVal;
59
59
  }
60
60
  render() {
61
- return (index.h("div", { key: 'd984cb3aeccf96cdedb62c741956b3d12615d884', class: "wrap" }, index.h("input", { key: 'c250c60908a8bb0c7d92da5eafe6ae6a61679067', type: "search", placeholder: this.placeholder, value: this.internalValue, onInput: this.onInput, onKeyDown: this.onKeyDown }), this.showSearchButton &&
62
- index.h("button", { key: '6e1dfe1e6b764d9166dbcdad615f177bcd7d8d8b', onClick: () => this.search.emit({ value: this.internalValue }), "aria-label": "Search" }, "\uD83D\uDD0D")));
61
+ return (index.h("div", { key: 'd4be8af300e73f7a6526ec48993db0259aafcbe2', class: "wrap" }, index.h("input", { key: '896908d0b0e10212eae53284d7a6fad79f34c5de', type: "search", placeholder: this.placeholder, value: this.internalValue, onInput: this.onInput, onKeyDown: this.onKeyDown }), this.showSearchButton &&
62
+ index.h("button", { key: '963394555e1febf733b50153e82858077021800c', onClick: () => this.search.emit({ value: this.internalValue }), "aria-label": "Search" }, "\uD83D\uDD0D")));
63
63
  }
64
64
  static get watchers() { return {
65
65
  "value": [{
@@ -83,7 +83,7 @@ const RecipeFilterBar = class {
83
83
  }
84
84
  render() {
85
85
  const cats = Array.isArray(this.categories) ? this.categories : [];
86
- return (index.h("div", { key: 'cc9c90176c682cf8b956a7b5e19096aac0891cb8', class: "wrap" }, index.h("button", { key: '7cc54529744166d2bcf5d45873aeaec0b3a37364', class: { chip: true, active: this.active === '' }, onClick: () => this.select('') }, "All"), cats.map((c) => (index.h("button", { class: { chip: true, active: this.active === c }, onClick: () => this.select(c) }, c)))));
86
+ return (index.h("div", { key: 'c19ffd90b0eff5c4b9b06602c9432b1210cc27f7', class: "wrap" }, index.h("button", { key: '3a629b0e211a6b9ab80b015da2d4be44c85f59a1', class: { chip: true, active: this.active === '' }, onClick: () => this.select('') }, "All"), cats.map((c) => (index.h("button", { class: { chip: true, active: this.active === c }, onClick: () => this.select(c) }, c)))));
87
87
  }
88
88
  };
89
89
  RecipeFilterBar.style = recipeFilterBarCss();
@@ -100,12 +100,12 @@ const RecipeRating = class {
100
100
  }
101
101
  render() {
102
102
  const stars = Array.from({ length: this.max }, (_, i) => i + 1);
103
- return (index.h("div", { key: '901887f72d31a470b1264d427806ea6a6184db12', class: "wrap", role: this.readonly ? undefined : 'radiogroup' }, stars.map((s) => (index.h("span", { class: { star: true, filled: s <= this.value, clickable: !this.readonly }, onClick: () => !this.readonly && this.ratingChange.emit({ value: s }) }, "\u2605")))));
103
+ return (index.h("div", { key: '47d3b89bf17f95c679f7931ee61d7eea4fc5c556', class: "wrap", role: this.readonly ? undefined : 'radiogroup' }, stars.map((s) => (index.h("span", { class: { star: true, filled: s <= this.value, clickable: !this.readonly }, onClick: () => !this.readonly && this.ratingChange.emit({ value: s }) }, "\u2605")))));
104
104
  }
105
105
  };
106
106
  RecipeRating.style = recipeRatingCss();
107
107
 
108
- const recipeFormCss = () => `:host{display:block;font-family:system-ui, sans-serif}form{display:flex;flex-direction:column;gap:14px}label{display:flex;flex-direction:column;gap:4px;font-size:13px;color:#333;font-weight:600}input,textarea,select{font-weight:400;padding:8px 10px;border:1px solid #ddd;border-radius:6px;font-size:14px;font-family:inherit;background:#fff;transition:border-color 0.15s ease, box-shadow 0.15s ease}input:focus,textarea:focus,select:focus{outline:none;border-color:rgb(186, 76, 37);box-shadow:0 0 0 3px rgba(255, 107, 53, 0.14)}select{appearance:none;-webkit-appearance:none;-moz-appearance:none;padding-right:34px;cursor:pointer;color:#1f2937;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;background-size:14px}select:hover{border-color:#bbb}select:invalid,select option[value=""]{color:#9ca3af}.row{display:grid;grid-template-columns:1fr 1fr;gap:14px}@media (max-width: 480px){.row{grid-template-columns:1fr}}.field{display:flex;flex-direction:column;gap:6px;font-size:13px;font-weight:600}.ingredient-row{display:flex;gap:6px}.ingredient-row input{flex:1}.ingredient-row button{border:none;background:#f4f4f4;border-radius:6px;cursor:pointer;width:30px}.add-btn{align-self:flex-start;border:1px dashed #bbb;background:none;border-radius:6px;padding:6px 10px;cursor:pointer;font-size:13px;color:#555}.error{color:#e74c3c;font-size:12px;font-weight:400}.footer{display:flex;justify-content:flex-end;gap:10px;margin-top:6px}.cancel-btn{border:1px solid #ddd;background:#fff;border-radius:8px;padding:8px 16px;cursor:pointer}.save-btn{border:none;background:#ff6b35;color:#fff;border-radius:8px;padding:8px 18px;cursor:pointer;font-weight:600}`;
108
+ const recipeFormCss = () => `:host{display:block;font-family:system-ui, sans-serif}form{display:flex;flex-direction:column;gap:14px}label{display:flex;flex-direction:column;gap:4px;font-size:13px;color:#333;font-weight:600}input,textarea,select{font-weight:400;padding:8px 10px;border:1px solid #ddd;border-radius:6px;font-size:14px;font-family:inherit;background:#fff;transition:border-color 0.15s ease, box-shadow 0.15s ease}input:focus,textarea:focus,select:focus{outline:none;border-color:rgb(235, 98, 48);box-shadow:0 0 0 3px rgba(255, 107, 53, 0.14)}select{appearance:none;-webkit-appearance:none;-moz-appearance:none;padding-right:34px;cursor:pointer;color:#1f2937;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;background-size:14px}select:hover{border-color:#bbb}select:invalid,select option[value=""]{color:#9ca3af}.row{display:grid;grid-template-columns:1fr 1fr;gap:14px}@media (max-width: 480px){.row{grid-template-columns:1fr}}.field{display:flex;flex-direction:column;gap:6px;font-size:13px;font-weight:600}.ingredient-row{display:flex;gap:6px}.ingredient-row input{flex:1}.ingredient-row button{border:none;background:#f4f4f4;border-radius:6px;cursor:pointer;width:30px}.add-btn{align-self:flex-start;border:1px dashed #bbb;background:none;border-radius:6px;padding:6px 10px;cursor:pointer;font-size:13px;color:#555}.error{color:#e74c3c;font-size:12px;font-weight:400}.footer{display:flex;justify-content:flex-end;gap:10px;margin-top:6px}.cancel-btn{border:1px solid #ddd;background:#fff;border-radius:8px;padding:8px 16px;cursor:pointer}.save-btn{border:none;background:#ff6b35;color:#fff;border-radius:8px;padding:8px 18px;cursor:pointer;font-weight:600}.required{color:red}`;
109
109
 
110
110
  const DEFAULT_CATEGORIES = [
111
111
  'Beef', 'Breakfast', 'Chicken', 'Dessert', 'Goat', 'Lamb',
@@ -119,6 +119,14 @@ const DEFAULT_AREAS = [
119
119
  'Portuguese', 'Russian', 'Spanish', 'Thai', 'Tunisian', 'Turkish',
120
120
  'Vietnamese', 'Other',
121
121
  ];
122
+ const LIMITS = {
123
+ title: { min: 5, max: 100 },
124
+ instructions: { min: 20, max: 5000 },
125
+ ingredient: { min: 5, max: 200 },
126
+ customCategory: { min: 5, max: 40 },
127
+ customArea: { min: 5, max: 40 },
128
+ image: { max: 500 },
129
+ };
122
130
  const RecipeForm = class {
123
131
  constructor(hostRef) {
124
132
  index.registerInstance(this, hostRef);
@@ -158,10 +166,9 @@ const RecipeForm = class {
158
166
  this.ingredients = [...this.ingredients, ''];
159
167
  };
160
168
  this.removeIngredient = (index) => {
161
- this.ingredients = this.ingredients.filter((_, i) => i !== index);
162
169
  const next = this.ingredients.filter((_, i) => i !== index);
163
170
  this.ingredients = next;
164
- this.clearErrorIf('ingredients', next.some((i) => i.trim()));
171
+ this.setFieldError('ingredients', this.validateIngredients(next));
165
172
  };
166
173
  }
167
174
  componentWillLoad() {
@@ -186,10 +193,83 @@ const RecipeForm = class {
186
193
  get fullAreaList() {
187
194
  return this.mergeList(this.areas, this.extraAreas);
188
195
  }
189
- /** Removes a field's error as soon as it becomes valid, without waiting
190
- * for the next full submit — called from each field's input handler. */
191
- clearErrorIf(key, isNowValid) {
192
- if (isNowValid && this.errors[key]) {
196
+ // ---- Per-field validators. Each returns an error message, or null when
197
+ // the value is valid. Used both by validate() on submit AND live on every
198
+ // keystroke, so the two can never drift out of sync with each other. ----
199
+ validateTitle(value) {
200
+ const len = value.trim().length;
201
+ if (len === 0)
202
+ return 'Title is required.';
203
+ if (len < LIMITS.title.min)
204
+ return `Title must be at least ${LIMITS.title.min} characters.`;
205
+ if (len > LIMITS.title.max)
206
+ return `Title must be ${LIMITS.title.max} characters or fewer.`;
207
+ return null;
208
+ }
209
+ validateImage(value) {
210
+ if (value.trim().length > LIMITS.image.max) {
211
+ return `Image URL must be ${LIMITS.image.max} characters or fewer.`;
212
+ }
213
+ return null;
214
+ }
215
+ validateIngredients(list) {
216
+ const clean = list.map((i) => i.trim()).filter(Boolean);
217
+ if (clean.length === 0)
218
+ return 'Add at least one ingredient.';
219
+ if (clean.some((i) => i.length < LIMITS.ingredient.min)) {
220
+ return `Each ingredient must be at least ${LIMITS.ingredient.min} characters.`;
221
+ }
222
+ if (clean.some((i) => i.length > LIMITS.ingredient.max)) {
223
+ return `Each ingredient must be ${LIMITS.ingredient.max} characters or fewer.`;
224
+ }
225
+ return null;
226
+ }
227
+ validateInstructions(value) {
228
+ const len = value.trim().length;
229
+ if (len === 0)
230
+ return 'Instructions are required.';
231
+ if (len < LIMITS.instructions.min) {
232
+ return `Instructions must be at least ${LIMITS.instructions.min} characters — add a bit more detail.`;
233
+ }
234
+ if (len > LIMITS.instructions.max)
235
+ return `Instructions must be ${LIMITS.instructions.max} characters or fewer.`;
236
+ return null;
237
+ }
238
+ validateCategory(value) {
239
+ return value.trim() ? null : 'Category is required.';
240
+ }
241
+ validateCustomCategory(value) {
242
+ const len = value.trim().length;
243
+ if (len === 0)
244
+ return 'Enter a category name.';
245
+ if (len < LIMITS.customCategory.min || len > LIMITS.customCategory.max) {
246
+ return `Category name must be ${LIMITS.customCategory.min}-${LIMITS.customCategory.max} characters.`;
247
+ }
248
+ return null;
249
+ }
250
+ validateArea(value) {
251
+ return value.trim() ? null : 'Cuisine / Area is required.';
252
+ }
253
+ validateCustomArea(value) {
254
+ const len = value.trim().length;
255
+ if (len === 0)
256
+ return 'Enter a cuisine name.';
257
+ if (len < LIMITS.customArea.min || len > LIMITS.customArea.max) {
258
+ return `Cuisine name must be ${LIMITS.customArea.min}-${LIMITS.customArea.max} characters.`;
259
+ }
260
+ return null;
261
+ }
262
+ /** Sets or clears a single field's error live — the core of "show error
263
+ * if any, else clear" behavior. Only touches `errors` when the message
264
+ * actually changed, to avoid redundant re-renders on every keystroke. */
265
+ setFieldError(key, message) {
266
+ const current = this.errors[key] ?? null;
267
+ if (current === message)
268
+ return;
269
+ if (message) {
270
+ this.errors = { ...this.errors, [key]: message };
271
+ }
272
+ else {
193
273
  const next = { ...this.errors };
194
274
  delete next[key];
195
275
  this.errors = next;
@@ -231,28 +311,45 @@ const RecipeForm = class {
231
311
  this.area = data.cuisine ?? '';
232
312
  this.customArea = '';
233
313
  }
314
+ // Reset any stale errors when the form is (re)populated, e.g. when
315
+ // switching from "new recipe" to editing an existing one.
316
+ this.errors = {};
234
317
  }
235
318
  }
319
+ /** Full validation pass, run on submit. Reuses the same per-field
320
+ * validators as live typing, so submit can never disagree with what
321
+ * the user already saw on screen. */
236
322
  validate() {
237
323
  const errors = {};
238
- if (!this.title.trim())
239
- errors.title = 'Title is required.';
240
- const cleanIngredients = this.ingredients.map((i) => i.trim()).filter(Boolean);
241
- if (cleanIngredients.length === 0)
242
- errors.ingredients = 'Add at least one ingredient.';
243
- if (!this.instructions.trim())
244
- errors.instructions = 'Instructions are required.';
245
- if (!this.category.trim()) {
246
- errors.category = 'Category is required.';
324
+ const titleErr = this.validateTitle(this.title);
325
+ if (titleErr)
326
+ errors.title = titleErr;
327
+ const imageErr = this.validateImage(this.image);
328
+ if (imageErr)
329
+ errors.image = imageErr;
330
+ const ingredientsErr = this.validateIngredients(this.ingredients);
331
+ if (ingredientsErr)
332
+ errors.ingredients = ingredientsErr;
333
+ const instructionsErr = this.validateInstructions(this.instructions);
334
+ if (instructionsErr)
335
+ errors.instructions = instructionsErr;
336
+ const categoryErr = this.validateCategory(this.category);
337
+ if (categoryErr) {
338
+ errors.category = categoryErr;
247
339
  }
248
- else if (this.category === 'Other' && !this.customCategory.trim()) {
249
- errors.customCategory = 'Enter a category name.';
340
+ else if (this.category === 'Other') {
341
+ const customCategoryErr = this.validateCustomCategory(this.customCategory);
342
+ if (customCategoryErr)
343
+ errors.customCategory = customCategoryErr;
250
344
  }
251
- if (!this.area.trim()) {
252
- errors.area = 'Cuisine / Area is required.';
345
+ const areaErr = this.validateArea(this.area);
346
+ if (areaErr) {
347
+ errors.area = areaErr;
253
348
  }
254
- else if (this.area === 'Other' && !this.customArea.trim()) {
255
- errors.customArea = 'Enter a cuisine name.';
349
+ else if (this.area === 'Other') {
350
+ const customAreaErr = this.validateCustomArea(this.customArea);
351
+ if (customAreaErr)
352
+ errors.customArea = customAreaErr;
256
353
  }
257
354
  this.errors = errors;
258
355
  return Object.keys(errors).length === 0;
@@ -261,33 +358,39 @@ const RecipeForm = class {
261
358
  const next = [...this.ingredients];
262
359
  next[index] = value;
263
360
  this.ingredients = next;
264
- this.clearErrorIf('ingredients', next.some((i) => i.trim()));
361
+ this.setFieldError('ingredients', this.validateIngredients(next));
265
362
  }
266
363
  render() {
267
- return (index.h("form", { key: 'c29ab62af9db4a95e11b75f25a2dd4c23e699dad', onSubmit: this.onSubmit }, index.h("slot", { key: 'ece02ab46a1075eaef912e8953e70faa11bc8d8d', name: "header" }), index.h("label", { key: '9e17e9ba311350d9235f5ec01c32743dfbf55b0e' }, "Title", index.h("input", { key: '2b8d632bc22eb5cde52198287ea2b19d87567dba', type: "text", value: this.title, onInput: (e) => {
268
- (this.title = e.target.value);
269
- this.clearErrorIf('title', !!this.title.trim());
270
- } }), this.errors.title && index.h("span", { key: '3a34d1f5e1f43ef190983dd0883f16eb6775f07c', class: "error" }, this.errors.title)), index.h("label", { key: '6acda6ccaff2e0315cc1964ae30c18647fe97bd1' }, "Image URL", index.h("input", { key: '6bdc06a2bebd6b5ae984865aefe292426fbe38cb', type: "text", value: this.image, onInput: (e) => (this.image = e.target.value) })), index.h("div", { key: 'f6ef49aaace211659296e93b4b5b47b29db73ac6', class: "row" }, index.h("label", { key: '9610c81a85ebddc2da4bdadcdee68ddea78e28d4' }, "Category", index.h("select", { key: '46542ff9e2c0f15d60be6744e063f3c860475772', onInput: (e) => {
271
- (this.category = e.target.value);
272
- this.clearErrorIf('category', !!this.category);
364
+ return (index.h("form", { key: 'f562e5fa4cdd1f49b6b119471bf7773e319423e8', onSubmit: this.onSubmit }, index.h("slot", { key: '0bcfd3c288244952e69b1206449d6046ff9f832b', name: "header" }), index.h("label", { key: 'b94ca99a985de9ab0a4a827854c6e9f3c2c32228' }, index.h("span", { key: 'ea3f3a53edb24ec2a8421c2b97fee17877fa507a' }, "Title ", index.h("span", { key: 'ef3093980bc013d65787ae4a0e04c5a86564433e', class: "required" }, "*")), index.h("input", { key: 'f9748efc0027f3783d2961d11524c59ca58121fe', type: "text", maxLength: LIMITS.title.max, value: this.title, onInput: (e) => {
365
+ this.title = e.target.value;
366
+ this.setFieldError('title', this.validateTitle(this.title));
367
+ } }), index.h("span", { key: '0ac83523c019253560b219137390b8fdd3626323', class: "char-count" }, this.title.trim().length, "/", LIMITS.title.max), this.errors.title && index.h("span", { key: '2f1a55d2aa593182c3fe6330b3248985331aca2e', class: "error" }, this.errors.title)), index.h("label", { key: 'aa7d01fd11b9b4ea0551e8a157a70a666b645720' }, "Image URL", index.h("input", { key: 'fd26aabc19167c469419a1714358615f62b125b9', type: "text", maxLength: LIMITS.image.max, value: this.image, onInput: (e) => {
368
+ this.image = e.target.value;
369
+ this.setFieldError('image', this.validateImage(this.image));
370
+ } }), this.errors.image && index.h("span", { key: '3f9561c7ee8433cee875c638edcb069baef96b86', class: "error" }, this.errors.image)), index.h("div", { key: '4253c71219c77f40ef8ec811700fa2ffb4e23d55', class: "row" }, index.h("label", { key: '7b892f3e2e87d7ab647050859996f7fcccbec42c' }, index.h("span", { key: '3f63f3f57f9df8673b54c42251282918fee1be69' }, "Category ", index.h("span", { key: 'd535dbd85bc96e1af88511f903d96046f23cb283', class: "required" }, "*")), index.h("select", { key: '311099318b5b7e6212eae0c9803adcf2437aa024', onInput: (e) => {
371
+ this.category = e.target.value;
372
+ this.setFieldError('category', this.validateCategory(this.category));
273
373
  if (this.category !== 'Other')
274
- this.clearErrorIf('customCategory', true);
275
- } }, index.h("option", { key: '2741b90edd32cb12903e877cbfa2db3b5ff04071', value: "", selected: this.category === '' }, "Select a category\u2026"), this.fullCategoryList.map((c) => (index.h("option", { value: c, selected: c === this.category }, c)))), this.errors.category && index.h("span", { key: 'dc8e7ab5a6decdece1b4559af856997251335abf', class: "error" }, this.errors.category)), index.h("label", { key: '792cb1c4fd6052f9d88c52649c6eb4dc064f0071' }, "Cuisine / Area", index.h("select", { key: '06a917ac951a4c6d3e8390534690545a1967581a', onInput: (e) => {
276
- (this.area = e.target.value);
277
- this.clearErrorIf('area', !!this.area);
374
+ this.setFieldError('customCategory', null);
375
+ else
376
+ this.setFieldError('customCategory', this.validateCustomCategory(this.customCategory));
377
+ } }, index.h("option", { key: '9ab9ed037d1cabc6829cf35bf8c77d5ae8ff6518', value: "", selected: this.category === '' }, "Select a category\u2026"), this.fullCategoryList.map((c) => (index.h("option", { value: c, selected: c === this.category }, c)))), this.errors.category && index.h("span", { key: '48a10209ff1b6425220388ba0f315899947bb37a', class: "error" }, this.errors.category)), index.h("label", { key: '294923e0daebc5aa126e66c6e8319ca087544dc4' }, index.h("span", { key: 'e9fc761f634c20cfc02829fcfeb4767dfaf4c69f' }, "Cuisine / Area ", index.h("span", { key: '52254653a745ef5459e7efd55d688e37324d358e', class: "required" }, "*")), index.h("select", { key: '7938a0a5b0bb6004b29578aee87cf5711e1d935d', onInput: (e) => {
378
+ this.area = e.target.value;
379
+ this.setFieldError('area', this.validateArea(this.area));
278
380
  if (this.area !== 'Other')
279
- this.clearErrorIf('customArea', true);
280
- } }, index.h("option", { key: '264c39814065b3f07626975b96eca6b7e1c9bb83', value: "", selected: this.area === '' }, "Select a cuisine\u2026"), this.fullAreaList.map((a) => (index.h("option", { value: a, selected: a === this.area }, a)))), this.errors.area && index.h("span", { key: 'f1a562402420fb7a813dd4184b439c03c71d93d9', class: "error" }, this.errors.area))), (this.category === 'Other' || this.area === 'Other') && (index.h("div", { key: '3c20e6b5b678027d711c7976c60e3be92d02fae3', class: "row" }, this.category === 'Other' && (index.h("label", { key: '4cd14baeff186c7db8fabe57b21b241567528437', class: "custom-area-field" }, "Enter category name", index.h("input", { key: 'b1880e7aaadb3ade3c980c7f44d6b0a8d4fc3923', type: "text", placeholder: "e.g. Brunch", value: this.customCategory, onInput: (e) => {
281
- (this.customCategory = e.target.value);
381
+ this.setFieldError('customArea', null);
382
+ else
383
+ this.setFieldError('customArea', this.validateCustomArea(this.customArea));
384
+ } }, index.h("option", { key: '2e3a43ac518d6d6662cda3c269055cac2d142cd0', value: "", selected: this.area === '' }, "Select a cuisine\u2026"), this.fullAreaList.map((a) => (index.h("option", { value: a, selected: a === this.area }, a)))), this.errors.area && index.h("span", { key: '9776cc4829e490a06c34ba6e6d8482fbda1499af', class: "error" }, this.errors.area))), (this.category === 'Other' || this.area === 'Other') && (index.h("div", { key: 'edf7709c6bb1fdf2d11d360dd4bff73067ace685', class: "row" }, this.category === 'Other' && (index.h("label", { key: 'e1b54a1298d4d9b7f2db16be20095c229fd7d72b', class: "custom-area-field" }, "Enter category name", index.h("input", { key: 'e538a1f8da6e55875d7b37538a205bfe09c7e48a', type: "text", placeholder: "e.g. Brunch", maxLength: LIMITS.customCategory.max, value: this.customCategory, onInput: (e) => {
282
385
  this.customCategory = e.target.value;
283
- this.clearErrorIf('customCategory', !!this.customCategory.trim());
284
- } }), this.errors.customCategory && index.h("span", { key: 'abd5094a23a182458ff2b76a58b55d65a9540c83', class: "error" }, this.errors.customCategory))), this.area === 'Other' && (index.h("label", { key: '696c0cc6af2cf16df8559da441e38635d8e0012c', class: "custom-area-field" }, "Enter cuisine name", index.h("input", { key: '663fee3941527c736d953cc993b886746dabab23', type: "text", placeholder: "e.g. Ethiopian", value: this.customArea, onInput: (e) => {
386
+ this.setFieldError('customCategory', this.validateCustomCategory(this.customCategory));
387
+ } }), this.errors.customCategory && index.h("span", { key: 'd76519fb39dbc17512c63f8923587219c6f126d1', class: "error" }, this.errors.customCategory))), this.area === 'Other' && (index.h("label", { key: '0b46c004deda4af6a71a503f950f990d340850f8', class: "custom-area-field" }, "Enter cuisine name", index.h("input", { key: '3f312b2aaa6b0345ac476a0aa7919ce5638f0205', type: "text", placeholder: "e.g. Ethiopian", maxLength: LIMITS.customArea.max, value: this.customArea, onInput: (e) => {
285
388
  this.customArea = e.target.value;
286
- this.clearErrorIf('customArea', !!this.customArea.trim());
287
- } }), this.errors.customArea && index.h("span", { key: '6e1b7ebfac7166fb1120b0d8f914bb076f5e40ee', class: "error" }, this.errors.customArea))))), index.h("div", { key: 'fe85b73291a5ee955d069f20ddf33835d09b95bd', class: "field" }, index.h("span", { key: 'fd010d7b12306bd4306caf03ebd972feb1e0ebb9' }, "Ingredients"), this.ingredients.map((ing, i) => (index.h("div", { class: "ingredient-row" }, index.h("input", { type: "text", value: ing, onInput: (e) => this.updateIngredient(i, e.target.value) }), index.h("button", { type: "button", onClick: () => this.removeIngredient(i), "aria-label": "Remove" }, "\u2715")))), index.h("button", { key: '8168f7e7f4bef8be1e74d1adf377b5471701c0ff', type: "button", class: "add-btn", onClick: this.addIngredient }, "+ Add ingredient"), this.errors.ingredients && index.h("span", { key: '3e35a8fe02833b27a333fa157548b88b3f5e3842', class: "error" }, this.errors.ingredients)), index.h("label", { key: 'c5c583cba5363ce35e6d41f95917bc49e3cca02d' }, "Instructions", index.h("textarea", { key: '514c219989a01cf1ad77739bd6c0df066362c689', rows: 5, onInput: (e) => {
389
+ this.setFieldError('customArea', this.validateCustomArea(this.customArea));
390
+ } }), this.errors.customArea && index.h("span", { key: 'a7d0487409a2f2338fd32ce23770da1fff318189', class: "error" }, this.errors.customArea))))), index.h("div", { key: '2af4f405001d202a86c0e94426db89b46d4af0c5', class: "field" }, index.h("span", { key: '1cc25f9cacb59981f1c8b6faf5c7669693454d86' }, "Ingredients ", index.h("span", { key: '12463cc84c7df68329044f6e00bbf489dc02efbc', class: "required" }, "*")), this.ingredients.map((ing, i) => (index.h("div", { class: "ingredient-row" }, index.h("input", { type: "text", maxLength: LIMITS.ingredient.max, value: ing, onInput: (e) => this.updateIngredient(i, e.target.value) }), index.h("button", { type: "button", onClick: () => this.removeIngredient(i), "aria-label": "Remove" }, "\u2715")))), index.h("button", { key: 'faadffd1444fa729327bb44e2ba430a38fb8d0f2', type: "button", class: "add-btn", onClick: this.addIngredient }, "+ Add ingredient"), this.errors.ingredients && index.h("span", { key: '93e3101aa6f8aa6adb28a5cb121cc2f735bdf3cb', class: "error" }, this.errors.ingredients)), index.h("label", { key: '142d68b899f6264366351ff0edefac0755f5c021' }, index.h("span", { key: 'd0469328a9f808854e2bc1daeb2df3a2562300e1' }, "Instructions ", index.h("span", { key: 'df0b49c132848b889adc1a070a17d90505f51f9b', class: "required" }, "*"), " "), index.h("textarea", { key: '790e5cb9c24947648df27697c7148636e2862bd5', rows: 5, maxLength: LIMITS.instructions.max, onInput: (e) => {
288
391
  this.instructions = e.target.value;
289
- this.clearErrorIf('instructions', !!this.instructions.trim());
290
- } }, this.instructions), this.errors.instructions && index.h("span", { key: '89881e3b0d4f864f7ccc835d86b094f367b54b2e', class: "error" }, this.errors.instructions)), index.h("div", { key: 'b9a812287af44062e1c68034881ba62321b0e660', class: "footer" }, index.h("slot", { key: 'a8307179d95cfd713d8f5fbe6e996fefb1d54907', name: "footer" }), index.h("button", { key: 'aa75decaf67c74a183af182864a37a6cd37b56ff', type: "button", class: "cancel-btn", onClick: () => this.cancel.emit() }, "Cancel"), index.h("button", { key: '13ea35b114f1a7721347f986c6c4e64968583025', type: "submit", class: "save-btn" }, "Save Recipe"))));
392
+ this.setFieldError('instructions', this.validateInstructions(this.instructions));
393
+ } }, this.instructions), index.h("span", { key: 'ae15de2b6a210955334f16fcc0040a739bf62f31', class: "char-count" }, this.instructions.trim().length, "/", LIMITS.instructions.max), this.errors.instructions && index.h("span", { key: 'db148c0b5347d6a899adb4daf18a59ea19221e7f', class: "error" }, this.errors.instructions)), index.h("div", { key: 'b42405e2778e862115d5841843a4e63bf5ad5e7a', class: "footer" }, index.h("slot", { key: 'c4a8533636d1dd5fac2b391e36314569f05a4d01', name: "footer" }), index.h("button", { key: '4ea910ae3983fcdb133dbdc38ad122b607eb3d5c', type: "button", class: "cancel-btn", onClick: () => this.cancel.emit() }, "Cancel"), index.h("button", { key: 'a2ea39907bba8f4c35b8f2eaaa501f31bdf17130', type: "submit", class: "save-btn" }, "Save Recipe"))));
291
394
  }
292
395
  static get watchers() { return {
293
396
  "initialData": [{
@@ -18,7 +18,7 @@ const AppToast = class {
18
18
  }
19
19
  }
20
20
  render() {
21
- return (index.h("div", { key: '65346b009a5016ad30b1ab85dc9c1d9bbd9b52f1', class: `toast ${this.type}` }, index.h("span", { key: '4bb1144fa221047ab51ab1b40647b07cc792d358' }, this.message), index.h("button", { key: '7dcd351feccbce93b6959cd04fb0372d42099c50', onClick: () => this.dismiss.emit(), "aria-label": "Dismiss" }, "\u2715")));
21
+ return (index.h("div", { key: '6d17f8582228f7ec27219c9a9a39d5bd76a780dd', class: `toast ${this.type}` }, index.h("span", { key: 'c0524ae830e6c3bdd73f1e0561caaf5a4ae73468' }, this.message), index.h("button", { key: '7aab76915df455b48d2ca6a5d9e0985f7bb87907', onClick: () => this.dismiss.emit(), "aria-label": "Dismiss" }, "\u2715")));
22
22
  }
23
23
  };
24
24
  AppToast.style = appToastCss();
@@ -12,7 +12,7 @@ export class AppToast {
12
12
  }
13
13
  }
14
14
  render() {
15
- return (h("div", { key: '65346b009a5016ad30b1ab85dc9c1d9bbd9b52f1', class: `toast ${this.type}` }, h("span", { key: '4bb1144fa221047ab51ab1b40647b07cc792d358' }, this.message), h("button", { key: '7dcd351feccbce93b6959cd04fb0372d42099c50', onClick: () => this.dismiss.emit(), "aria-label": "Dismiss" }, "\u2715")));
15
+ return (h("div", { key: '6d17f8582228f7ec27219c9a9a39d5bd76a780dd', class: `toast ${this.type}` }, h("span", { key: 'c0524ae830e6c3bdd73f1e0561caaf5a4ae73468' }, this.message), h("button", { key: '7aab76915df455b48d2ca6a5d9e0985f7bb87907', onClick: () => this.dismiss.emit(), "aria-label": "Dismiss" }, "\u2715")));
16
16
  }
17
17
  static get is() { return "app-toast"; }
18
18
  static get encapsulation() { return "shadow"; }
@@ -51,7 +51,7 @@
51
51
  }
52
52
 
53
53
  .fav-btn.active {
54
- color: rgb(186, 76, 37);
54
+ color:rgb(235, 98, 48);
55
55
  }
56
56
 
57
57
  .body {
@@ -77,8 +77,8 @@ h3 {
77
77
  text-transform: uppercase;
78
78
  letter-spacing: 0.03em;
79
79
  border: 1px solid;
80
- border-color: rgb(186, 76, 37);
81
- color: rgb(186, 76, 37);
80
+ border-color: rgb(235, 98, 48);
81
+ color: rgb(235, 98, 48);
82
82
  font-weight: 500;
83
83
  border-radius: 15px;
84
84
  padding: 3px 8px;
@@ -19,7 +19,7 @@ export class RecipeCard {
19
19
  };
20
20
  }
21
21
  render() {
22
- return (h("div", { key: '64d7f6fb92a01c650f3972c4c0f0164f1904d959', class: "card", onClick: () => this.cardClick.emit() }, h("div", { key: 'b9d014e3fc77346eaa76e6feb15369e9fb622624', class: "image-wrap" }, this.image ? h("img", { src: this.image, alt: this.recipeTitle }) : h("div", { class: "placeholder" }), h("button", { key: '1690299a371310ad5e6b0e0cc238853fb3e9f439', class: { 'fav-btn': true, active: this.favorite }, onClick: this.onFavoriteClick, "aria-label": "Toggle favorite" }, this.favorite ? '★' : '☆')), h("div", { key: '52e02f3b16c2ab51209c6812aa94b4642504836a', class: "body" }, h("h3", { key: '6df2f0f3146dd8ef5f91cce8e29f0dca00ffbd0a' }, this.recipeTitle), h("span", { key: '072d88d380bf578e5a7a967c55d257aa604de23a', class: "types" }, this.category && h("span", { key: '636f5c1a350c93c6111415ac8a25b05301a60899', class: "category" }, this.category), this.cuisine && h("span", { key: '149cc29a60528ecd2734650b52857a00a685cfd6', class: "cuisine" }, this.cuisine)), h("div", { key: '2a75de4cbb0eb4a4e37e35a29c48131c3e88080c', class: "actions" }, h("slot", { key: '3c249490cef1005c546c6ee139bb70acabc41535', name: "actions" })))));
22
+ return (h("div", { key: '57023f5420716527cf5bb0a6f775bb62a19f70b6', class: "card", onClick: () => this.cardClick.emit() }, h("div", { key: '137fab4bde9ee40e26217280ba5736b51b96767f', class: "image-wrap" }, this.image ? h("img", { src: this.image, alt: this.recipeTitle }) : h("div", { class: "placeholder" }), h("button", { key: '99b992c4c50082ec42c4b9473b8418d11fe02e10', class: { 'fav-btn': true, active: this.favorite }, onClick: this.onFavoriteClick, "aria-label": "Toggle favorite" }, this.favorite ? '★' : '☆')), h("div", { key: '34a0670cb5e1716e6d7b17d4b1ad3aa5e19b614f', class: "body" }, h("h3", { key: 'c2f5ed25dc9435c10a0b476cebf10e2a7fcfcbd4' }, this.recipeTitle), h("span", { key: 'e4ee0142c7290110c4cb61bd9a3cae4f2503ca69', class: "types" }, this.category && h("span", { key: '0faf05e5d79a532dd5947f4f51fc44ca75f51844', class: "category" }, this.category), this.cuisine && h("span", { key: 'e8aee0b08bed65c359fc589412d89a14ed81f091', class: "cuisine" }, this.cuisine)), h("div", { key: '7c000476afc7687f4318abf6df26ba83bac131e2', class: "actions" }, h("slot", { key: 'e613619696cb0f3967426e1dbb245d23a8913d3d', name: "actions" })))));
23
23
  }
24
24
  static get is() { return "recipe-card"; }
25
25
  static get encapsulation() { return "shadow"; }
@@ -15,7 +15,7 @@ export class RecipeFilterBar {
15
15
  }
16
16
  render() {
17
17
  const cats = Array.isArray(this.categories) ? this.categories : [];
18
- return (h("div", { key: 'cc9c90176c682cf8b956a7b5e19096aac0891cb8', class: "wrap" }, h("button", { key: '7cc54529744166d2bcf5d45873aeaec0b3a37364', class: { chip: true, active: this.active === '' }, onClick: () => this.select('') }, "All"), cats.map((c) => (h("button", { class: { chip: true, active: this.active === c }, onClick: () => this.select(c) }, c)))));
18
+ return (h("div", { key: 'c19ffd90b0eff5c4b9b06602c9432b1210cc27f7', class: "wrap" }, h("button", { key: '3a629b0e211a6b9ab80b015da2d4be44c85f59a1', class: { chip: true, active: this.active === '' }, onClick: () => this.select('') }, "All"), cats.map((c) => (h("button", { class: { chip: true, active: this.active === c }, onClick: () => this.select(c) }, c)))));
19
19
  }
20
20
  static get is() { return "recipe-filter-bar"; }
21
21
  static get encapsulation() { return "shadow"; }
@@ -35,7 +35,7 @@ input:focus,
35
35
  textarea:focus,
36
36
  select:focus {
37
37
  outline: none;
38
- border-color: rgb(186, 76, 37);
38
+ border-color: rgb(235, 98, 48);
39
39
  box-shadow: 0 0 0 3px rgba(255, 107, 53, 0.14);
40
40
  }
41
41
 
@@ -138,4 +138,8 @@ select option[value=""] {
138
138
  padding: 8px 18px;
139
139
  cursor: pointer;
140
140
  font-weight: 600;
141
+ }
142
+
143
+ .required {
144
+ color: red;
141
145
  }