diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 4f283ca..6abc85b 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -1,5 +1,15 @@ # Attribution +## Food Truck Catering Social Posts + +The Food Truck Catering example set uses source-neutral Unsplash photos passed through template modification JSON files: + +- `examples/use-cases/assets/food-truck-catering-truck-unsplash.jpg` - Photo by Zhivko Dimitrov on Unsplash: https://unsplash.com/photos/oI9pFRh5mXM +- `examples/use-cases/assets/food-truck-catering-street-unsplash.jpg` - Photo by Jon Tyson on Unsplash: https://unsplash.com/photos/EhxhCueTZAk +- `examples/use-cases/assets/food-truck-catering-event-unsplash.jpg` - Photo by Sebastian Schuster on Unsplash: https://unsplash.com/photos/HUyOmTCIQTc + +Unsplash License: https://unsplash.com/license + ## Weave Home Retail Social Posts The Weave Home example set uses source-neutral Unsplash photos passed through template modification JSON files: diff --git a/README.md b/README.md index 174e4c7..b43ac24 100644 --- a/README.md +++ b/README.md @@ -723,6 +723,100 @@ live in [docs/examples.md](./docs/examples.md): - local templates, image layers, custom fonts, debug bundles, config recipes, and multi-size sets; - extra preset variations that demonstrate alternate option combinations. +### Render Food Truck Catering Social Posts + +Use a reusable mixed photo-and-graphic template for a fictional food truck monetization campaign: +route preorders, office catering inquiries, and private event deposits. Source-neutral Unsplash +photos are passed through JSON image layers, while the route cards, offer badges, borders, and +pattern work are intentional CSS geometry. + +#### Route Preorder Post + +CLI: + +```bash +npm run dev -- template examples/use-cases/food-truck-catering.html \ + --css examples/use-cases/food-truck-catering.css \ + --modify-file examples/use-cases/food-truck-catering-schedule.json \ + --out examples/use-cases/food-truck-catering-schedule.png \ + --width 1080 \ + --height 1080 +``` + +Library: + +```ts +await renderTemplate({ + htmlPath: "examples/use-cases/food-truck-catering.html", + cssPath: "examples/use-cases/food-truck-catering.css", + modifications: JSON.parse(await readFile("examples/use-cases/food-truck-catering-schedule.json", "utf8")), + viewport: { width: 1080, height: 1080 }, + output: { path: "examples/use-cases/food-truck-catering-schedule.png" }, +}); +``` + +Result: + +![Food truck route preorder social post result](./examples/use-cases/food-truck-catering-schedule.png) + +#### Office Catering Inquiry Post + +CLI: + +```bash +npm run dev -- template examples/use-cases/food-truck-catering.html \ + --css examples/use-cases/food-truck-catering.css \ + --modify-file examples/use-cases/food-truck-catering-catering.json \ + --out examples/use-cases/food-truck-catering-catering.png \ + --width 1080 \ + --height 1080 +``` + +Library: + +```ts +await renderTemplate({ + htmlPath: "examples/use-cases/food-truck-catering.html", + cssPath: "examples/use-cases/food-truck-catering.css", + modifications: JSON.parse(await readFile("examples/use-cases/food-truck-catering-catering.json", "utf8")), + viewport: { width: 1080, height: 1080 }, + output: { path: "examples/use-cases/food-truck-catering-catering.png" }, +}); +``` + +Result: + +![Food truck office catering inquiry social post result](./examples/use-cases/food-truck-catering-catering.png) + +#### Private Event Booking Post + +CLI: + +```bash +npm run dev -- template examples/use-cases/food-truck-catering.html \ + --css examples/use-cases/food-truck-catering.css \ + --modify-file examples/use-cases/food-truck-catering-booking.json \ + --out examples/use-cases/food-truck-catering-booking.png \ + --width 1080 \ + --height 1080 +``` + +Library: + +```ts +await renderTemplate({ + htmlPath: "examples/use-cases/food-truck-catering.html", + cssPath: "examples/use-cases/food-truck-catering.css", + modifications: JSON.parse(await readFile("examples/use-cases/food-truck-catering-booking.json", "utf8")), + viewport: { width: 1080, height: 1080 }, + output: { path: "examples/use-cases/food-truck-catering-booking.png" }, +}); +``` + +Result: + +![Food truck private event booking social post result](./examples/use-cases/food-truck-catering-booking.png) + ## Package Release The npm package is prepared as `@maurogoncalo/clickclick` with MIT licensing, public npm access, diff --git a/examples/use-cases/assets/food-truck-catering-event-unsplash.jpg b/examples/use-cases/assets/food-truck-catering-event-unsplash.jpg new file mode 100644 index 0000000..9245bc1 Binary files /dev/null and b/examples/use-cases/assets/food-truck-catering-event-unsplash.jpg differ diff --git a/examples/use-cases/assets/food-truck-catering-street-unsplash.jpg b/examples/use-cases/assets/food-truck-catering-street-unsplash.jpg new file mode 100644 index 0000000..2057c2c Binary files /dev/null and b/examples/use-cases/assets/food-truck-catering-street-unsplash.jpg differ diff --git a/examples/use-cases/assets/food-truck-catering-truck-unsplash.jpg b/examples/use-cases/assets/food-truck-catering-truck-unsplash.jpg new file mode 100644 index 0000000..a526ea2 Binary files /dev/null and b/examples/use-cases/assets/food-truck-catering-truck-unsplash.jpg differ diff --git a/examples/use-cases/food-truck-catering-booking.json b/examples/use-cases/food-truck-catering-booking.json new file mode 100644 index 0000000..8e6b870 --- /dev/null +++ b/examples/use-cases/food-truck-catering-booking.json @@ -0,0 +1,16 @@ +[ + { "name": "post", "className": "post post-booking" }, + { "name": "brand", "text": "Copper Cart" }, + { "name": "tagline", "text": "Private events and festivals" }, + { "name": "eyebrow", "text": "Bookings open" }, + { "name": "title", "html": "Bring the
truck in" }, + { "name": "subtitle", "text": "A photo-led booking card for venues turning weekend foot traffic into paid event deposits." }, + { "name": "hero_image", "src": "./assets/food-truck-catering-event-unsplash.jpg", "fit": "cover", "anchor": "50% 52%" }, + { "name": "route_label", "text": "Best for" }, + { "name": "route_one", "text": "Courtyard parties" }, + { "name": "route_two", "text": "Brewery nights" }, + { "name": "route_three", "text": "Local festivals" }, + { "name": "offer_label", "text": "Hold" }, + { "name": "offer_value", "text": "$250 deposit" }, + { "name": "cta", "text": "Book a service window at coppercart.example/events" } +] diff --git a/examples/use-cases/food-truck-catering-booking.png b/examples/use-cases/food-truck-catering-booking.png new file mode 100644 index 0000000..87a243d Binary files /dev/null and b/examples/use-cases/food-truck-catering-booking.png differ diff --git a/examples/use-cases/food-truck-catering-catering.json b/examples/use-cases/food-truck-catering-catering.json new file mode 100644 index 0000000..63394ce --- /dev/null +++ b/examples/use-cases/food-truck-catering-catering.json @@ -0,0 +1,16 @@ +[ + { "name": "post", "className": "post post-catering" }, + { "name": "brand", "text": "Copper Cart" }, + { "name": "tagline", "text": "Office lunches, packed hot" }, + { "name": "eyebrow", "text": "Catering" }, + { "name": "title", "html": "Feed the
whole team" }, + { "name": "subtitle", "text": "Boxed lunches and pop-up bars built for weekday office events." }, + { "name": "hero_image", "src": "./assets/food-truck-catering-street-unsplash.jpg", "fit": "cover", "anchor": "47% 45%" }, + { "name": "route_label", "text": "Packages" }, + { "name": "route_one", "text": "25 person lunch drop" }, + { "name": "route_two", "text": "Market table buffet" }, + { "name": "route_three", "text": "Late-night snack bar" }, + { "name": "offer_label", "text": "Starts" }, + { "name": "offer_value", "text": "$18 per guest" }, + { "name": "cta", "text": "Request dates at coppercart.example/catering" } +] diff --git a/examples/use-cases/food-truck-catering-catering.png b/examples/use-cases/food-truck-catering-catering.png new file mode 100644 index 0000000..df099e0 Binary files /dev/null and b/examples/use-cases/food-truck-catering-catering.png differ diff --git a/examples/use-cases/food-truck-catering-contact-sheet.png b/examples/use-cases/food-truck-catering-contact-sheet.png new file mode 100644 index 0000000..3aa0ccf Binary files /dev/null and b/examples/use-cases/food-truck-catering-contact-sheet.png differ diff --git a/examples/use-cases/food-truck-catering-schedule.json b/examples/use-cases/food-truck-catering-schedule.json new file mode 100644 index 0000000..b4a2e16 --- /dev/null +++ b/examples/use-cases/food-truck-catering-schedule.json @@ -0,0 +1,16 @@ +[ + { "name": "post", "className": "post post-schedule" }, + { "name": "brand", "text": "Copper Cart" }, + { "name": "tagline", "text": "Mobile kitchen and catering" }, + { "name": "eyebrow", "text": "This week" }, + { "name": "title", "html": "Lunch route
drop" }, + { "name": "subtitle", "text": "Three stops, one brisket taco special, preorders open now." }, + { "name": "hero_image", "src": "./assets/food-truck-catering-truck-unsplash.jpg", "fit": "cover", "anchor": "50% 54%" }, + { "name": "route_label", "text": "Route" }, + { "name": "route_one", "text": "Mon - Foundry Yard" }, + { "name": "route_two", "text": "Wed - Arcade Park" }, + { "name": "route_three", "text": "Fri - Harbor Lawn" }, + { "name": "offer_label", "text": "Preorder" }, + { "name": "offer_value", "text": "$12 lunch box" }, + { "name": "cta", "text": "Order by 10am at coppercart.example" } +] diff --git a/examples/use-cases/food-truck-catering-schedule.png b/examples/use-cases/food-truck-catering-schedule.png new file mode 100644 index 0000000..b49741c Binary files /dev/null and b/examples/use-cases/food-truck-catering-schedule.png differ diff --git a/examples/use-cases/food-truck-catering.css b/examples/use-cases/food-truck-catering.css new file mode 100644 index 0000000..8772232 --- /dev/null +++ b/examples/use-cases/food-truck-catering.css @@ -0,0 +1,394 @@ +body { + margin: 0; + background: #101010; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +.post { + --paper: #fff7e8; + --ink: #18120e; + --accent: #df4f28; + --accent-strong: #b7351a; + --cream: #fff7e8; + box-sizing: border-box; + position: relative; + overflow: hidden; + width: 1080px; + height: 1080px; + background: var(--paper); + color: var(--ink); +} + +.post::before, +.post::after { + position: absolute; + z-index: 1; + content: ""; +} + +.post::before { + inset: 0; + background: + linear-gradient(90deg, rgb(24 18 14 / 0.16) 1px, transparent 1px), + linear-gradient(180deg, rgb(24 18 14 / 0.13) 1px, transparent 1px); + background-size: 54px 54px; + opacity: 0.34; +} + +.post::after { + right: -150px; + bottom: -160px; + width: 440px; + height: 440px; + border: 34px solid var(--accent); + border-radius: 50%; + opacity: 0.72; +} + +.hero-image { + position: absolute; + z-index: 0; + object-fit: cover; +} + +.masthead, +.copy-panel, +.route-card, +.offer-card, +.cta { + position: absolute; + z-index: 3; +} + +.masthead { + top: 54px; + left: 58px; + right: 58px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.brand, +.tagline, +.eyebrow, +.subtitle, +.route-label, +.route-line, +.offer-label, +.offer-value, +.cta { + margin: 0; + letter-spacing: 0; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 12px; + font-size: 30px; + font-weight: 850; + line-height: 1; + text-transform: uppercase; +} + +.brand::before { + width: 38px; + height: 38px; + border: 4px solid currentColor; + border-radius: 50%; + box-shadow: inset 0 0 0 8px var(--accent); + content: ""; +} + +.tagline, +.eyebrow, +.route-label, +.offer-label { + font-size: 18px; + font-weight: 780; + text-transform: uppercase; +} + +.tagline { + color: rgb(24 18 14 / 0.68); +} + +.copy-panel h1 { + margin: 0; + font-size: 91px; + font-weight: 900; + line-height: 0.92; + letter-spacing: 0; +} + +.eyebrow { + margin-bottom: 18px; + color: var(--accent-strong); +} + +.subtitle { + margin-top: 28px; + max-width: 506px; + font-size: 31px; + font-weight: 650; + line-height: 1.18; +} + +.route-card, +.offer-card { + border: 4px solid var(--ink); + background: var(--cream); + box-shadow: 14px 14px 0 var(--ink); +} + +.route-card { + width: 392px; + padding: 30px 34px 34px; +} + +.route-label, +.offer-label { + color: var(--accent-strong); +} + +.route-line { + padding: 18px 0 0; + font-size: 29px; + font-weight: 820; + line-height: 1.04; +} + +.route-line + .route-line { + margin-top: 15px; + border-top: 2px solid rgb(24 18 14 / 0.22); +} + +.offer-card { + display: grid; + place-content: center; + width: 248px; + height: 248px; + border-radius: 50%; + text-align: center; + transform: rotate(-7deg); +} + +.offer-value { + max-width: 176px; + font-size: 39px; + font-weight: 920; + line-height: 0.95; +} + +.cta { + right: 58px; + bottom: 54px; + left: 58px; + display: flex; + align-items: center; + justify-content: center; + min-height: 66px; + border: 3px solid var(--ink); + background: var(--ink); + color: var(--cream); + font-size: 23px; + font-weight: 790; + text-align: center; +} + +.post-schedule { + --paper: #f8d95d; + --cream: #fff8e3; + --accent: #e95322; + --accent-strong: #a82d17; +} + +.post-schedule .hero-image { + top: 206px; + right: 58px; + width: 475px; + height: 562px; + border: 5px solid var(--ink); + box-shadow: 18px 18px 0 var(--accent-strong); +} + +.post-schedule .copy-panel { + top: 206px; + left: 58px; + width: 512px; +} + +.post-schedule .route-card { + left: 68px; + bottom: 142px; +} + +.post-schedule .offer-card { + right: 92px; + bottom: 158px; +} + +.post-catering { + --paper: #153c37; + --ink: #fff8e3; + --accent: #f4b941; + --accent-strong: #ff6b3c; + --cream: #fff8e3; + color: var(--ink); +} + +.post-catering::before { + background: + radial-gradient(circle at 16px 16px, rgb(255 248 227 / 0.42) 0 4px, transparent 5px), + linear-gradient(135deg, transparent 0 46%, rgb(255 248 227 / 0.18) 47% 53%, transparent 54%); + background-size: 52px 52px, 92px 92px; + opacity: 0.42; +} + +.post-catering::after { + border-color: var(--accent); + opacity: 0.32; +} + +.post-catering .masthead { + color: var(--cream); +} + +.post-catering .tagline { + color: rgb(255 248 227 / 0.72); +} + +.post-catering .hero-image { + left: 58px; + bottom: 148px; + width: 450px; + height: 575px; + border: 5px solid var(--cream); + border-radius: 235px 235px 32px 32px; + box-shadow: 18px 18px 0 var(--accent); +} + +.post-catering .copy-panel { + top: 180px; + right: 58px; + width: 456px; +} + +.post-catering .copy-panel h1 { + color: var(--cream); + font-size: 78px; +} + +.post-catering .subtitle { + color: rgb(255 248 227 / 0.88); + font-size: 28px; +} + +.post-catering .route-card, +.post-catering .offer-card { + border-color: var(--cream); + background: #fff8e3; + color: #173b36; + box-shadow: 14px 14px 0 var(--accent-strong); +} + +.post-catering .route-card { + right: 88px; + bottom: 161px; +} + +.post-catering .offer-card { + left: 292px; + top: 488px; +} + +.post-catering .cta { + border-color: var(--accent); + background: var(--accent); + color: #173b36; +} + +.post-booking { + --paper: #f4eee1; + --ink: #1c1a17; + --accent: #3aa0a1; + --accent-strong: #d83c2f; + --cream: #fffdf5; +} + +.post-booking .hero-image { + inset: 0; + width: 100%; + height: 100%; +} + +.post-booking::before { + z-index: 1; + background: + linear-gradient(90deg, rgb(18 16 13 / 0.78), rgb(18 16 13 / 0.35) 48%, rgb(18 16 13 / 0.05)), + linear-gradient(180deg, rgb(18 16 13 / 0.2), rgb(18 16 13 / 0.6)); + opacity: 1; +} + +.post-booking::after { + top: 128px; + right: 76px; + bottom: auto; + width: 220px; + height: 220px; + border-color: var(--accent); + opacity: 0.92; +} + +.post-booking .masthead { + color: var(--cream); +} + +.post-booking .tagline { + color: rgb(255 253 245 / 0.72); +} + +.post-booking .copy-panel { + top: 204px; + left: 58px; + width: 560px; + color: var(--cream); +} + +.post-booking .eyebrow { + color: #ffd166; +} + +.post-booking .copy-panel h1 { + font-size: 88px; +} + +.post-booking .subtitle { + color: rgb(255 253 245 / 0.89); +} + +.post-booking .route-card, +.post-booking .offer-card { + border-color: var(--cream); + background: rgb(255 253 245 / 0.94); + color: var(--ink); + box-shadow: 14px 14px 0 var(--accent-strong); +} + +.post-booking .route-card { + left: 58px; + bottom: 150px; + width: 460px; +} + +.post-booking .offer-card { + right: 86px; + bottom: 178px; +} + +.post-booking .cta { + border-color: #ffd166; + background: #ffd166; + color: var(--ink); +} diff --git a/examples/use-cases/food-truck-catering.html b/examples/use-cases/food-truck-catering.html new file mode 100644 index 0000000..68211d2 --- /dev/null +++ b/examples/use-cases/food-truck-catering.html @@ -0,0 +1,28 @@ +
+ + +
+

Copper Cart

+

Mobile kitchen and catering

+
+ +
+

This week

+

Lunch route drop

+

Three stops, one brisket taco special, preorders open now.

+
+ +
+

Route

+

Mon - Foundry Yard

+

Wed - Arcade Park

+

Fri - Harbor Lawn

+
+ +
+

Preorder

+

$12 lunch box

+
+ +

Order by 10am at coppercart.example

+
diff --git a/examples/use-cases/food-truck-catering.manifest.json b/examples/use-cases/food-truck-catering.manifest.json new file mode 100644 index 0000000..4d2e763 --- /dev/null +++ b/examples/use-cases/food-truck-catering.manifest.json @@ -0,0 +1,31 @@ +{ + "name": "Food Truck Catering Social Posts", + "description": "Use a reusable mixed photo-and-graphic template for a fictional food truck monetization campaign: route preorders, office catering inquiries, and private event deposits. Source-neutral Unsplash photos are passed through JSON image layers, while the route cards, offer badges, borders, and pattern work are intentional CSS geometry.", + "template": "./food-truck-catering.html", + "css": "./food-truck-catering.css", + "width": 1080, + "height": 1080, + "variants": [ + { + "id": "schedule", + "title": "Route Preorder Post", + "modifyFile": "./food-truck-catering-schedule.json", + "output": "./food-truck-catering-schedule.png", + "alt": "Food truck route preorder social post result" + }, + { + "id": "catering", + "title": "Office Catering Inquiry Post", + "modifyFile": "./food-truck-catering-catering.json", + "output": "./food-truck-catering-catering.png", + "alt": "Food truck office catering inquiry social post result" + }, + { + "id": "booking", + "title": "Private Event Booking Post", + "modifyFile": "./food-truck-catering-booking.json", + "output": "./food-truck-catering-booking.png", + "alt": "Food truck private event booking social post result" + } + ] +} diff --git a/test/food-truck-catering-example.test.ts b/test/food-truck-catering-example.test.ts new file mode 100644 index 0000000..751e536 --- /dev/null +++ b/test/food-truck-catering-example.test.ts @@ -0,0 +1,31 @@ +import { readFile } from "node:fs/promises"; +import { describe, expect, it } from "vitest"; + +describe("food truck catering example set", () => { + it("keeps media in variant modifications and documents every manifest variant", async () => { + const manifest = JSON.parse(await readFile("examples/use-cases/food-truck-catering.manifest.json", "utf8")) as { + template: string; + css: string; + variants: Array<{ title: string; modifyFile: string; output: string }>; + }; + const html = await readFile("examples/use-cases/food-truck-catering.html", "utf8"); + const readme = await readFile("README.md", "utf8"); + const attribution = await readFile("ATTRIBUTION.md", "utf8"); + + expect(manifest.template).toBe("./food-truck-catering.html"); + expect(manifest.css).toBe("./food-truck-catering.css"); + expect(manifest.variants).toHaveLength(3); + expect(html).not.toContain("unsplash"); + expect(html).not.toContain("food-truck-catering-"); + + for (const variant of manifest.variants) { + const modifications = await readFile(`examples/use-cases/${variant.modifyFile.replace("./", "")}`, "utf8"); + expect(modifications).toContain("./assets/food-truck-catering-"); + expect(readme).toContain(variant.title); + expect(readme).toContain(variant.output.replace("./", "examples/use-cases/")); + } + + expect(attribution).toContain("Food Truck Catering Social Posts"); + expect(attribution).toContain("Unsplash License"); + }); +});