From a891d079846a52b093b024b4fd2114a1c30f291a Mon Sep 17 00:00:00 2001 From: Boxel Submission Bot Date: Wed, 8 Apr 2026 12:19:20 +0800 Subject: [PATCH] add Recipe Card changes [boxel-content-hash:2d60b1ba1b6f] --- .../9087e4dc-6b0e-4536-8bba-9cb2eeb7d6c8.json | 64 + RecipeCard/chocolate-chip-cookies.json | 97 ++ .../df451c49-fa03-406e-a7f5-0d0bd1d1f9b8.json | 36 +- recipe-card.gts | 1167 +++++++++++++++++ 4 files changed, 1346 insertions(+), 18 deletions(-) create mode 100644 CardListing/9087e4dc-6b0e-4536-8bba-9cb2eeb7d6c8.json create mode 100644 RecipeCard/chocolate-chip-cookies.json create mode 100644 recipe-card.gts diff --git a/CardListing/9087e4dc-6b0e-4536-8bba-9cb2eeb7d6c8.json b/CardListing/9087e4dc-6b0e-4536-8bba-9cb2eeb7d6c8.json new file mode 100644 index 0000000..080f5d5 --- /dev/null +++ b/CardListing/9087e4dc-6b0e-4536-8bba-9cb2eeb7d6c8.json @@ -0,0 +1,64 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "CardListing", + "module": "https://realms-staging.stack.cards/catalog/catalog-app/listing/listing" + } + }, + "type": "card", + "attributes": { + "name": "Recipe Card", + "images": [], + "summary": "The RecipeCard module defines a comprehensive data structure for representing cooking recipes, including fields for recipe name, description, preparation and cooking times, servings, difficulty level, cuisine type, ingredients, instructions, tips, and tags. It offers multiple presentation formats such as embedded, fitted, isolated, badge, strip, tile, and full card views, each with specific styles and layout adaptations for various display sizes. The card integrates visual icons and dynamic computations for total time and difficulty color coding, facilitating rich, adaptable recipe displays in user interfaces.", + "cardInfo": { + "name": null, + "notes": null, + "summary": null, + "cardThumbnailURL": null + } + }, + "relationships": { + "tags": { + "links": { + "self": null + } + }, + "specs.0": { + "links": { + "self": "../Spec/df451c49-fa03-406e-a7f5-0d0bd1d1f9b8" + } + }, + "skills": { + "links": { + "self": null + } + }, + "license": { + "links": { + "self": null + } + }, + "publisher": { + "links": { + "self": null + } + }, + "categories": { + "links": { + "self": null + } + }, + "examples.0": { + "links": { + "self": "../RecipeCard/chocolate-chip-cookies" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} \ No newline at end of file diff --git a/RecipeCard/chocolate-chip-cookies.json b/RecipeCard/chocolate-chip-cookies.json new file mode 100644 index 0000000..9bbebd0 --- /dev/null +++ b/RecipeCard/chocolate-chip-cookies.json @@ -0,0 +1,97 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "RecipeCard", + "module": "../recipe-card" + } + }, + "type": "card", + "attributes": { + "tags": [ + "dessert", + "baking", + "cookies", + "classic" + ], + "tips": "For extra chewy cookies, chill the dough for at least 30 minutes before baking. Underbaking slightly is the key to a gooey centre — they firm up as they cool.", + "cuisine": "American", + "cardInfo": { + "name": "Classic Chocolate Chip Cookies", + "notes": null, + "summary": "Crispy on the edges, chewy in the middle — a timeless family favourite.", + "cardThumbnailURL": null + }, + "cookTime": 12, + "prepTime": 15, + "servings": 24, + "difficulty": "Easy", + "recipeName": "Classic Chocolate Chip Cookies", + "description": "Crispy on the edges, chewy in the middle — a timeless family favourite.", + "ingredients": [ + { + "name": "all-purpose flour", + "amount": "2¼", + "unit": "cups", + "notes": null + }, + { + "name": "baking soda", + "amount": "1", + "unit": "tsp", + "notes": null + }, + { + "name": "salt", + "amount": "1", + "unit": "tsp", + "notes": null + }, + { + "name": "unsalted butter", + "amount": "1", + "unit": "cup", + "notes": "softened" + }, + { + "name": "granulated sugar", + "amount": "¾", + "unit": "cup", + "notes": null + }, + { + "name": "brown sugar", + "amount": "¾", + "unit": "cup", + "notes": "packed" + }, + { + "name": "eggs", + "amount": "2", + "unit": "", + "notes": "large" + }, + { + "name": "vanilla extract", + "amount": "2", + "unit": "tsp", + "notes": null + }, + { + "name": "chocolate chips", + "amount": "2", + "unit": "cups", + "notes": null + } + ], + "instructions": "## Steps\n\n1. **Preheat** the oven to 375°F (190°C). Line baking sheets with parchment paper.\n\n2. **Whisk** flour, baking soda and salt in a bowl. Set aside.\n\n3. **Beat** butter and both sugars together in a large bowl until light and fluffy, about 3 minutes.\n\n4. **Add** eggs one at a time, beating well after each. Mix in vanilla.\n\n5. **Gradually stir** in the flour mixture until just combined — don't overmix.\n\n6. **Fold in** the chocolate chips.\n\n7. **Drop** rounded tablespoons of dough onto the prepared baking sheets, spacing 2 inches apart.\n\n8. **Bake** for 10–12 minutes until the edges are golden but centres look slightly underdone.\n\n9. **Cool** on the baking sheet for 5 minutes before transferring to a wire rack." + }, + "relationships": { + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} \ No newline at end of file diff --git a/Spec/df451c49-fa03-406e-a7f5-0d0bd1d1f9b8.json b/Spec/df451c49-fa03-406e-a7f5-0d0bd1d1f9b8.json index 1b3fbce..56e05cc 100644 --- a/Spec/df451c49-fa03-406e-a7f5-0d0bd1d1f9b8.json +++ b/Spec/df451c49-fa03-406e-a7f5-0d0bd1d1f9b8.json @@ -1,40 +1,40 @@ { "data": { + "meta": { + "adoptsFrom": { + "name": "Spec", + "module": "https://cardstack.com/base/spec" + } + }, "type": "card", "attributes": { - "readMe": null, "ref": { - "module": "../recipe-card", - "name": "RecipeCard" + "name": "RecipeCard", + "module": "../recipe-card" }, - "specType": "card", - "containedExamples": [], - "cardTitle": "Recipe", - "cardDescription": null, + "readMe": "Sure, here's the README documentation for the `RecipeCard` spec:\n\n## Summary\nThe `RecipeCard` spec defines a card for displaying recipe information. It includes fields for the recipe name, description, preparation time, cooking time, servings, difficulty, cuisine, ingredients, instructions, tips, and tags.\n\n## Import\n```javascript\nimport { RecipeCard } from 'https://realms-staging.stack.cards/richard.tan/25-mar-2026/recipe-card';\n```\n\n## Usage as a Field\nTo use the `RecipeCard` as a field within a consuming card or field, you can define it like this:\n\n```javascript\n@field recipeCard = linksTo(RecipeCard);\n```\n\nThen, in your template, you can display the recipe card using the field's template:\n\n```hbs\n<@fields.recipeCard @format=\"embedded\" />\n```\n\n## Template Usage\nYou can also invoke the `RecipeCard` spec directly within a consuming card or field's template. Here's an example:\n\n```hbs\n\n```\n\nIn this example, `recipeData` is the model data for the recipe card.", "cardInfo": { "name": null, + "notes": null, "summary": null, - "cardThumbnailURL": null, - "notes": null - } + "cardThumbnailURL": null + }, + "specType": "card", + "cardTitle": "Recipe", + "cardDescription": null, + "containedExamples": [] }, "relationships": { - "linkedExamples": { + "cardInfo.theme": { "links": { "self": null } }, - "cardInfo.theme": { + "linkedExamples": { "links": { "self": null } } - }, - "meta": { - "adoptsFrom": { - "module": "https://cardstack.com/base/spec", - "name": "Spec" - } } } } \ No newline at end of file diff --git a/recipe-card.gts b/recipe-card.gts new file mode 100644 index 0000000..4fb3aad --- /dev/null +++ b/recipe-card.gts @@ -0,0 +1,1167 @@ +import { and } from '@cardstack/boxel-ui/helpers'; +// ═══ [EDIT TRACKING: ON] Mark all changes with ⁿ ═══ +import { + CardDef, + FieldDef, + field, + contains, + containsMany, + Component, +} from 'https://cardstack.com/base/card-api'; // ¹ +import StringField from 'https://cardstack.com/base/string'; // ² +import NumberField from 'https://cardstack.com/base/number'; // ³ +import TextAreaField from 'https://cardstack.com/base/text-area'; // ⁴ +import MarkdownField from 'https://cardstack.com/base/markdown'; // ⁵ +import enumField from 'https://cardstack.com/base/enum'; // ⁶ +import UtensilsIcon from '@cardstack/boxel-icons/utensils'; // ⁷ +import ClockIcon from '@cardstack/boxel-icons/clock'; // ⁸ +import UsersIcon from '@cardstack/boxel-icons/users'; // ⁹ +import ChefHatIcon from '@cardstack/boxel-icons/chef-hat'; // ¹⁰ + +// ¹¹ Difficulty enum +const DifficultyField = enumField(StringField, { + options: ['Easy', 'Medium', 'Hard', 'Expert'], +}); + +// ¹² Ingredient field definition +export class IngredientField extends FieldDef { + // ¹³ + static displayName = 'Ingredient'; + + @field name = contains(StringField); // ¹⁴ + @field amount = contains(StringField); // ¹⁵ + @field unit = contains(StringField); // ¹⁶ + @field notes = contains(StringField); // ¹⁷ + + static embedded = class Embedded extends Component { + // ¹⁸ + + }; + + static atom = class Atom extends Component { + // ¹⁹ + + }; +} + +// ²⁰ Main recipe card +export class RecipeCard extends CardDef { + // ²¹ + static displayName = 'Recipe'; + static icon = UtensilsIcon; // ²² + static prefersWideFormat = true; // ²³ + + @field recipeName = contains(StringField); // ²⁴ + @field description = contains(TextAreaField); // ²⁵ + @field prepTime = contains(NumberField); // ²⁶ in minutes + @field cookTime = contains(NumberField); // ²⁷ in minutes + @field servings = contains(NumberField); // ²⁸ + @field difficulty = contains(DifficultyField); // ²⁹ + @field cuisine = contains(StringField); // ³⁰ + @field ingredients = containsMany(IngredientField); // ³¹ + @field instructions = contains(MarkdownField); // ³² + @field tips = contains(TextAreaField); // ³³ + @field tags = containsMany(StringField); // ³⁴ + + @field cardTitle = contains(StringField, { + // ³⁵ + computeVia: function (this: RecipeCard) { + return this.cardInfo?.name ?? this.recipeName ?? 'Untitled Recipe'; + }, + }); + + // ³⁶ Total time computed + get totalTimeDisplay() { + // ³⁷ + try { + const prep = this.prepTime ?? 0; + const cook = this.cookTime ?? 0; + const total = prep + cook; + if (total === 0) return null; + if (total >= 60) { + const hours = Math.floor(total / 60); + const mins = total % 60; + return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`; + } + return `${total}m`; + } catch (e) { + return null; + } + } + + // ³⁸ Isolated format + static isolated = class Isolated extends Component { + get prepDisplay() { + // ³⁹ + try { + const v = this.args.model?.prepTime; + if (!v) return null; + return v >= 60 + ? `${Math.floor(v / 60)}h ${v % 60 > 0 ? (v % 60) + 'm' : ''}`.trim() + : `${v}m`; + } catch (e) { + return null; + } + } + + get cookDisplay() { + // ⁴⁰ + try { + const v = this.args.model?.cookTime; + if (!v) return null; + return v >= 60 + ? `${Math.floor(v / 60)}h ${v % 60 > 0 ? (v % 60) + 'm' : ''}`.trim() + : `${v}m`; + } catch (e) { + return null; + } + } + + get totalDisplay() { + // ⁴¹ + try { + const prep = this.args.model?.prepTime ?? 0; + const cook = this.args.model?.cookTime ?? 0; + const total = prep + cook; + if (total === 0) return null; + return total >= 60 + ? `${Math.floor(total / 60)}h ${total % 60 > 0 ? (total % 60) + 'm' : ''}`.trim() + : `${total}m`; + } catch (e) { + return null; + } + } + + get difficultyColor() { + // ⁴² + const map: Record = { + Easy: 'var(--chart-2)', + Medium: 'var(--chart-3)', + Hard: 'var(--chart-4)', + Expert: 'var(--chart-1)', + }; + return ( + map[this.args.model?.difficulty ?? ''] ?? 'var(--muted-foreground)' + ); + } + + + }; + + // ⁴⁴ Embedded format + static embedded = class Embedded extends Component { + get totalDisplay() { + // ⁴⁵ + try { + const prep = this.args.model?.prepTime ?? 0; + const cook = this.args.model?.cookTime ?? 0; + const total = prep + cook; + if (total === 0) return null; + return total >= 60 + ? `${Math.floor(total / 60)}h ${total % 60 > 0 ? (total % 60) + 'm' : ''}`.trim() + : `${total}m`; + } catch (e) { + return null; + } + } + + + }; + + // ⁴⁶ Fitted format + static fitted = class Fitted extends Component { + get totalDisplay() { + // ⁴⁷ + try { + const prep = this.args.model?.prepTime ?? 0; + const cook = this.args.model?.cookTime ?? 0; + const total = prep + cook; + if (total === 0) return null; + return total >= 60 + ? `${Math.floor(total / 60)}h ${total % 60 > 0 ? (total % 60) + 'm' : ''}`.trim() + : `${total}m`; + } catch (e) { + return null; + } + } + + + }; +}