diff --git a/RECOMMENDED_FONTS.md b/RECOMMENDED_FONTS.md index f992e0c..a4abeb9 100644 --- a/RECOMMENDED_FONTS.md +++ b/RECOMMENDED_FONTS.md @@ -1,49 +1,50 @@ # Coffee — Recommended fonts -The Coffee theme ships `fonts.json = []` per the wave-1 -[FONTS.md](../docs/FONTS.md) policy. No woff2s are bundled in this pass. -Templates resolve font families through the BlockNinja CSS variables -`--font-heading`, `--font-body`, and `--font-mono` with the fallback stacks -defined in `assets/style.css`. The site admin assigns fonts in the -typography panel; the picks below are the spec-aligned defaults that match -the visual identity. +The Coffee theme **bundles** its type as woff2 in `assets/fonts/` (latin +subsets, SIL Open Font License 1.1 — see `assets/fonts/OFL.txt`). The three +families become `source=template` rows in the fonts table and are selectable +in the admin typography picker. Templates always resolve font families through +the BlockNinja CSS variables `--font-heading`, `--font-body`, `--font-mono` +with the fallback stacks defined in the theme CSS — no family is hardcoded. -## How to apply +## Bundled families -1. Open the admin → Theme → Typography panel. -2. Switch to the **Google Fonts** tab. -3. Search and pick each family below. -4. Assign to the matching slot (Heading / Body / Mono). -5. Save. The picks are persisted as `google:` and rendered as - `@import` URLs by `theme.GenerateCSS()`. +| Slot | Family | Weights | Role | +|-----------|------------|-----------|------| +| Heading | Fraunces | 400, 600 | Ligature-rich display serif for headings and prices. Warm, hand-cut character. | +| Body | Work Sans | 400, 600 | Relaxed grotesque for long copy at 17px+. Reads well on cream and espresso backgrounds. | +| Accent | Caveat | 400, 700 | Hand-drawn script for badges, stamps, and doodle captions. | -## Picks +Mono is intentionally **not** bundled: prices and tabular figures fall back to +`"JetBrains Mono", ..., monospace` via `--font-mono`. Assign a mono family from +the Google Fonts tab if you want a specific face. -| Slot | Source | Family | Why | -|----------|------------------|------------------|-----| -| Heading | `google:Fraunces` | Fraunces | Ligature-rich display serif. Italic stylistic alternates land on h2/h3 headings; pairs with the spec's "Quentin Blake ink lines" aesthetic. | -| Body | `google:Inter` | Inter | Relaxed body sans for long copy at 17px+. Excellent screen rendering across cream and espresso backgrounds. | -| Mono | `google:JetBrains Mono` | JetBrains Mono | Tabular-numerals mono for prices in `menu_board` and `featured_pour`, and hours in `hours_strip`. | +## Default behaviour (no admin picks) -All three are already in the curated Google Fonts list in the picker and -have permissive SIL OFL 1.1 licences. +On a fresh install the admin has not assigned any slot, so `--font-heading` / +`--font-body` / `--font-mono` are unset and the CSS fallbacks apply. Because the +bundled families emit `@font-face` from the fonts table, the fallbacks resolve +to the bundled woff2: -## Fallback stacks (used before admin picks fonts) +- Headings render in **Fraunces**. +- Body renders in **Work Sans**. +- Hand-drawn accents (`.coffee-hand`) render in **Caveat**. -Defined in `assets/style.css`: +## How to change + +1. Open admin → Theme → Typography. +2. Pick a family per slot (bundled families appear as **Template** fonts; the + Google Fonts tab offers ~50 more). +3. Assign to Heading / Body / Mono and save. + +Note: the platform exposes three controllable slots. The hand-drawn accent +utility resolves through `--font-heading` (falling back to Caveat), so if you +deliberately change the heading font the badge accents follow it — that is the +three-slot model working as intended. + +## Fallback stacks (theme CSS) - `--coffee-heading-fallback`: `"Fraunces", "Playfair Display", Georgia, "Times New Roman", serif` -- `--coffee-body-fallback`: `"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif` +- `--coffee-body-fallback`: `"Work Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif` +- `--coffee-hand-fallback`: `"Caveat", "Segoe Print", "Bradley Hand", cursive` - `--coffee-mono-fallback`: `"JetBrains Mono", "Fira Code", Menlo, Consolas, monospace` - -Templates always consume the variable form, e.g. -`font-family: var(--font-heading, var(--coffee-heading-fallback))`. - -## Wave-2 follow-up - -If/when bundling becomes necessary (offline-first deployments, brand -exclusivity), commission/license the spec's exact Fraunces weights -(Regular 400, SemiBold 600, BoldItalic 700), Inter (Regular 400, Medium -500), and JetBrains Mono (Regular 400), and re-populate `fonts.json` per -[FONTS.md §"fonts.json schema"](../docs/FONTS.md). A `LICENSES.md` at the -theme root must also land in that pass. diff --git a/RECOMMENDED_ICONS.md b/RECOMMENDED_ICONS.md new file mode 100644 index 0000000..9abfc5f --- /dev/null +++ b/RECOMMENDED_ICONS.md @@ -0,0 +1,26 @@ +# Coffee — Recommended icon packs + +The Coffee theme renders icons through the CMS sprite system only +(``). It relies on two +bundled packs, declared in `plugin.mod` `required_icon_packs`: + +| Pack | Used for | +|----------------|----------| +| `lucide` | UI glyphs in builtin overrides (arrows, chevrons, check, map-pin, navigation, star, play, download, x). Bundled with the CMS — no install needed. | +| `simple-icons` | Brand marks in author-bio-hero and social-links (x, linkedin, github, instagram, facebook). Bundled with the CMS — no install needed. | + +Both packs ship with the CMS, so the theme renders with no manual setup. + +## If the loader does not auto-install + +The `required_icon_packs` field is forward-declared. Until the loader honors +it, confirm the packs are present: + +1. Open `/admin/icons`. +2. Confirm **Lucide** and **Simple Icons** are installed and enabled (both are + bundled defaults). +3. No downloads are required. + +Block content stores icon references as `"pack:name"` strings (e.g. +`"lucide:coffee"`, `"simple-icons:instagram"`). The theme never inlines SVG +paths. diff --git a/assets/fonts/OFL.txt b/assets/fonts/OFL.txt new file mode 100644 index 0000000..0bc9995 --- /dev/null +++ b/assets/fonts/OFL.txt @@ -0,0 +1,111 @@ +Bundled fonts — SIL Open Font License 1.1 +========================================= + +This directory bundles latin-subset woff2 files for three families, all +licensed under the SIL Open Font License, Version 1.1 (OFL-1.1). + +Copyright notices +----------------- +Fraunces: Copyright 2018 The Fraunces Project Authors (https://github.com/undercasetype/Fraunces) +Work Sans: Copyright 2019 The Work Sans Project Authors (https://github.com/weiweihuanghuang/Work-Sans) +Caveat: Copyright 2019 The Caveat Project Authors (https://github.com/googlefonts/caveat) + +Files +----- +fraunces-latin-400.woff2, fraunces-latin-600.woff2 — Fraunces (heading) +worksans-latin-400.woff2, worksans-latin-600.woff2 — Work Sans (body) +caveat-latin-400.woff2, caveat-latin-700.woff2 — Caveat (hand-drawn accents) + +Full license text (SIL OFL 1.1) +------------------------------- +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/caveat-latin-400.woff2 b/assets/fonts/caveat-latin-400.woff2 new file mode 100644 index 0000000..fb050a6 Binary files /dev/null and b/assets/fonts/caveat-latin-400.woff2 differ diff --git a/assets/fonts/caveat-latin-700.woff2 b/assets/fonts/caveat-latin-700.woff2 new file mode 100644 index 0000000..e8b6ac7 Binary files /dev/null and b/assets/fonts/caveat-latin-700.woff2 differ diff --git a/assets/fonts/fraunces-latin-400.woff2 b/assets/fonts/fraunces-latin-400.woff2 new file mode 100644 index 0000000..83cbd8f Binary files /dev/null and b/assets/fonts/fraunces-latin-400.woff2 differ diff --git a/assets/fonts/fraunces-latin-600.woff2 b/assets/fonts/fraunces-latin-600.woff2 new file mode 100644 index 0000000..58cee1f Binary files /dev/null and b/assets/fonts/fraunces-latin-600.woff2 differ diff --git a/assets/fonts/worksans-latin-400.woff2 b/assets/fonts/worksans-latin-400.woff2 new file mode 100644 index 0000000..02a6a42 Binary files /dev/null and b/assets/fonts/worksans-latin-400.woff2 differ diff --git a/assets/fonts/worksans-latin-600.woff2 b/assets/fonts/worksans-latin-600.woff2 new file mode 100644 index 0000000..1ec3048 Binary files /dev/null and b/assets/fonts/worksans-latin-600.woff2 differ diff --git a/assets/style.css b/assets/style.css index 01b7ede..3b25961 100644 --- a/assets/style.css +++ b/assets/style.css @@ -1,51 +1,49 @@ /* Coffee theme styles * - * Uses the 19 shadcn-style HSL token CSS variables (--background, - * --foreground, --primary, --accent, --border, --muted, --card, ...) via - * `hsl(var(--token))`. Font families are resolved through the BlockNinja font - * variables (--font-heading, --font-body, --font-mono) with fallback stacks - * derived from the spec §3 typography list. + * Dual-mode via the 19 shadcn-style HSL token CSS variables (--background, + * --foreground, --primary, --accent, --border, --muted, --card, ...) consumed + * as hsl(var(--token)). No literal colors. Font families resolve ONLY through + * the BlockNinja font variables (--font-heading, --font-body, --font-mono) with + * the fallback stacks below — never hardcoded, so the admin font picker works. */ -/* --- Font-family fallbacks --------------------------------------------- - * - * Templates use the variable form; the second argument is the fallback the - * site shows before the admin assigns fonts via the typography picker. - */ :root { --coffee-heading-fallback: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif; - --coffee-body-fallback: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --coffee-body-fallback: "Work Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --coffee-hand-fallback: "Caveat", "Segoe Print", "Bradley Hand", cursive; --coffee-mono-fallback: "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace; } -/* --- Paper grain background -------------------------------------------- - * - * Inline SVG noise overlay applied as the body background. Keeps file size - * tiny and palette-neutral so it works with all three presets. - */ +/* --- Paper grain background --------------------------------------------- */ body.coffee-paper { background-color: hsl(var(--background)); background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'%3E%3Cfilter id='paper-grain'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.18 0 0 0 0 0.13 0 0 0 0 0.09 0 0 0 0.07 0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23paper-grain)'/%3E%3C/svg%3E"); background-repeat: repeat; } -/* --- Headings ----------------------------------------------------------- */ +/* --- Type roles --------------------------------------------------------- */ .coffee-display { font-family: var(--font-heading, var(--coffee-heading-fallback)); font-feature-settings: "liga" 1, "dlig" 1; letter-spacing: -0.01em; } - .coffee-body { font-family: var(--font-body, var(--coffee-body-fallback)); line-height: 1.65; } - .coffee-mono { font-family: var(--font-mono, var(--coffee-mono-fallback)); } +/* Hand-drawn accents (badges, stamps, captions). Routed through --font-heading + * with a Caveat fallback: unassigned it renders Caveat; if the admin sets a + * heading font the accents follow it (the 3-slot model working as intended). */ +.coffee-hand { + font-family: var(--font-heading, var(--coffee-hand-fallback)); + letter-spacing: 0.01em; + line-height: 1.15; +} -/* --- Doodle underline (heading override) -------------------------------- */ +/* --- Doodle underline --------------------------------------------------- */ .coffee-doodle-underline { background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M2 8 Q 20 2 40 7 T 80 6 T 118 7' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' opacity='0.55'/%3E%3C/svg%3E"); background-repeat: no-repeat; @@ -54,7 +52,7 @@ body.coffee-paper { padding-bottom: 0.18em; } -/* --- Drop-cap (text override / article body) ---------------------------- */ +/* --- Drop-cap ----------------------------------------------------------- */ .coffee-dropcap > p:first-of-type::first-letter { font-family: var(--font-heading, var(--coffee-heading-fallback)); font-size: 4em; @@ -64,7 +62,7 @@ body.coffee-paper { color: hsl(var(--primary)); } -/* --- Kraft-tag button --------------------------------------------------- */ +/* --- Kraft-tag button (stamp press on hover) ---------------------------- */ .kraft-tag { position: relative; display: inline-flex; @@ -78,22 +76,29 @@ body.coffee-paper { font-family: var(--font-body, var(--coffee-body-fallback)); font-weight: 500; letter-spacing: 0.02em; - box-shadow: 0 1px 0 hsl(var(--border)); + box-shadow: 0 2px 0 hsl(var(--border)); transition: transform 120ms ease, box-shadow 120ms ease; cursor: pointer; } - .kraft-tag:hover { - transform: rotate(-1.2deg); - box-shadow: 0 2px 0 hsl(var(--border)); + transform: translateY(1px) rotate(-1.2deg); + box-shadow: 0 1px 0 hsl(var(--border)); } - .kraft-tag:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 3px; } +.kraft-tag--ghost { + background-color: transparent; + color: hsl(var(--primary)); + box-shadow: none; + border-style: dashed; +} +.kraft-tag--ghost:hover { + background-color: hsl(var(--muted)); +} -/* --- Torn-edge utility -------------------------------------------------- */ +/* --- Torn-paper edges --------------------------------------------------- */ .coffee-torn-top { -webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 12 L0 6 Q 5 2 10 5 T 20 4 T 30 6 T 40 3 T 50 5 T 60 4 T 70 5 T 80 3 T 90 5 T 100 4 L100 12 Z' fill='black'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 12 L0 6 Q 5 2 10 5 T 20 4 T 30 6 T 40 3 T 50 5 T 60 4 T 70 5 T 80 3 T 90 5 T 100 4 L100 12 Z' fill='black'/%3E%3C/svg%3E"); @@ -102,7 +107,6 @@ body.coffee-paper { -webkit-mask-size: 100% 100%; mask-size: 100% 100%; } - .coffee-torn-bottom { -webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L100 0 L100 6 Q 95 10 90 7 T 80 8 T 70 6 T 60 9 T 50 7 T 40 8 T 30 7 T 20 9 T 10 7 T 0 8 Z' fill='black'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L100 0 L100 6 Q 95 10 90 7 T 80 8 T 70 6 T 60 9 T 50 7 T 40 8 T 30 7 T 20 9 T 10 7 T 0 8 Z' fill='black'/%3E%3C/svg%3E"); @@ -112,25 +116,40 @@ body.coffee-paper { mask-size: 100% 100%; } -/* --- Coffee surfaces ---------------------------------------------------- */ +/* --- Surfaces ----------------------------------------------------------- */ .coffee-card { background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border: 1px solid hsl(var(--border)); border-radius: 0.5rem; } - .coffee-frame { border: 1px solid hsl(var(--border)); background-color: hsl(var(--card)); } - .coffee-pencil-rule { border-color: hsl(var(--border)); border-style: solid; } -/* --- Today highlight ---------------------------------------------------- */ +/* --- Chalkboard menu panel (inverts foreground/background for a hung board; + * light mode = dark board + cream chalk, dark mode = light menu paper) --- */ +.coffee-chalkboard { + position: relative; + background-color: hsl(var(--foreground)); + color: hsl(var(--background)); + border-radius: 0.5rem; + box-shadow: inset 0 0 0 3px hsl(var(--background) / 0.12), inset 0 0 0 4px hsl(var(--foreground)); +} +.coffee-chalk-rule { + border-color: hsl(var(--background) / 0.28); + border-style: solid; +} +.coffee-chalk-muted { + color: hsl(var(--background) / 0.72); +} + +/* --- Hours: today highlight --------------------------------------------- */ .coffee-hours-today { background-color: hsl(var(--accent) / 0.15); color: hsl(var(--accent-foreground)); @@ -138,14 +157,108 @@ body.coffee-paper { padding-left: 0.75rem; } -/* --- Price typography --------------------------------------------------- */ +/* --- Price + spec chips ------------------------------------------------- */ .coffee-price { font-family: var(--font-mono, var(--coffee-mono-fallback)); font-variant-numeric: tabular-nums; color: hsl(var(--accent)); } +.coffee-chip { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.3rem 0.7rem; + border: 1px dashed hsl(var(--border)); + border-radius: 9999px; + background-color: hsl(var(--muted)); + color: hsl(var(--foreground)); + font-family: var(--font-body, var(--coffee-body-fallback)); + font-size: 0.85rem; +} -/* --- Doodle pin overlay for location card ------------------------------- */ +/* --- Circular stamp / badge (press on hover) ---------------------------- */ +.coffee-stamp { + display: inline-flex; + align-items: center; + justify-content: center; + width: 4.25rem; + height: 4.25rem; + border-radius: 9999px; + background-color: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); + border: 2px solid hsl(var(--accent-foreground) / 0.35); + box-shadow: inset 0 0 0 3px hsl(var(--accent)); + font-family: var(--font-heading, var(--coffee-hand-fallback)); + font-size: 0.95rem; + text-align: center; + line-height: 1.05; + transform: rotate(-6deg); + transition: transform 140ms ease; +} +.coffee-stamp:hover { + transform: rotate(-6deg) scale(0.94); +} +.coffee-stamp-sm { + width: 2.25rem; + height: 2.25rem; + font-size: 1rem; + transform: rotate(-4deg); + box-shadow: none; +} + +/* --- Loyalty punch stamps ----------------------------------------------- */ +.coffee-punch { + display: inline-flex; + align-items: center; + justify-content: center; + aspect-ratio: 1 / 1; + border-radius: 9999px; + border: 2px dashed hsl(var(--border)); + color: hsl(var(--muted-foreground)); +} +.coffee-punch-on { + border-style: solid; + border-color: hsl(var(--accent)); + background-color: hsl(var(--accent) / 0.15); + color: hsl(var(--accent)); + transform: rotate(-5deg); +} +.coffee-punch-reward { + border-color: hsl(var(--primary)); + background-color: hsl(var(--primary) / 0.12); + color: hsl(var(--primary)); +} + +/* --- Location pin ------------------------------------------------------- */ .coffee-pin { color: hsl(var(--accent)); } + +/* --- Doodle divider draw-in --------------------------------------------- */ +.coffee-doodle-draw path, +.coffee-doodle-draw ellipse, +.coffee-doodle-draw circle { + stroke-dasharray: 240; + stroke-dashoffset: 240; + animation: coffee-draw 1.4s ease forwards; +} + +@keyframes coffee-draw { + to { stroke-dashoffset: 0; } +} + +/* --- Motion safety: static fallback ------------------------------------- */ +@media (prefers-reduced-motion: reduce) { + .kraft-tag, + .coffee-stamp, + .coffee-punch-on { transition: none; } + .kraft-tag:hover, + .coffee-stamp:hover { transform: none; } + .coffee-doodle-draw path, + .coffee-doodle-draw ellipse, + .coffee-doodle-draw circle { + stroke-dasharray: none; + stroke-dashoffset: 0; + animation: none; + } +} diff --git a/blocks/blocks.yaml b/blocks/blocks.yaml index eef4aca..384d143 100644 --- a/blocks/blocks.yaml +++ b/blocks/blocks.yaml @@ -1,42 +1,32 @@ -# Coffee theme block definitions (codeless .bnp, WO-WZ-021). -# Keys are fully qualified (coffee:) because the codeless loader registers -# definitions verbatim — there is no PluginBlockRegistry prefixing in the -# codeless path (unlike the wasm/templ path). The qualified keys match the -# hardcoded data-block attributes and the master_pages.json references. +# Coffee theme block definitions (codeless .bnp). +# Only true persona furniture stays a theme block. Everything that duplicated a +# builtin (menu, hours, location, footer, divider) is now a template override of +# the builtin key under templates/overrides/coffee/ — not a block here. +# +# Keys are fully qualified (coffee:): the codeless loader registers +# definitions verbatim (no PluginBlockRegistry prefixing in the codeless path). blocks: - - key: coffee:menu_board - title: Menu Board - description: Kraft-paper menu with sections of items (espresso, filter, pastry, ...) - category: content - schema: menu_board.schema.json - template: menu_board.ninjatpl - - key: coffee:hours_strip - title: Hours Strip - description: Weekly hours strip listing each day's opening hours. - category: content - schema: hours_strip.schema.json - template: hours_strip.ninjatpl - - key: coffee:location_card - title: Location Card - description: Address card with optional static map image and doodled pin overlay. - category: content - schema: location_card.schema.json - template: location_card.ninjatpl - key: coffee:featured_pour title: Featured Pour - description: Hero card for a featured coffee, tea or pastry with tasting notes and price. + description: Single-origin spotlight card with hand-drawn badge, tasting notes and price. category: content schema: featured_pour.schema.json template: featured_pour.ninjatpl - - key: coffee:footer - title: Footer - description: Torn-edge footer with optional location summary and newsletter caption. - category: layout - schema: footer.schema.json - template: footer.ninjatpl - - key: coffee:doodle_divider - title: Doodle Divider - description: Hand-drawn divider in one of four motifs (beans, croissant, cup, leaf). - category: layout - schema: doodle_divider.schema.json - template: doodle_divider.ninjatpl + - key: coffee:roast_profile + title: Roast Profile + description: Roastery spec card — origin, process, roast level and tasting notes with a circular roast stamp. + category: content + schema: roast_profile.schema.json + template: roast_profile.ninjatpl + - key: coffee:loyalty_card + title: Loyalty Card + description: Ten-stamp coffee punch card with a reward on the tenth stamp. + category: content + schema: loyalty_card.schema.json + template: loyalty_card.ninjatpl + - key: coffee:brew_guide + title: Brew Guide + description: Step-by-step brew method card with a ratio/grind/water/temp/time strip and numbered steps. + category: content + schema: brew_guide.schema.json + template: brew_guide.ninjatpl diff --git a/blocks/brew_guide.ninjatpl b/blocks/brew_guide.ninjatpl new file mode 100644 index 0000000..c97192c --- /dev/null +++ b/blocks/brew_guide.ninjatpl @@ -0,0 +1,29 @@ +
+
+
+ {% if kicker %}

{{ kicker }}

{% endif %} + {% if method %}

{{ method }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+ {% if ratio %}Ratio {{ ratio }}{% endif %} + {% if grind %}Grind {{ grind }}{% endif %} + {% if water %}Water {{ water }}{% endif %} + {% if temperature %}Temp {{ temperature }}{% endif %} + {% if time %}Time {{ time }}{% endif %} +
+ {% if steps %} +
    + {% for step in steps %} +
  1. + {{ forloop.Counter }} +
    + {% if step.title %}

    {{ step.title }}

    {% endif %} + {% if step.text %}

    {{ step.text }}

    {% endif %} +
    +
  2. + {% endfor %} +
+ {% endif %} +
+
+
diff --git a/blocks/brew_guide.schema.json b/blocks/brew_guide.schema.json new file mode 100644 index 0000000..2d34772 --- /dev/null +++ b/blocks/brew_guide.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Brew Guide", + "description": "Step-by-step brew method card with a ratio/grind/water/temp/time spec strip and numbered steps", + "type": "object", + "properties": { + "kicker": { + "type": "string", + "title": "Kicker (hand-drawn)", + "default": "How we brew it", + "x-editor": "text" + }, + "method": { + "type": "string", + "title": "Method", + "description": "e.g. \"V60 Pour-Over\"", + "x-editor": "text" + }, + "intro": { + "type": "string", + "title": "Intro", + "x-editor": "textarea" + }, + "ratio": { + "type": "string", + "title": "Ratio", + "description": "e.g. \"1:16\"", + "x-editor": "text" + }, + "grind": { + "type": "string", + "title": "Grind", + "description": "e.g. \"Medium-fine\"", + "x-editor": "text" + }, + "water": { + "type": "string", + "title": "Water", + "description": "e.g. \"300g\"", + "x-editor": "text" + }, + "temperature": { + "type": "string", + "title": "Temperature", + "description": "e.g. \"94C\"", + "x-editor": "text" + }, + "time": { + "type": "string", + "title": "Total Time", + "description": "e.g. \"3:00\"", + "x-editor": "text" + }, + "steps": { + "type": "array", + "title": "Steps", + "x-editor": "collection", + "items": { + "type": "object", + "properties": { + "title": { "type": "string", "title": "Step Title", "x-editor": "text" }, + "text": { "type": "string", "title": "Step Text", "x-editor": "textarea" } + } + } + } + } +} diff --git a/blocks/doodle_divider.ninjatpl b/blocks/doodle_divider.ninjatpl deleted file mode 100644 index a97a2d0..0000000 --- a/blocks/doodle_divider.ninjatpl +++ /dev/null @@ -1,39 +0,0 @@ -
- {% if motif == "croissant" %} - - {% elif motif == "cup" %} - - {% elif motif == "leaf" %} - - {% else %} - - {% endif %} -
diff --git a/blocks/doodle_divider.schema.json b/blocks/doodle_divider.schema.json deleted file mode 100644 index 3505381..0000000 --- a/blocks/doodle_divider.schema.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Doodle Divider", - "description": "Inline SVG divider in one of four hand-drawn motifs", - "type": "object", - "properties": { - "motif": { - "type": "string", - "title": "Motif", - "description": "Which doodle to render", - "default": "beans", - "x-editor": "select", - "enum": ["beans", "croissant", "cup", "leaf"] - } - } -} diff --git a/blocks/featured_pour.ninjatpl b/blocks/featured_pour.ninjatpl index eda009d..65e2223 100644 --- a/blocks/featured_pour.ninjatpl +++ b/blocks/featured_pour.ninjatpl @@ -1,15 +1,14 @@ -
-
- - Featured - +
+
+ {{ label|default:"Featured pour" }}
- {% if image %}{{ name }}{% else %}
Add a hero image
{% endif %} + {% if image %}{% img image alt=name|default:"Featured pour" class="h-full w-full object-cover" %}{% else %}
Add a hero image
{% endif %}
- {% if name %}

{{ name }}

{% else %}

Featured pour

{% endif %} - {% if tasting %}
{{ tasting|safe }}
{% endif %} - {% if price %}
{{ price }}
{% endif %} + {% if origin %}

{{ origin }}

{% endif %} + {% if name %}

{{ name }}

{% else %}

Featured pour

{% endif %} + {% if tasting %}
{{ tasting|safe }}
{% endif %} + {% if price %}
{{ currency|default:"$" }}{{ price }}
{% endif %}
diff --git a/blocks/featured_pour.schema.json b/blocks/featured_pour.schema.json index f7020f9..609c6b8 100644 --- a/blocks/featured_pour.schema.json +++ b/blocks/featured_pour.schema.json @@ -4,6 +4,19 @@ "description": "Hero card for a featured coffee, tea, or pastry with image, tasting notes and price", "type": "object", "properties": { + "label": { + "type": "string", + "title": "Badge Label", + "description": "Hand-drawn badge text", + "default": "Featured pour", + "x-editor": "text" + }, + "origin": { + "type": "string", + "title": "Origin / Kicker", + "description": "Small hand-drawn line above the name (e.g. \"Single origin\")", + "x-editor": "text" + }, "name": { "type": "string", "title": "Name", @@ -22,6 +35,12 @@ "description": "Hero image", "x-editor": "media" }, + "currency": { + "type": "string", + "title": "Currency Symbol", + "default": "$", + "x-editor": "text" + }, "price": { "type": "string", "title": "Price", diff --git a/blocks/footer.ninjatpl b/blocks/footer.ninjatpl deleted file mode 100644 index 694152c..0000000 --- a/blocks/footer.ninjatpl +++ /dev/null @@ -1,28 +0,0 @@ -
-
-
-

Hello there

-

Pull up a seat. Pastries from 7, coffee until late.

-
- {% if showLocation != "false" and showLocation != false %} -
-

Visit

-
42 Roastery Lane -City, State 9000
-
- {% endif %} -
-

Stay in touch

- {% if newsletterText %}

{{ newsletterText }}

{% else %}

Slow notes from the bar. Brew tips, seasonal pours, no spam.

{% endif %} -
- - - -
-
-
-
- © Coffee theme — kraft paper edition. - Hand-drawn with care. -
-
diff --git a/blocks/footer.schema.json b/blocks/footer.schema.json deleted file mode 100644 index 8c74c49..0000000 --- a/blocks/footer.schema.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Coffee Footer", - "description": "Torn-edge footer with optional location block and newsletter caption", - "type": "object", - "properties": { - "showLocation": { - "type": "string", - "title": "Show Location", - "description": "Whether to render the location/hours summary", - "default": "true", - "x-editor": "select", - "enum": ["true", "false"] - }, - "newsletterText": { - "type": "string", - "title": "Newsletter Caption", - "description": "Short caption above the newsletter input", - "x-editor": "textarea" - } - } -} diff --git a/blocks/hours_strip.ninjatpl b/blocks/hours_strip.ninjatpl deleted file mode 100644 index e23ad0f..0000000 --- a/blocks/hours_strip.ninjatpl +++ /dev/null @@ -1,13 +0,0 @@ -{% if context.now %}{% set today = context.now|date:"D"|lower %}{% endif %} - diff --git a/blocks/hours_strip.schema.json b/blocks/hours_strip.schema.json deleted file mode 100644 index f9a1c42..0000000 --- a/blocks/hours_strip.schema.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Hours Strip", - "description": "Horizontal opening hours strip with today's row server-side highlighted", - "type": "object", - "properties": { - "todayLabel": { - "type": "string", - "title": "Today Label", - "description": "Label rendered next to today's row (e.g. \"Today\")", - "default": "Today", - "x-editor": "text" - }, - "hours": { - "type": "array", - "title": "Weekly Hours", - "description": "One row per day. Days not listed render as closed.", - "default": [], - "x-editor": "collection", - "items": { - "type": "object", - "properties": { - "day": { - "type": "string", - "title": "Day", - "description": "Weekday (Mon, Tue, Wed, Thu, Fri, Sat, Sun)", - "x-editor": "select", - "enum": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - }, - "open": { - "type": "string", - "title": "Opens", - "description": "Opening time (e.g. \"7:00\")", - "x-editor": "text" - }, - "close": { - "type": "string", - "title": "Closes", - "description": "Closing time (e.g. \"15:00\")", - "x-editor": "text" - } - }, - "required": ["day"] - } - } - } -} diff --git a/blocks/location_card.ninjatpl b/blocks/location_card.ninjatpl deleted file mode 100644 index f61f3b6..0000000 --- a/blocks/location_card.ninjatpl +++ /dev/null @@ -1,18 +0,0 @@ -
-
-
- {% if mapImage %}Map of our location{% else %}
Map preview
{% endif %} - -
-
-

- Find us -

- {% if address %}
{{ address }}
{% else %}

Add an address to render the location card.

{% endif %} - {% if directionsUrl %}Directions{% endif %} -
-
-
diff --git a/blocks/location_card.schema.json b/blocks/location_card.schema.json deleted file mode 100644 index 334f5e4..0000000 --- a/blocks/location_card.schema.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Location Card", - "description": "Address card with optional map image and directions link, doodle-pin overlay", - "type": "object", - "properties": { - "address": { - "type": "string", - "title": "Address", - "description": "Street address as a multi-line block", - "x-editor": "textarea" - }, - "mapImage": { - "type": "string", - "title": "Map Image", - "description": "Optional static map image", - "x-editor": "media" - }, - "directionsUrl": { - "type": "string", - "title": "Directions URL", - "description": "Link to Google Maps or similar", - "x-editor": "link" - } - } -} diff --git a/blocks/loyalty_card.ninjatpl b/blocks/loyalty_card.ninjatpl new file mode 100644 index 0000000..4221bfd --- /dev/null +++ b/blocks/loyalty_card.ninjatpl @@ -0,0 +1,24 @@ +{% with filled=filled|default:0 %} +
+
+
+ {% if title %}

{{ title }}

{% endif %} + {% if subtitle %}

{{ subtitle }}

{% endif %} +
+ {% if filled >= 1 %}{% endif %} + {% if filled >= 2 %}{% endif %} + {% if filled >= 3 %}{% endif %} + {% if filled >= 4 %}{% endif %} + {% if filled >= 5 %}{% endif %} + {% if filled >= 6 %}{% endif %} + {% if filled >= 7 %}{% endif %} + {% if filled >= 8 %}{% endif %} + {% if filled >= 9 %}{% endif %} + +
+ {% if reward %}

{{ reward }}

{% endif %} + {% if footnote %}

{{ footnote }}

{% endif %} +
+
+
+{% endwith %} diff --git a/blocks/loyalty_card.schema.json b/blocks/loyalty_card.schema.json new file mode 100644 index 0000000..c017431 --- /dev/null +++ b/blocks/loyalty_card.schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Loyalty Card", + "description": "Ten-stamp coffee punch card. The tenth stamp is the reward. Set how many stamps are filled for the demo state.", + "type": "object", + "properties": { + "title": { + "type": "string", + "title": "Title", + "default": "Buy nine, tenth on us", + "x-editor": "text" + }, + "subtitle": { + "type": "string", + "title": "Subtitle (hand-drawn)", + "x-editor": "text" + }, + "filled": { + "type": "number", + "title": "Stamps Filled", + "description": "How many of the ten stamps are punched (0-10)", + "default": 0, + "minimum": 0, + "maximum": 10, + "x-editor": "number" + }, + "reward": { + "type": "string", + "title": "Reward Line", + "x-editor": "text" + }, + "footnote": { + "type": "string", + "title": "Footnote (hand-drawn)", + "x-editor": "text" + } + } +} diff --git a/blocks/menu_board.ninjatpl b/blocks/menu_board.ninjatpl deleted file mode 100644 index 718b34d..0000000 --- a/blocks/menu_board.ninjatpl +++ /dev/null @@ -1,26 +0,0 @@ -
-
-

- {{ title|default:"Menu" }} -

- {% if not sections %}

Add a section to get started.

{% endif %} - {% for section in sections %} -
- {% if section.name %}

{{ section.name }}

{% endif %} - {% if not section.items %}

No items yet.

{% endif %} -
    - {% for item in section.items %} -
  • -
    -
    {{ item.name }}
    - {% if item.note %}
    {{ item.note }}
    {% endif %} - {% if item.allergens %}
    Allergens: {{ item.allergens }}
    {% endif %} -
    - {% if item.price %}
    {{ item.price }}
    {% endif %} -
  • - {% endfor %} -
-
- {% endfor %} -
-
diff --git a/blocks/menu_board.schema.json b/blocks/menu_board.schema.json deleted file mode 100644 index 0e9bfaa..0000000 --- a/blocks/menu_board.schema.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Menu Board", - "description": "Kraft-paper menu card with sections of items (espresso, filter, pastry, etc.)", - "type": "object", - "properties": { - "title": { - "type": "string", - "title": "Title", - "description": "Heading shown at the top of the menu (e.g. \"Menu\", \"Today's Pour\")", - "default": "Menu", - "x-editor": "text" - }, - "sections": { - "type": "array", - "title": "Sections", - "description": "Menu sections such as Espresso, Filter, Pastry", - "default": [], - "x-editor": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Section Name", - "description": "Section heading (e.g. \"Espresso\")", - "x-editor": "text" - }, - "items": { - "type": "array", - "title": "Items", - "description": "Items in this section", - "x-editor": "collection", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Name", - "description": "Item name (e.g. \"Flat White\")", - "x-editor": "text" - }, - "price": { - "type": "string", - "title": "Price", - "description": "Price (e.g. \"5.50\")", - "x-editor": "text" - }, - "note": { - "type": "string", - "title": "Note", - "description": "Optional descriptive line (e.g. tasting notes)", - "x-editor": "text" - }, - "allergens": { - "type": "string", - "title": "Allergens", - "description": "Free-text allergen list (e.g. \"contains dairy, gluten\")", - "x-editor": "text" - } - }, - "required": ["name"] - } - } - }, - "required": ["name"] - } - } - } -} diff --git a/blocks/roast_profile.ninjatpl b/blocks/roast_profile.ninjatpl new file mode 100644 index 0000000..3f4ea2f --- /dev/null +++ b/blocks/roast_profile.ninjatpl @@ -0,0 +1,26 @@ +
+
+
+
+
+
+ {% if origin %}

{{ origin }}

{% endif %} + {% if name %}

{{ name }}

{% endif %} + {% if producer %}

{{ producer }}

{% endif %} +
+ {% if roastLevel %}{{ roastLevel }}{% endif %} +
+ {% if notes %}
{{ notes|safe }}
{% endif %} +
+ {% if process %}
Process
{{ process }}
{% endif %} + {% if varietal %}
Varietal
{{ varietal }}
{% endif %} + {% if altitude %}
Altitude
{{ altitude }}
{% endif %} + {% if harvest %}
Harvest
{{ harvest }}
{% endif %} +
+
+
+ {% if image %}{% img image alt=name|default:"Roast profile" class="h-full w-full object-cover" %}{% else %}
Bean photo
{% endif %} +
+
+
+
diff --git a/blocks/roast_profile.schema.json b/blocks/roast_profile.schema.json new file mode 100644 index 0000000..27a1b04 --- /dev/null +++ b/blocks/roast_profile.schema.json @@ -0,0 +1,64 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Roast Profile", + "description": "Single-origin roastery spec card: origin, process, roast level and tasting notes with a circular roast stamp", + "type": "object", + "properties": { + "origin": { + "type": "string", + "title": "Origin / Kicker", + "description": "Hand-drawn line above the name (e.g. \"Single origin\")", + "x-editor": "text" + }, + "name": { + "type": "string", + "title": "Coffee Name", + "description": "e.g. \"Guji Highlands\"", + "x-editor": "text" + }, + "producer": { + "type": "string", + "title": "Producer / Farm", + "x-editor": "text" + }, + "roastLevel": { + "type": "string", + "title": "Roast Level (stamp)", + "description": "Short label shown in the circular stamp (e.g. \"Medium\")", + "x-editor": "text" + }, + "notes": { + "type": "string", + "title": "Tasting Notes", + "description": "Rich text tasting description", + "x-editor": "richtext" + }, + "process": { + "type": "string", + "title": "Process", + "description": "e.g. Washed, Natural, Honey", + "x-editor": "text" + }, + "varietal": { + "type": "string", + "title": "Varietal", + "x-editor": "text" + }, + "altitude": { + "type": "string", + "title": "Altitude", + "description": "e.g. \"1900-2100 masl\"", + "x-editor": "text" + }, + "harvest": { + "type": "string", + "title": "Harvest", + "x-editor": "text" + }, + "image": { + "type": "string", + "title": "Image", + "x-editor": "media" + } + } +} diff --git a/fonts.json b/fonts.json index fe51488..5985fd7 100644 --- a/fonts.json +++ b/fonts.json @@ -1 +1,26 @@ -[] +[ + { + "name": "Fraunces", + "family": "Fraunces", + "variants": [ + { "weight": "400", "style": "normal", "file": "fonts/fraunces-latin-400.woff2" }, + { "weight": "600", "style": "normal", "file": "fonts/fraunces-latin-600.woff2" } + ] + }, + { + "name": "Work Sans", + "family": "Work Sans", + "variants": [ + { "weight": "400", "style": "normal", "file": "fonts/worksans-latin-400.woff2" }, + { "weight": "600", "style": "normal", "file": "fonts/worksans-latin-600.woff2" } + ] + }, + { + "name": "Caveat", + "family": "Caveat", + "variants": [ + { "weight": "400", "style": "normal", "file": "fonts/caveat-latin-400.woff2" }, + { "weight": "700", "style": "normal", "file": "fonts/caveat-latin-700.woff2" } + ] + } +] diff --git a/manifest.yaml b/manifest.yaml index afe2102..ba4b013 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -1,95 +1,157 @@ -# Coffee theme — codeless .bnp manifest (WO-WZ-021 Phase B1). +# Coffee theme — codeless .bnp manifest (Wave A, WO-TF-013). # Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm). theme_presets: presets.json bundled_fonts: fonts.json master_pages: master_pages.json +required_icon_packs: + - lucide + - simple-icons + system_templates: - key: coffee title: Coffee - description: "Warm, hand-crafted theme for cafes, bakeries, and slow-craft makers" + description: "Warm, hand-crafted cafe and roastery theme — kraft paper, chalkboard menus, doodles and stamps." page_templates: - system: coffee key: default title: Default - description: "Header + main + footer with warm paper background" + description: "Header + main + footer over warm paper grain." slots: [header, main, footer] - system: coffee key: landing title: Landing - description: "Hero pour + featured menu, big imagery" + description: "Hero pour, featured menu, story and invitation." slots: [hero, menu, story, cta, footer] - system: coffee key: article title: Article - description: "Narrow narrative column for journal / recipes" + description: "Narrow narrative column with drop-cap for journal and recipes." slots: [header, main, footer] - system: coffee key: full-width title: Full Width - description: "Edge-to-edge gallery / interior shots" + description: "Edge-to-edge gallery and interior shots." slots: [header, main, footer] - -template_overrides: - - { template: coffee, block: heading } - - { template: coffee, block: text } - - { template: coffee, block: button } - - { template: coffee, block: image } + - system: coffee + key: blog-index + title: Blog Index + description: "Blog hero, featured posts and a post grid." + slots: [header, featured, main, footer] + - system: coffee + key: contact + title: Contact + description: "Contact form, hours and location." + slots: [header, main, footer] + - system: coffee + key: auth + title: Auth + description: "Centered card for login, register and password reset." + slots: [main, footer] email_wrappers: - coffee +template_overrides: + - { template: coffee, block: announcement-bar } + - { template: coffee, block: audio-embed } + - { template: coffee, block: auth-form } + - { template: coffee, block: author-bio } + - { template: coffee, block: author-bio-hero } + - { template: coffee, block: auth-status } + - { template: coffee, block: blog-hero } + - { template: coffee, block: breadcrumbs } + - { template: coffee, block: button } + - { template: coffee, block: card } + - { template: coffee, block: category-list } + - { template: coffee, block: contact-card } + - { template: coffee, block: contact-form } + - { template: coffee, block: countdown } + - { template: coffee, block: cta } + - { template: coffee, block: divider } + - { template: coffee, block: event-list } + - { template: coffee, block: faq } + - { template: coffee, block: featured-posts } + - { template: coffee, block: feature-grid } + - { template: coffee, block: footer } + - { template: coffee, block: gallery } + - { template: coffee, block: heading } + - { template: coffee, block: hero } + - { template: coffee, block: hours-location } + - { template: coffee, block: image } + - { template: coffee, block: logo-strip } + - { template: coffee, block: menu-list } + - { template: coffee, block: navbar } + - { template: coffee, block: newsletter-signup } + - { template: coffee, block: page-suggestions } + - { template: coffee, block: password-reset-form } + - { template: coffee, block: popular-posts } + - { template: coffee, block: post-hero } + - { template: coffee, block: post-metadata } + - { template: coffee, block: pricing } + - { template: coffee, block: quote } + - { template: coffee, block: related-posts } + - { template: coffee, block: rich-section } + - { template: coffee, block: rich-text } + - { template: coffee, block: sidebar-nav } + - { template: coffee, block: social-links } + - { template: coffee, block: stats } + - { template: coffee, block: team } + - { template: coffee, block: testimonial } + - { template: coffee, block: text } + - { template: coffee, block: timeline } + - { template: coffee, block: utility-bar } + - { template: coffee, block: video-embed } + css: input_css_append: | /* Coffee theme styles * - * Uses the 19 shadcn-style HSL token CSS variables (--background, - * --foreground, --primary, --accent, --border, --muted, --card, ...) via - * `hsl(var(--token))`. Font families are resolved through the BlockNinja font - * variables (--font-heading, --font-body, --font-mono) with fallback stacks - * derived from the spec §3 typography list. + * Dual-mode via the 19 shadcn-style HSL token CSS variables (--background, + * --foreground, --primary, --accent, --border, --muted, --card, ...) consumed + * as hsl(var(--token)). No literal colors. Font families resolve ONLY through + * the BlockNinja font variables (--font-heading, --font-body, --font-mono) with + * the fallback stacks below — never hardcoded, so the admin font picker works. */ - /* --- Font-family fallbacks --------------------------------------------- - * - * Templates use the variable form; the second argument is the fallback the - * site shows before the admin assigns fonts via the typography picker. - */ :root { --coffee-heading-fallback: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif; - --coffee-body-fallback: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --coffee-body-fallback: "Work Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --coffee-hand-fallback: "Caveat", "Segoe Print", "Bradley Hand", cursive; --coffee-mono-fallback: "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace; } - /* --- Paper grain background -------------------------------------------- - * - * Inline SVG noise overlay applied as the body background. Keeps file size - * tiny and palette-neutral so it works with all three presets. - */ + /* --- Paper grain background --------------------------------------------- */ body.coffee-paper { background-color: hsl(var(--background)); background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'%3E%3Cfilter id='paper-grain'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.18 0 0 0 0 0.13 0 0 0 0 0.09 0 0 0 0.07 0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23paper-grain)'/%3E%3C/svg%3E"); background-repeat: repeat; } - /* --- Headings ----------------------------------------------------------- */ + /* --- Type roles --------------------------------------------------------- */ .coffee-display { font-family: var(--font-heading, var(--coffee-heading-fallback)); font-feature-settings: "liga" 1, "dlig" 1; letter-spacing: -0.01em; } - .coffee-body { font-family: var(--font-body, var(--coffee-body-fallback)); line-height: 1.65; } - .coffee-mono { font-family: var(--font-mono, var(--coffee-mono-fallback)); } + /* Hand-drawn accents (badges, stamps, captions). Routed through --font-heading + * with a Caveat fallback: unassigned it renders Caveat; if the admin sets a + * heading font the accents follow it (the 3-slot model working as intended). */ + .coffee-hand { + font-family: var(--font-heading, var(--coffee-hand-fallback)); + letter-spacing: 0.01em; + line-height: 1.15; + } - /* --- Doodle underline (heading override) -------------------------------- */ + /* --- Doodle underline --------------------------------------------------- */ .coffee-doodle-underline { background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M2 8 Q 20 2 40 7 T 80 6 T 118 7' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' opacity='0.55'/%3E%3C/svg%3E"); background-repeat: no-repeat; @@ -98,7 +160,7 @@ css: padding-bottom: 0.18em; } - /* --- Drop-cap (text override / article body) ---------------------------- */ + /* --- Drop-cap ----------------------------------------------------------- */ .coffee-dropcap > p:first-of-type::first-letter { font-family: var(--font-heading, var(--coffee-heading-fallback)); font-size: 4em; @@ -108,7 +170,7 @@ css: color: hsl(var(--primary)); } - /* --- Kraft-tag button --------------------------------------------------- */ + /* --- Kraft-tag button (stamp press on hover) ---------------------------- */ .kraft-tag { position: relative; display: inline-flex; @@ -122,22 +184,29 @@ css: font-family: var(--font-body, var(--coffee-body-fallback)); font-weight: 500; letter-spacing: 0.02em; - box-shadow: 0 1px 0 hsl(var(--border)); + box-shadow: 0 2px 0 hsl(var(--border)); transition: transform 120ms ease, box-shadow 120ms ease; cursor: pointer; } - .kraft-tag:hover { - transform: rotate(-1.2deg); - box-shadow: 0 2px 0 hsl(var(--border)); + transform: translateY(1px) rotate(-1.2deg); + box-shadow: 0 1px 0 hsl(var(--border)); } - .kraft-tag:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 3px; } + .kraft-tag--ghost { + background-color: transparent; + color: hsl(var(--primary)); + box-shadow: none; + border-style: dashed; + } + .kraft-tag--ghost:hover { + background-color: hsl(var(--muted)); + } - /* --- Torn-edge utility -------------------------------------------------- */ + /* --- Torn-paper edges --------------------------------------------------- */ .coffee-torn-top { -webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 12 L0 6 Q 5 2 10 5 T 20 4 T 30 6 T 40 3 T 50 5 T 60 4 T 70 5 T 80 3 T 90 5 T 100 4 L100 12 Z' fill='black'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 12 L0 6 Q 5 2 10 5 T 20 4 T 30 6 T 40 3 T 50 5 T 60 4 T 70 5 T 80 3 T 90 5 T 100 4 L100 12 Z' fill='black'/%3E%3C/svg%3E"); @@ -146,7 +215,6 @@ css: -webkit-mask-size: 100% 100%; mask-size: 100% 100%; } - .coffee-torn-bottom { -webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L100 0 L100 6 Q 95 10 90 7 T 80 8 T 70 6 T 60 9 T 50 7 T 40 8 T 30 7 T 20 9 T 10 7 T 0 8 Z' fill='black'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L100 0 L100 6 Q 95 10 90 7 T 80 8 T 70 6 T 60 9 T 50 7 T 40 8 T 30 7 T 20 9 T 10 7 T 0 8 Z' fill='black'/%3E%3C/svg%3E"); @@ -156,25 +224,40 @@ css: mask-size: 100% 100%; } - /* --- Coffee surfaces ---------------------------------------------------- */ + /* --- Surfaces ----------------------------------------------------------- */ .coffee-card { background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border: 1px solid hsl(var(--border)); border-radius: 0.5rem; } - .coffee-frame { border: 1px solid hsl(var(--border)); background-color: hsl(var(--card)); } - .coffee-pencil-rule { border-color: hsl(var(--border)); border-style: solid; } - /* --- Today highlight ---------------------------------------------------- */ + /* --- Chalkboard menu panel (inverts foreground/background for a hung board; + * light mode = dark board + cream chalk, dark mode = light menu paper) --- */ + .coffee-chalkboard { + position: relative; + background-color: hsl(var(--foreground)); + color: hsl(var(--background)); + border-radius: 0.5rem; + box-shadow: inset 0 0 0 3px hsl(var(--background) / 0.12), inset 0 0 0 4px hsl(var(--foreground)); + } + .coffee-chalk-rule { + border-color: hsl(var(--background) / 0.28); + border-style: solid; + } + .coffee-chalk-muted { + color: hsl(var(--background) / 0.72); + } + + /* --- Hours: today highlight --------------------------------------------- */ .coffee-hours-today { background-color: hsl(var(--accent) / 0.15); color: hsl(var(--accent-foreground)); @@ -182,14 +265,108 @@ css: padding-left: 0.75rem; } - /* --- Price typography --------------------------------------------------- */ + /* --- Price + spec chips ------------------------------------------------- */ .coffee-price { font-family: var(--font-mono, var(--coffee-mono-fallback)); font-variant-numeric: tabular-nums; color: hsl(var(--accent)); } + .coffee-chip { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.3rem 0.7rem; + border: 1px dashed hsl(var(--border)); + border-radius: 9999px; + background-color: hsl(var(--muted)); + color: hsl(var(--foreground)); + font-family: var(--font-body, var(--coffee-body-fallback)); + font-size: 0.85rem; + } - /* --- Doodle pin overlay for location card ------------------------------- */ + /* --- Circular stamp / badge (press on hover) ---------------------------- */ + .coffee-stamp { + display: inline-flex; + align-items: center; + justify-content: center; + width: 4.25rem; + height: 4.25rem; + border-radius: 9999px; + background-color: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); + border: 2px solid hsl(var(--accent-foreground) / 0.35); + box-shadow: inset 0 0 0 3px hsl(var(--accent)); + font-family: var(--font-heading, var(--coffee-hand-fallback)); + font-size: 0.95rem; + text-align: center; + line-height: 1.05; + transform: rotate(-6deg); + transition: transform 140ms ease; + } + .coffee-stamp:hover { + transform: rotate(-6deg) scale(0.94); + } + .coffee-stamp-sm { + width: 2.25rem; + height: 2.25rem; + font-size: 1rem; + transform: rotate(-4deg); + box-shadow: none; + } + + /* --- Loyalty punch stamps ----------------------------------------------- */ + .coffee-punch { + display: inline-flex; + align-items: center; + justify-content: center; + aspect-ratio: 1 / 1; + border-radius: 9999px; + border: 2px dashed hsl(var(--border)); + color: hsl(var(--muted-foreground)); + } + .coffee-punch-on { + border-style: solid; + border-color: hsl(var(--accent)); + background-color: hsl(var(--accent) / 0.15); + color: hsl(var(--accent)); + transform: rotate(-5deg); + } + .coffee-punch-reward { + border-color: hsl(var(--primary)); + background-color: hsl(var(--primary) / 0.12); + color: hsl(var(--primary)); + } + + /* --- Location pin ------------------------------------------------------- */ .coffee-pin { color: hsl(var(--accent)); } + + /* --- Doodle divider draw-in --------------------------------------------- */ + .coffee-doodle-draw path, + .coffee-doodle-draw ellipse, + .coffee-doodle-draw circle { + stroke-dasharray: 240; + stroke-dashoffset: 240; + animation: coffee-draw 1.4s ease forwards; + } + + @keyframes coffee-draw { + to { stroke-dashoffset: 0; } + } + + /* --- Motion safety: static fallback ------------------------------------- */ + @media (prefers-reduced-motion: reduce) { + .kraft-tag, + .coffee-stamp, + .coffee-punch-on { transition: none; } + .kraft-tag:hover, + .coffee-stamp:hover { transform: none; } + .coffee-doodle-draw path, + .coffee-doodle-draw ellipse, + .coffee-doodle-draw circle { + stroke-dasharray: none; + stroke-dashoffset: 0; + animation: none; + } + } diff --git a/master_pages.json b/master_pages.json index 74349ff..d9158a4 100644 --- a/master_pages.json +++ b/master_pages.json @@ -2,12 +2,11 @@ { "key": "coffee:default-master", "title": "Coffee Default Master", - "page_templates": ["default", "article"], + "page_templates": ["default", "article", "full-width", "contact"], "blocks": [ - { "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main" }, "slot": "header", "sort_order": 0 }, - { "block_key": "coffee:hours_strip", "title": "Hours Strip", "content": { "todayLabel": "Today" }, "slot": "header", "sort_order": 1 }, + { "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main", "companyName": "Ritual Coffee" }, "slot": "header", "sort_order": 0 }, { "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "Pour something in here" }, "slot": "main", "sort_order": 0 }, - { "block_key": "coffee:footer", "title": "Site Footer", "content": { "showLocation": true }, "slot": "footer", "sort_order": 0 } + { "block_key": "footer", "title": "Site Footer", "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily." }, "slot": "footer", "sort_order": 0 } ] }, { @@ -15,12 +14,32 @@ "title": "Coffee Landing Master", "page_templates": ["landing"], "blocks": [ - { "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main" }, "slot": "header", "sort_order": 0 }, - { "block_key": "slot", "title": "Hero Slot", "content": { "slotName": "hero", "placeholder": "Drop a hero block" }, "slot": "hero", "sort_order": 0 }, - { "block_key": "coffee:featured_pour", "title": "Featured Pour", "content": {}, "slot": "menu", "sort_order": 0 }, - { "block_key": "slot", "title": "Story Slot", "content": { "slotName": "story", "placeholder": "Tell the story" }, "slot": "story", "sort_order": 0 }, - { "block_key": "coffee:location_card", "title": "Location Card", "content": {}, "slot": "cta", "sort_order": 0 }, - { "block_key": "coffee:footer", "title": "Site Footer", "content": { "showLocation": true }, "slot": "footer", "sort_order": 0 } + { "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main", "companyName": "Ritual Coffee" }, "slot": "hero", "sort_order": 0 }, + { "block_key": "slot", "title": "Hero", "content": { "slotName": "hero", "placeholder": "Drop a hero block" }, "slot": "hero", "sort_order": 1 }, + { "block_key": "slot", "title": "Menu", "content": { "slotName": "menu", "placeholder": "Feature the menu" }, "slot": "menu", "sort_order": 0 }, + { "block_key": "slot", "title": "Story", "content": { "slotName": "story", "placeholder": "Tell the story" }, "slot": "story", "sort_order": 0 }, + { "block_key": "slot", "title": "Call To Action", "content": { "slotName": "cta", "placeholder": "Invite them in" }, "slot": "cta", "sort_order": 0 }, + { "block_key": "footer", "title": "Site Footer", "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily." }, "slot": "footer", "sort_order": 0 } + ] + }, + { + "key": "coffee:blog-master", + "title": "Coffee Blog Master", + "page_templates": ["blog-index"], + "blocks": [ + { "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main", "companyName": "Ritual Coffee" }, "slot": "header", "sort_order": 0 }, + { "block_key": "slot", "title": "Featured", "content": { "slotName": "featured", "placeholder": "Featured posts" }, "slot": "featured", "sort_order": 0 }, + { "block_key": "slot", "title": "Posts", "content": { "slotName": "main", "placeholder": "Post grid" }, "slot": "main", "sort_order": 0 }, + { "block_key": "footer", "title": "Site Footer", "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily." }, "slot": "footer", "sort_order": 0 } + ] + }, + { + "key": "coffee:auth-master", + "title": "Coffee Auth Master", + "page_templates": ["auth"], + "blocks": [ + { "block_key": "slot", "title": "Auth Form", "content": { "slotName": "main", "placeholder": "Auth form" }, "slot": "main", "sort_order": 0 }, + { "block_key": "slot", "title": "Footer Note", "content": { "slotName": "footer", "placeholder": "Small print" }, "slot": "footer", "sort_order": 0 } ] } ] diff --git a/plugin.mod b/plugin.mod index 99a6283..f923636 100644 --- a/plugin.mod +++ b/plugin.mod @@ -7,3 +7,4 @@ description = "Warm, hand-crafted theme for cafes, bakeries, and slow-craft make kind = "theme" categories = ["templates"] tags = ["warm", "hospitality", "cafe", "bakery", "food", "handcraft", "organic", "artisan", "menu"] +required_icon_packs = ["lucide", "simple-icons"] diff --git a/presets.json b/presets.json index 9a12e66..57c2aa1 100644 --- a/presets.json +++ b/presets.json @@ -2,9 +2,9 @@ { "id": "morning-pour", "name": "Morning Pour", - "description": "Cream paper, espresso ink, terracotta accent", + "description": "Cream paper and espresso ink with a terracotta accent. Daylight cafe.", "theme": { - "mode": "light", + "mode": "both", "lightColors": { "background": "36 30% 96%", "foreground": "25 35% 18%", @@ -52,9 +52,9 @@ { "id": "dark-roast", "name": "Dark Roast", - "description": "Espresso-deep base with copper accent for evening cafes and roasteries", + "description": "Espresso-deep base with a copper accent. Evening roastery.", "theme": { - "mode": "dark", + "mode": "both", "lightColors": { "background": "36 30% 96%", "foreground": "25 35% 18%", @@ -102,9 +102,9 @@ { "id": "kraft-cream", "name": "Kraft Cream", - "description": "Warmer ivory paper with deeper terracotta accent", + "description": "Warmer ivory paper with a deeper terracotta accent. Bakery counter.", "theme": { - "mode": "light", + "mode": "both", "lightColors": { "background": "32 35% 92%", "foreground": "22 40% 16%", @@ -148,5 +148,105 @@ "ring": "16 50% 55%" } } + }, + { + "id": "chalkboard", + "name": "Chalkboard", + "description": "Slate-black menu board with chalk-white lettering and a chalk-cream accent.", + "theme": { + "mode": "both", + "lightColors": { + "background": "40 20% 94%", + "foreground": "200 12% 16%", + "card": "40 18% 90%", + "cardForeground": "200 12% 16%", + "popover": "40 18% 90%", + "popoverForeground": "200 12% 16%", + "primary": "195 22% 26%", + "primaryForeground": "40 30% 96%", + "secondary": "40 16% 86%", + "secondaryForeground": "200 12% 16%", + "muted": "40 14% 88%", + "mutedForeground": "200 10% 34%", + "accent": "28 55% 48%", + "accentForeground": "40 30% 96%", + "destructive": "0 68% 46%", + "destructiveForeground": "40 30% 96%", + "border": "200 10% 78%", + "input": "200 10% 78%", + "ring": "195 22% 26%" + }, + "darkColors": { + "background": "195 18% 12%", + "foreground": "40 34% 92%", + "card": "195 16% 15%", + "cardForeground": "40 34% 92%", + "popover": "195 16% 15%", + "popoverForeground": "40 34% 92%", + "primary": "40 34% 90%", + "primaryForeground": "195 18% 12%", + "secondary": "195 14% 20%", + "secondaryForeground": "40 34% 92%", + "muted": "195 12% 18%", + "mutedForeground": "40 18% 72%", + "accent": "30 70% 62%", + "accentForeground": "195 18% 12%", + "destructive": "0 62% 55%", + "destructiveForeground": "40 34% 92%", + "border": "195 12% 24%", + "input": "195 12% 22%", + "ring": "40 34% 90%" + } + } + }, + { + "id": "matcha-oat", + "name": "Matcha and Oat", + "description": "Soft sage green and oat cream for tea rooms and matcha bars.", + "theme": { + "mode": "both", + "lightColors": { + "background": "44 32% 94%", + "foreground": "120 15% 18%", + "card": "44 28% 91%", + "cardForeground": "120 15% 18%", + "popover": "44 28% 91%", + "popoverForeground": "120 15% 18%", + "primary": "132 26% 32%", + "primaryForeground": "44 32% 94%", + "secondary": "80 18% 84%", + "secondaryForeground": "120 15% 18%", + "muted": "80 16% 87%", + "mutedForeground": "120 12% 34%", + "accent": "36 60% 48%", + "accentForeground": "44 32% 94%", + "destructive": "0 68% 46%", + "destructiveForeground": "44 32% 94%", + "border": "90 14% 76%", + "input": "90 14% 76%", + "ring": "132 26% 32%" + }, + "darkColors": { + "background": "130 16% 11%", + "foreground": "44 30% 91%", + "card": "130 14% 14%", + "cardForeground": "44 30% 91%", + "popover": "130 14% 14%", + "popoverForeground": "44 30% 91%", + "primary": "128 34% 60%", + "primaryForeground": "130 16% 11%", + "secondary": "130 12% 20%", + "secondaryForeground": "44 30% 91%", + "muted": "130 10% 18%", + "mutedForeground": "80 14% 68%", + "accent": "38 72% 60%", + "accentForeground": "130 16% 11%", + "destructive": "0 60% 55%", + "destructiveForeground": "44 30% 91%", + "border": "130 10% 24%", + "input": "130 10% 22%", + "ring": "128 34% 60%" + } + } } ] diff --git a/seed/seed.json b/seed/seed.json new file mode 100644 index 0000000..ed6acea --- /dev/null +++ b/seed/seed.json @@ -0,0 +1,177 @@ +{ + "demo": true, + "settings": { + "merge": { + "site_title": "Ritual Coffee", + "site_description": "A neighbourhood cafe and small-batch roastery." + } + }, + "data_tables": [ + { + "key": "contact_submissions", + "name": "Contact Submissions", + "description": "Messages sent from the cafe contact form.", + "primary_key": "id", + "columns": [ + { "key": "name", "label": "Name", "type": "text", "required": true }, + { "key": "email", "label": "Email", "type": "email", "required": true }, + { "key": "reason", "label": "Reason", "type": "text", "required": false }, + { "key": "message", "label": "Message", "type": "text", "required": true } + ] + } + ], + "menu_items": [ + { "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 0 }, + { "menu": "main", "label": "Menu", "url": "/#menu", "sort_order": 1 }, + { "menu": "main", "label": "About", "page_slug": "/about", "sort_order": 2 }, + { "menu": "main", "label": "Journal", "page_slug": "/blog", "sort_order": 3 }, + { "menu": "main", "label": "Visit", "page_slug": "/contact", "sort_order": 4 } + ], + "pages": [ + { + "slug": "/", + "title": "Ritual Coffee", + "template_key": "default", + "blocks": [ + { "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Ritual Coffee", "ctaLabel": "Order ahead", "ctaUrl": "/contact" } }, + { "block_key": "hero", "title": "Hero", "slot": "main", "sort_order": 0, "content": { "template": "centered", "eyebrow": "Open from 7", "headline": "Coffee worth slowing down for", "subheadline": "We roast in small batches and pull every shot to order.", "ctaPrimary": { "text": "See the menu", "url": "/#menu" }, "ctaSecondary": { "text": "Find us", "url": "/contact" }, "minHeight": "50vh" } }, + { "block_key": "coffee:featured_pour", "title": "Featured Pour", "slot": "main", "sort_order": 1, "content": { "label": "This week", "origin": "Single origin", "name": "Guji Highlands", "tasting": "

Bright and floral. Notes of peach, jasmine and brown sugar.

", "price": "6.00", "currency": "$" } }, + { "block_key": "menu-list", "title": "Menu", "slot": "main", "sort_order": 2, "content": { "heading": "On the board", "intro": "Prices are for takeaway. Add fifty cents to dine in.", "currency": "$", "columns": 2, "sections": [ + { "title": "Espresso", "items": [ + { "name": "Espresso", "description": "Double shot", "price": "4.00" }, + { "name": "Flat white", "price": "5.00" }, + { "name": "Latte", "price": "5.50" }, + { "name": "Batch filter", "description": "Rotating single origin", "price": "5.00", "badges": [{ "label": "Filter" }] } + ] }, + { "title": "Kitchen", "items": [ + { "name": "Butter croissant", "price": "5.50" }, + { "name": "Sourdough toast", "description": "With jam or honey", "price": "6.00" }, + { "name": "Bacon and egg roll", "price": "12.00" }, + { "name": "Granola bowl", "description": "Oats, yoghurt, seasonal fruit", "price": "11.00", "badges": [{ "label": "V" }] } + ] } + ] } }, + { "block_key": "hours-location", "title": "Hours", "slot": "main", "sort_order": 3, "content": { "heading": "Come and sit", "address": "12 Baker Lane\nNorthbridge", "phone": "08 1234 5678", "hours": [ + { "day": "Monday", "hours": "7:00 - 15:00" }, + { "day": "Tuesday", "hours": "7:00 - 15:00" }, + { "day": "Wednesday", "hours": "7:00 - 15:00" }, + { "day": "Thursday", "hours": "7:00 - 15:00" }, + { "day": "Friday", "hours": "7:00 - 16:00" }, + { "day": "Saturday", "hours": "8:00 - 16:00" }, + { "day": "Sunday", "closed": true } + ] } }, + { "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily.", "address": "12 Baker Lane, Northbridge", "email": "hello@ritual.coffee", "copyright": "Ritual Coffee" } } + ] + }, + { + "slug": "/about", + "title": "About", + "template_key": "default", + "blocks": [ + { "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Ritual Coffee", "ctaLabel": "Order ahead", "ctaUrl": "/contact" } }, + { "block_key": "hero", "title": "Hero", "slot": "main", "sort_order": 0, "content": { "template": "centered", "eyebrow": "Our story", "headline": "One roaster, one room", "subheadline": "We started on a single machine and never grew out of caring about it.", "minHeight": "auto" } }, + { "block_key": "rich-section", "title": "Story", "slot": "main", "sort_order": 1, "content": { "eyebrow": "Why we roast", "heading": "Fresh, not fast", "body": "

We roast twice a week and rest every batch before it hits the hopper. That means the coffee you drink was roasted days ago, not months.

Everything on the board is made in-house. The croissants are laminated overnight. The jam is cooked in small pots on the back bench.

", "mediaPosition": "right" } }, + { "block_key": "coffee:brew_guide", "title": "Brew Guide", "slot": "main", "sort_order": 2, "content": { "kicker": "Make it at home", "method": "V60 pour-over", "intro": "This is how we brew filter on the bar. Scale it up or down by the ratio.", "ratio": "1:16", "grind": "Medium-fine", "water": "300g", "temperature": "94C", "time": "3:00", "steps": [ + { "title": "Rinse and dose", "text": "Rinse the paper. Add 19g of coffee and level the bed." }, + { "title": "Bloom", "text": "Pour 45g of water. Wait 40 seconds." }, + { "title": "Pour in stages", "text": "Pour in slow circles to 300g by two minutes." }, + { "title": "Draw down", "text": "Let it finish by three minutes. Swirl and serve." } + ] } }, + { "block_key": "team", "title": "Team", "slot": "main", "sort_order": 3, "content": { "heading": "Behind the bar", "members": [ + { "name": "Mara", "role": "Head roaster", "bio": "Runs the roaster and tastes every batch." }, + { "name": "Theo", "role": "Baker", "bio": "In before the birds. Owns the croissants." }, + { "name": "Priya", "role": "Barista", "bio": "Knows your order before you say it." } + ] } }, + { "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily.", "email": "hello@ritual.coffee", "copyright": "Ritual Coffee" } } + ] + }, + { + "slug": "/blog", + "title": "Journal", + "template_key": "blog-index", + "blocks": [ + { "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Ritual Coffee", "ctaLabel": "Order ahead", "ctaUrl": "/contact" } }, + { "block_key": "blog-hero", "title": "Journal Hero", "slot": "featured", "sort_order": 0, "content": { "title": "The Journal", "subtitle": "Notes from the roaster and the bar." } }, + { "block_key": "card", "title": "Post: Guji", "slot": "main", "sort_order": 0, "content": { "title": "Meet the Guji Highlands", "text": "Our new single origin is bright, floral and easy to love.", "link": "/blog/meet-the-guji-highlands", "linkLabel": "Read on" } }, + { "block_key": "card", "title": "Post: Roasting week", "slot": "main", "sort_order": 1, "content": { "title": "A week on the roaster", "text": "How we plan a roasting week and why we rest every batch.", "link": "/blog/a-week-on-the-roaster", "linkLabel": "Read on" } }, + { "block_key": "card", "title": "Post: Filter", "slot": "main", "sort_order": 2, "content": { "title": "How we brew filter", "text": "The recipe we use on the bar, step by step.", "link": "/blog/how-we-brew-filter", "linkLabel": "Read on" } }, + { "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily.", "email": "hello@ritual.coffee", "copyright": "Ritual Coffee" } } + ] + }, + { + "slug": "/blog/meet-the-guji-highlands", + "parent_slug": "/blog", + "title": "Meet the Guji Highlands", + "template_key": "article", + "blocks": [ + { "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Ritual Coffee", "ctaLabel": "Order ahead", "ctaUrl": "/contact" } }, + { "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 0, "content": { "level": 1, "text": "Meet the Guji Highlands" } }, + { "block_key": "text", "title": "Body", "slot": "main", "sort_order": 1, "content": { "text": "

Our new single origin comes from the Guji zone in southern Ethiopia. It is washed, which keeps it clean and bright.

We taste peach and jasmine up front, then a soft brown sugar finish. It works as espresso and shines as filter.

Come in this week and try it on the bar.

" } }, + { "block_key": "coffee:roast_profile", "title": "Roast Profile", "slot": "main", "sort_order": 2, "content": { "origin": "Single origin", "name": "Guji Highlands", "producer": "Smallholders, Guji zone", "roastLevel": "Medium", "notes": "

Peach, jasmine, brown sugar.

", "process": "Washed", "varietal": "Heirloom", "altitude": "1900-2100 masl" } }, + { "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily.", "email": "hello@ritual.coffee", "copyright": "Ritual Coffee" } } + ] + }, + { + "slug": "/blog/a-week-on-the-roaster", + "parent_slug": "/blog", + "title": "A week on the roaster", + "template_key": "article", + "blocks": [ + { "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Ritual Coffee", "ctaLabel": "Order ahead", "ctaUrl": "/contact" } }, + { "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 0, "content": { "level": 1, "text": "A week on the roaster" } }, + { "block_key": "text", "title": "Body", "slot": "main", "sort_order": 1, "content": { "text": "

We roast on Tuesdays and Fridays. Each batch is small, and we rest it before it goes on the bar.

Resting lets the coffee settle after roasting. Pulled too soon, it tastes sharp and gassy. Given a few days, it opens up.

That is why the date on the bag matters more than the date on the door.

" } }, + { "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily.", "email": "hello@ritual.coffee", "copyright": "Ritual Coffee" } } + ] + }, + { + "slug": "/blog/how-we-brew-filter", + "parent_slug": "/blog", + "title": "How we brew filter", + "template_key": "article", + "blocks": [ + { "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Ritual Coffee", "ctaLabel": "Order ahead", "ctaUrl": "/contact" } }, + { "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 0, "content": { "level": 1, "text": "How we brew filter" } }, + { "block_key": "text", "title": "Body", "slot": "main", "sort_order": 1, "content": { "text": "

Filter is the clearest way to taste a coffee. We keep the recipe simple so you can repeat it at home.

Weigh the coffee and the water. Keep the grind even. Pour in slow circles and give it room to draw down.

" } }, + { "block_key": "coffee:brew_guide", "title": "Brew Guide", "slot": "main", "sort_order": 2, "content": { "kicker": "The recipe", "method": "V60 pour-over", "ratio": "1:16", "grind": "Medium-fine", "water": "300g", "temperature": "94C", "time": "3:00", "steps": [ + { "title": "Rinse and dose", "text": "Rinse the paper. Add 19g and level the bed." }, + { "title": "Bloom", "text": "Pour 45g. Wait 40 seconds." }, + { "title": "Pour", "text": "Slow circles to 300g by two minutes." }, + { "title": "Finish", "text": "Draw down by three minutes. Serve." } + ] } }, + { "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily.", "email": "hello@ritual.coffee", "copyright": "Ritual Coffee" } } + ] + }, + { + "slug": "/contact", + "title": "Visit", + "template_key": "contact", + "blocks": [ + { "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Ritual Coffee", "ctaLabel": "Order ahead", "ctaUrl": "/contact" } }, + { "block_key": "hours-location", "title": "Hours", "slot": "main", "sort_order": 0, "content": { "heading": "Find us", "address": "12 Baker Lane\nNorthbridge", "phone": "08 1234 5678", "hours": [ + { "day": "Monday", "hours": "7:00 - 15:00" }, + { "day": "Tuesday", "hours": "7:00 - 15:00" }, + { "day": "Wednesday", "hours": "7:00 - 15:00" }, + { "day": "Thursday", "hours": "7:00 - 15:00" }, + { "day": "Friday", "hours": "7:00 - 16:00" }, + { "day": "Saturday", "hours": "8:00 - 16:00" }, + { "day": "Sunday", "closed": true } + ] } }, + { "block_key": "contact-form", "title": "Contact Form", "slot": "main", "sort_order": 1, "content": { "heading": "Say hello", "description": "Booking a table for six or more? Send us a note and we will sort it.", "submitLabel": "Send message", "successMessage": "Thanks. We will get back to you soon.", "formConfig": { "targetTable": "contact_submissions" }, "fields": [ + { "name": "name", "label": "Name", "type": "text", "required": true, "width": "half" }, + { "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" }, + { "name": "reason", "label": "Reason", "type": "text", "required": false, "width": "full" }, + { "name": "message", "label": "Message", "type": "textarea", "required": true, "width": "full" } + ] } }, + { "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Ritual Coffee", "tagline": "Small-batch coffee, baked fresh daily.", "email": "hello@ritual.coffee", "copyright": "Ritual Coffee" } } + ] + }, + { + "slug": "/login", + "title": "Login", + "template_key": "auth", + "blocks": [ + { "block_key": "auth-form", "title": "Auth Form", "slot": "main", "sort_order": 0, "content": { "default_tab": "login", "show_social": false } }, + { "block_key": "text", "title": "Footer Note", "slot": "footer", "sort_order": 0, "content": { "text": "

Members get first pick of new roasts.

" } } + ] + } + ] +} diff --git a/seed/workflows.json b/seed/workflows.json new file mode 100644 index 0000000..c3aad46 --- /dev/null +++ b/seed/workflows.json @@ -0,0 +1,25 @@ +{ + "workflows": [ + { + "key": "notify_admins_on_contact", + "name": "Notify admins on contact", + "description": "Email site admins when someone submits the cafe contact form.", + "enabled": true, + "trigger": { + "type": "row_inserted", + "table": "contact_submissions" + }, + "steps": [ + { + "type": "email", + "name": "Email admins", + "template": "Form Submission Notification", + "recipients": { + "mode": "admins", + "scope": "all" + } + } + ] + } + ] +} diff --git a/templates/coffee/article.ninjatpl b/templates/coffee/article.ninjatpl index 9214286..fc8881d 100644 --- a/templates/coffee/article.ninjatpl +++ b/templates/coffee/article.ninjatpl @@ -1,5 +1,5 @@ - + {{ head_html|safe }} {{ admin_banner_html|safe }} diff --git a/templates/coffee/auth.ninjatpl b/templates/coffee/auth.ninjatpl new file mode 100644 index 0000000..d9377a5 --- /dev/null +++ b/templates/coffee/auth.ninjatpl @@ -0,0 +1,19 @@ + + +{{ head_html|safe }} + + {{ admin_banner_html|safe }} +
+
+ + {% if site_settings.Logo %}{{ site_settings.LogoAlt|default:site_settings.Title }}{% else %}{{ site_settings.Title|default:"Coffee" }}{% endif %} + +
+ {% if slots.main %}{{ slots.main|safe }}{% else %}

No form assigned to this page.

{% endif %} +
+ {% if slots.footer %}
{{ slots.footer|safe }}
{% endif %} +
+
+ {{ body_end_html|safe }} + + diff --git a/templates/coffee/blog-index.ninjatpl b/templates/coffee/blog-index.ninjatpl new file mode 100644 index 0000000..7a59504 --- /dev/null +++ b/templates/coffee/blog-index.ninjatpl @@ -0,0 +1,15 @@ + + +{{ head_html|safe }} + + {{ admin_banner_html|safe }} +
{{ slots.header|safe }}
+
+ {% if title %}

{{ title }}

{% if page_meta.MetaDescription %}

{{ page_meta.MetaDescription }}

{% endif %}
{% endif %} + {% if slots.featured %}
{{ slots.featured|safe }}
{% endif %} + {% if slots.main %}
{{ slots.main|safe }}
{% else %}

No stories brewed yet.

{% endif %} +
+
{{ slots.footer|safe }}
+ {{ body_end_html|safe }} + + diff --git a/templates/coffee/contact.ninjatpl b/templates/coffee/contact.ninjatpl new file mode 100644 index 0000000..d468bae --- /dev/null +++ b/templates/coffee/contact.ninjatpl @@ -0,0 +1,13 @@ + + +{{ head_html|safe }} + + {{ admin_banner_html|safe }} +
{{ slots.header|safe }}
+
+ {% if slots.main %}{{ slots.main|safe }}{% else %}

Add a contact form and your hours.

{% endif %} +
+
{{ slots.footer|safe }}
+ {{ body_end_html|safe }} + + diff --git a/templates/coffee/default.ninjatpl b/templates/coffee/default.ninjatpl index 2604f5d..b2e07c5 100644 --- a/templates/coffee/default.ninjatpl +++ b/templates/coffee/default.ninjatpl @@ -1,11 +1,11 @@ - + {{ head_html|safe }} {{ admin_banner_html|safe }}
{{ slots.header|safe }}
-
- {% if slots.main %}{{ slots.main|safe }}{% else %}

Pour something in here.

{% endif %} +
+ {% if slots.main %}{{ slots.main|safe }}{% else %}

Pour something in here.

{% endif %}
{{ slots.footer|safe }}
{{ body_end_html|safe }} diff --git a/templates/coffee/full-width.ninjatpl b/templates/coffee/full-width.ninjatpl index 6217ba2..edc2771 100644 --- a/templates/coffee/full-width.ninjatpl +++ b/templates/coffee/full-width.ninjatpl @@ -1,5 +1,5 @@ - + {{ head_html|safe }} {{ admin_banner_html|safe }} diff --git a/templates/coffee/landing.ninjatpl b/templates/coffee/landing.ninjatpl index 3e19c1b..9247a91 100644 --- a/templates/coffee/landing.ninjatpl +++ b/templates/coffee/landing.ninjatpl @@ -1,5 +1,5 @@ - + {{ head_html|safe }} {{ admin_banner_html|safe }} diff --git a/templates/overrides/coffee/announcement-bar.ninjatpl b/templates/overrides/coffee/announcement-bar.ninjatpl new file mode 100644 index 0000000..f28ade7 --- /dev/null +++ b/templates/overrides/coffee/announcement-bar.ninjatpl @@ -0,0 +1,21 @@ +{# coffee announcement-bar — dismissable strip; dismissal persists in localStorage keyed by variant+message hash. #} +{% set anncls = "bg-accent/15 text-foreground" %} +{% if variant == "promo" %}{% set anncls = "bg-accent text-accent-foreground" %}{% elif variant == "alert" %}{% set anncls = "bg-destructive text-destructive-foreground" %}{% endif %} +
+
+

{{ message }}{% if linkText and linkUrl %} {{ linkText }}{% endif %}

+{% if dismissable %}{% endif %} +
+
+{% if dismissable %}{% endif %} diff --git a/templates/overrides/coffee/audio-embed.ninjatpl b/templates/overrides/coffee/audio-embed.ninjatpl new file mode 100644 index 0000000..09d813d --- /dev/null +++ b/templates/overrides/coffee/audio-embed.ninjatpl @@ -0,0 +1,15 @@ +
{% if artwork %}
{% img artwork alt=title|default:"" class="h-full w-full object-cover" %}
{% endif %}
{% if title %}

{{ title }}

{% endif %}{% if artist %}

{{ artist }}

{% endif %}
{% if download %} Download{% endif %}
diff --git a/templates/overrides/coffee/auth-form.ninjatpl b/templates/overrides/coffee/auth-form.ninjatpl new file mode 100644 index 0000000..a1c014b --- /dev/null +++ b/templates/overrides/coffee/auth-form.ninjatpl @@ -0,0 +1,36 @@ +{# coffee auth-form — combined login/register in kraft chrome. Endpoints and panel + ids match the platform auth flow (/api/auth/login, /api/auth/register, + #auth-message, #login-panel/#register-panel). Auth state via the auth_state + provider (auth.logged_in / auth.display_name). Captcha, when enabled, is + emitted by the platform auth path. #} +{% if auth.logged_in %} +
+

You are logged in as {{ auth.display_name }}

+ My account +
+{% else %} +{% set defaultTab = default_tab|default:"login" %} +
+
+
+ + +
+
+
+
+
+ +

Forgot password?

+
+
+
+
+
+
+
+ +
+
+
+{% endif %} diff --git a/templates/overrides/coffee/auth-status.ninjatpl b/templates/overrides/coffee/auth-status.ninjatpl new file mode 100644 index 0000000..5571626 --- /dev/null +++ b/templates/overrides/coffee/auth-status.ninjatpl @@ -0,0 +1,27 @@ +{# coffee auth-status — header login state. Auth state via the auth_state provider; + endpoints match the platform (/login, /login?tab=register, /api/auth/logout, + /api/auth/resend-verification). #} +{% if not auth.logged_in %} +
+ Login + Sign up +
+{% else %} +{% if not auth.email_verified %}
Please verify your email address.
{% endif %} +
+ + +
+ +{% endif %} diff --git a/templates/overrides/coffee/author-bio-hero.ninjatpl b/templates/overrides/coffee/author-bio-hero.ninjatpl new file mode 100644 index 0000000..55118fd --- /dev/null +++ b/templates/overrides/coffee/author-bio-hero.ninjatpl @@ -0,0 +1,20 @@ +{% if author %} +
+
+{% if showAvatar and author.avatar_url %}
{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}
{% endif %} +

{{ author.display_name }}

+{% if author.job_title or author.company %}

{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}

{% endif %} +{% if author.bio %}

{{ author.bio }}

{% endif %} +
+{% if showPostCount %}{{ author.post_count|default:0 }} post{% if author.post_count != 1 %}s{% endif %}{% endif %} +{% if author.location %}{{ author.location }}{% endif %} +
+{% if showSocial %}
+{% if author.website_url %}{% endif %} +{% if author.twitter_handle %}{% endif %} +{% if author.linkedin_url %}{% endif %} +{% if author.github_url %}{% endif %} +
{% endif %} +
+
+{% endif %} diff --git a/templates/overrides/coffee/author-bio.ninjatpl b/templates/overrides/coffee/author-bio.ninjatpl new file mode 100644 index 0000000..e633802 --- /dev/null +++ b/templates/overrides/coffee/author-bio.ninjatpl @@ -0,0 +1,12 @@ +{% if post.author_name %} + +{% endif %} diff --git a/templates/overrides/coffee/blog-hero.ninjatpl b/templates/overrides/coffee/blog-hero.ninjatpl new file mode 100644 index 0000000..c5692e6 --- /dev/null +++ b/templates/overrides/coffee/blog-hero.ninjatpl @@ -0,0 +1,22 @@ +
+
+
+{% if title %}

{{ title }}

{% endif %} +{% if subtitle %}

{{ subtitle }}

{% endif %} +
+{% if posts %} +
+{% for post in posts %} + +{% endfor %} +
+{% endif %} +
+
diff --git a/templates/overrides/coffee/breadcrumbs.ninjatpl b/templates/overrides/coffee/breadcrumbs.ninjatpl new file mode 100644 index 0000000..67d4440 --- /dev/null +++ b/templates/overrides/coffee/breadcrumbs.ninjatpl @@ -0,0 +1,12 @@ +{% set arrows = template == "arrows" %} + diff --git a/templates/overrides/coffee/button.ninjatpl b/templates/overrides/coffee/button.ninjatpl index 5747e56..239f4f0 100644 --- a/templates/overrides/coffee/button.ninjatpl +++ b/templates/overrides/coffee/button.ninjatpl @@ -1 +1,5 @@ -{% if url %}{{ text }}{% else %}{% endif %} +{% with align=align|default:"left" style=style|default:"primary" %} +
+{% if link %}{% if icon and iconPosition != "right" %}{% endif %}{{ label|default:"Learn more" }}{% if icon and iconPosition == "right" %}{% endif %}{% else %}{% endif %} +
+{% endwith %} diff --git a/templates/overrides/coffee/card.ninjatpl b/templates/overrides/coffee/card.ninjatpl new file mode 100644 index 0000000..6be4284 --- /dev/null +++ b/templates/overrides/coffee/card.ninjatpl @@ -0,0 +1,12 @@ +{% with layout=layout|default:"vertical" %} + +{% endwith %} diff --git a/templates/overrides/coffee/category-list.ninjatpl b/templates/overrides/coffee/category-list.ninjatpl new file mode 100644 index 0000000..ef3bdb5 --- /dev/null +++ b/templates/overrides/coffee/category-list.ninjatpl @@ -0,0 +1,17 @@ +{% set pills = style == "pills" %} +
+{% if heading %}

{{ heading }}

{% endif %} +{% if pills %} +
+{% for cat in categories %} +{{ cat.name }}{% if showCount %}{{ cat.post_count }}{% endif %} +{% endfor %} +
+{% else %} + +{% endif %} +
diff --git a/templates/overrides/coffee/contact-card.ninjatpl b/templates/overrides/coffee/contact-card.ninjatpl new file mode 100644 index 0000000..75423c2 --- /dev/null +++ b/templates/overrides/coffee/contact-card.ninjatpl @@ -0,0 +1,17 @@ +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if description %}

{{ description }}

{% endif %} +
+ {% for channel in channels %} +
+ {% if channel.icon %}{% endif %} +
+ {% if channel.label %}
{{ channel.label }}
{% endif %} + {% if channel.value %}
{% if channel.link %}{{ channel.value }}{% else %}{{ channel.value }}{% endif %}
{% endif %} +
+
+ {% endfor %} +
+
+
diff --git a/templates/overrides/coffee/contact-form.ninjatpl b/templates/overrides/coffee/contact-form.ninjatpl new file mode 100644 index 0000000..2e3f6fc --- /dev/null +++ b/templates/overrides/coffee/contact-form.ninjatpl @@ -0,0 +1,39 @@ +{% with bid=_block_id|default:context.blockId %} +
+
+ {% if form.submitted %} +
+
+

{{ form.message|default:successMessage|default:"Thanks. Your message is on its way." }}

+
+ {% else %} + {% if heading %}

{{ heading }}

{% endif %} + {% if description %}

{{ description }}

{% endif %} + {% if form.errors._form %}
{{ form.errors._form }}
{% endif %} + {% with formAction="/api/blocks/"|add:bid|add:"/submit" formTarget="#contact-form-"|add:bid %} + {% form action=formAction hx_target=formTarget hx_swap="outerHTML" class="mt-8 grid grid-cols-1 gap-5 sm:grid-cols-2" %} + {% for field in fields %} +
+ {% if field.label %}{% endif %} + {% if field.type == "textarea" %} + + {% elif field.type == "select" %} + + {% else %} + + {% endif %} +
+ {% endfor %} + +
+ +
+ {% endform %} + {% endwith %} + {% endif %} +
+
+{% endwith %} diff --git a/templates/overrides/coffee/countdown.ninjatpl b/templates/overrides/coffee/countdown.ninjatpl new file mode 100644 index 0000000..3f0d30c --- /dev/null +++ b/templates/overrides/coffee/countdown.ninjatpl @@ -0,0 +1,42 @@ +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+
+
--{{ labelDays|default:"Days" }}
+
--{{ labelHours|default:"Hours" }}
+
--{{ labelMinutes|default:"Minutes" }}
+
--{{ labelSeconds|default:"Seconds" }}
+
+ +
+ +
+
+ diff --git a/templates/overrides/coffee/cta.ninjatpl b/templates/overrides/coffee/cta.ninjatpl new file mode 100644 index 0000000..0e55039 --- /dev/null +++ b/templates/overrides/coffee/cta.ninjatpl @@ -0,0 +1,14 @@ +
+
+
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if text %}

{{ text }}

{% endif %} +
+
+ {% if primaryUrl and primaryLabel %}{{ primaryLabel }}{% endif %} + {% if secondaryUrl and secondaryLabel %}{{ secondaryLabel }}{% endif %} +
+
+
+
diff --git a/templates/overrides/coffee/divider.ninjatpl b/templates/overrides/coffee/divider.ninjatpl new file mode 100644 index 0000000..44c34aa --- /dev/null +++ b/templates/overrides/coffee/divider.ninjatpl @@ -0,0 +1,17 @@ +{% with variant=variant|default:"line" width=width|default:"full" %} + +{% endwith %} diff --git a/templates/overrides/coffee/event-list.ninjatpl b/templates/overrides/coffee/event-list.ninjatpl new file mode 100644 index 0000000..016723a --- /dev/null +++ b/templates/overrides/coffee/event-list.ninjatpl @@ -0,0 +1,40 @@ +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} + {% if layout == "cards" %} +
+ {% for event in events %}{% if not (hidePast and event.past) %} +
+ {% if event.media %}
{% img event.media alt=event.title class="h-full w-full object-cover" %}
{% endif %} +
+
{{ event.start }}{% if event.end %} – {{ event.end }}{% endif %}{% if event.timezone %} {{ event.timezone }}{% endif %}
+

{{ event.title }}

+ {% if event.venue %}

{{ event.venue }}

{% endif %} + {% if event.description %}

{{ event.description }}

{% endif %} + {% if event.link %}{{ event.linkLabel|default:"Details" }}{% endif %} +
+
+ {% endif %}{% endfor %} +
+ {% else %} +
    + {% for event in events %}{% if not (hidePast and event.past) %} +
  • +
    +
    {{ event.start }}
    + {% if event.end %}
    to {{ event.end }}
    {% endif %} + {% if event.timezone %}
    {{ event.timezone }}
    {% endif %} +
    +
    +

    {{ event.title }}

    + {% if event.venue %}

    {{ event.venue }}

    {% endif %} + {% if event.description %}

    {{ event.description }}

    {% endif %} +
    + {% if event.link %}{{ event.linkLabel|default:"Details" }}{% endif %} +
  • + {% endif %}{% endfor %} +
+ {% endif %} +
+
diff --git a/templates/overrides/coffee/faq.ninjatpl b/templates/overrides/coffee/faq.ninjatpl new file mode 100644 index 0000000..3d7e4fe --- /dev/null +++ b/templates/overrides/coffee/faq.ninjatpl @@ -0,0 +1,17 @@ +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+ {% for item in items %} +
+ + {{ item.question }} + + +
{{ item.answer|safe }}
+
+ {% endfor %} +
+
+
diff --git a/templates/overrides/coffee/feature-grid.ninjatpl b/templates/overrides/coffee/feature-grid.ninjatpl new file mode 100644 index 0000000..f8d935b --- /dev/null +++ b/templates/overrides/coffee/feature-grid.ninjatpl @@ -0,0 +1,17 @@ +
+
+ {% if eyebrow %}

{{ eyebrow }}

{% endif %} + {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+ {% for item in items %} +
+ {% if item.icon %}
{% endif %} +

{% if item.linkUrl %}{{ item.title }}{% else %}{{ item.title }}{% endif %}

+ {% if item.text %}

{{ item.text }}

{% endif %} + {% if item.linkUrl and item.linkLabel %}{{ item.linkLabel }}{% endif %} +
+ {% endfor %} +
+
+
diff --git a/templates/overrides/coffee/featured-posts.ninjatpl b/templates/overrides/coffee/featured-posts.ninjatpl new file mode 100644 index 0000000..d55f640 --- /dev/null +++ b/templates/overrides/coffee/featured-posts.ninjatpl @@ -0,0 +1,21 @@ +
+
+{% if heading %}

{{ heading }}

{% endif %} +
+{% for post in posts %} +
+{% if showFeaturedImage and post.featured_image_url %}{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}{% endif %} +
+

{{ post.title }}

+{% if showExcerpt and post.excerpt %}

{{ post.excerpt }}

{% endif %} +
+{% if showAuthor and post.author_name %}{{ post.author_name }}{% endif %} +{% if showDate and post.published_at %}{{ post.published_at|date:"M j, Y" }}{% endif %} +{% if post.reading_time %}{{ post.reading_time }} min{% endif %} +
+
+
+{% endfor %} +
+
+
diff --git a/templates/overrides/coffee/footer.ninjatpl b/templates/overrides/coffee/footer.ninjatpl new file mode 100644 index 0000000..393f97b --- /dev/null +++ b/templates/overrides/coffee/footer.ninjatpl @@ -0,0 +1,38 @@ +{# coffee footer — torn-paper top edge, kraft chrome, builtin footer_menus provider. #} +{% set fl = layout %} +{% if not fl %}{% if template == "centered" %}{% set fl = "centered" %}{% else %}{% set fl = "columns" %}{% endif %}{% endif %} +{% macro sociallinks(cls) %}{% if social_items %}{% endif %}{% endmacro %} +{% macro newsletter() %}{% if showNewsletter %}

{{ newsletterTitle|default:"Stay in the loop" }}

{% if newsletterDescription %}

{{ newsletterDescription }}

{% endif %}
{% signup_form list=newsletterListSlug|default:"main" source="footer" submit=newsletterButtonText|default:"Subscribe" %}
{% endif %}{% endmacro %} +{% macro legalbar() %}

{% if copyright %}© {{ copyright }}{% else %}© {{ companyName }}{% endif %}

{% if legal_items %}{% endif %}
{% endmacro %} +
+
+{% if fl == "minimal" %} +
+
{% if logo %}{{ companyName }}{% elif companyName %}{{ companyName }}{% endif %}{% if tagline %}{% endif %}
+{{ sociallinks("") }} +
+{{ legalbar() }} +{% elif fl == "centered" %} +
+{% if logo %}{{ companyName }}{% elif companyName %}{{ companyName }}{% endif %} +{% if tagline %}

{{ tagline }}

{% endif %} +{% if columns %}{% endif %} +{{ newsletter() }} +{{ sociallinks("justify-center") }} +
+{{ legalbar() }} +{% else %} +
+
+{% if logo %}{{ companyName }}{% elif companyName %}{{ companyName }}{% endif %} +{% if tagline %}

{{ tagline }}

{% endif %} +{% if address or email or phone %}
{% if address %}
{{ address }}
{% endif %}{% if email %}{% endif %}{% if phone %}{% endif %}
{% endif %} +{{ sociallinks("mt-5") }} +
+{% for col in columns %}

{{ col.header }}

{% endfor %} +{% if showNewsletter %}
{{ newsletter() }}
{% endif %} +
+{{ legalbar() }} +{% endif %} +
+
diff --git a/templates/overrides/coffee/gallery.ninjatpl b/templates/overrides/coffee/gallery.ninjatpl new file mode 100644 index 0000000..76b306d --- /dev/null +++ b/templates/overrides/coffee/gallery.ninjatpl @@ -0,0 +1,9 @@ +{% with layout=layout|default:"grid" gap=gap|default:"md" cols=columns|default:3 %} + +{% endwith %} diff --git a/templates/overrides/coffee/heading.ninjatpl b/templates/overrides/coffee/heading.ninjatpl index c1e1f92..e89342c 100644 --- a/templates/overrides/coffee/heading.ninjatpl +++ b/templates/overrides/coffee/heading.ninjatpl @@ -1 +1,6 @@ -{% if level|integer == 1 %}

{{ text }}

{% elif level|integer == 3 %}

{{ text }}

{% elif level|integer == 4 %}

{{ text }}

{% elif level|integer == 5 %}
{{ text }}
{% elif level|integer == 6 %}
{{ text }}
{% else %}

{{ text }}

{% endif %} +{% if level == 1 %}

{{ text }}

+{% elif level == 3 %}

{{ text }}

+{% elif level == 4 %}

{{ text }}

+{% elif level == 5 %}
{{ text }}
+{% elif level == 6 %}
{{ text }}
+{% else %}

{{ text }}

{% endif %} diff --git a/templates/overrides/coffee/hero.ninjatpl b/templates/overrides/coffee/hero.ninjatpl new file mode 100644 index 0000000..149c6ae --- /dev/null +++ b/templates/overrides/coffee/hero.ninjatpl @@ -0,0 +1,36 @@ +{# coffee hero — paper chrome; styled-text fields accept {text} object OR bare string. #} +{% set eyebrowText = eyebrow.text|default:eyebrow %} +{% set headlineText = headline.text|default:headline %} +{% set subText = subheadline.text|default:subheadline %} +{% set descText = description.text|default:description %} +{% set pText = ctaPrimary.text %} +{% set pUrl = ctaPrimary.url %} +{% set sText = ctaSecondary.text %} +{% set sUrl = ctaSecondary.url %} +{% set variant = template|default:"centered" %} +{% set hasImage = backgroundImage %} +{% set showOverlay = hasImage and overlayColor != "none" and overlayColor != "" %} +{% macro herocontent(align) %} +{% if eyebrowText %}{{ eyebrowText }}{% endif %} +{% if headlineText %}

{{ headlineText }}

{% endif %} +{% if subText %}

{{ subText }}

{% endif %} +{% if descText %}

{{ descText }}

{% endif %} +{% if pText or sText %}
+{% if pText and pUrl %}{{ pText }}{% endif %} +{% if sText and sUrl %}{{ sText }}{% endif %} +
{% endif %} +{% if trustLogos %}

Regulars love

{% for logo in trustLogos %}{% if logo.url %}{% img logo.image alt=logo.alt class="h-8 w-auto" %}{% else %}{% img logo.image alt=logo.alt class="h-8 w-auto opacity-60" %}{% endif %}{% endfor %}
{% endif %} +{% endmacro %} +{% if variant == "split" %} +
+
{% if hasImage %}{% img backgroundImage alt=headlineText class="absolute inset-0 h-full w-full object-cover" layout="hero" %}{% endif %}
+
{{ herocontent("left") }}
+
+{% else %} +{% set align = "center" %} +{% if variant == "left-aligned" %}{% set align = "left" %}{% endif %} +
+{% if hasImage %}
{% img backgroundImage alt=headlineText class="h-full w-full object-cover" layout="hero" %}{% if showOverlay %}
{% endif %}
{% endif %} +
{{ herocontent(align) }}
+
+{% endif %} diff --git a/templates/overrides/coffee/hours-location.ninjatpl b/templates/overrides/coffee/hours-location.ninjatpl new file mode 100644 index 0000000..954f336 --- /dev/null +++ b/templates/overrides/coffee/hours-location.ninjatpl @@ -0,0 +1,40 @@ +
+
+ {% if heading %}

{{ heading }}

{% endif %} +
+
+ {% if hours %} + + + {% for row in hours %} + + + + + {% endfor %} + +
{{ row.day }}{% if row.closed %}Closed{% else %}{{ row.hours }}{% endif %}
+ {% endif %} + {% if specialHoursNote %}

{{ specialHoursNote }}

{% endif %} + {% if address %}
Find us
{{ address }}
{% endif %} + {% if phone %}
Call ahead
{{ phone }}
{% endif %} + {% if directionsUrl %}Get directions{% endif %} +
+ {% if mapEmbedUrl %} +
+ +
+ {% endif %} +
+
+
+ diff --git a/templates/overrides/coffee/image.ninjatpl b/templates/overrides/coffee/image.ninjatpl index 1040ab1..e0a5475 100644 --- a/templates/overrides/coffee/image.ninjatpl +++ b/templates/overrides/coffee/image.ninjatpl @@ -1,6 +1,8 @@ -
-
- {% if src %}{{ alt }}{% else %}
Add an image to render the torn-edge frame.
{% endif %} +{% with size=size|default:"full" align=align|default:"center" %} +
+
+ {% if src %}{% img src alt=alt class="block h-auto w-full rounded-sm object-cover" layout=size %}{% else %}
Add an image
{% endif %}
- {% if caption %}
{{ caption }}
{% endif %} + {% if caption %}
{{ caption }}
{% endif %}
+{% endwith %} diff --git a/templates/overrides/coffee/logo-strip.ninjatpl b/templates/overrides/coffee/logo-strip.ninjatpl new file mode 100644 index 0000000..669d575 --- /dev/null +++ b/templates/overrides/coffee/logo-strip.ninjatpl @@ -0,0 +1,15 @@ +
+
+ {% if heading %}

{{ heading }}

{% endif %} +
+
+ {% for logo in logos %} + {% if logo.url %}{% endif %} + {% img logo.image alt=logo.alt class="h-8 w-auto object-contain" %} + {% if logo.url %}{% endif %} + {% endfor %} +
+
+
+
+{% if marquee %}{% endif %} diff --git a/templates/overrides/coffee/menu-list.ninjatpl b/templates/overrides/coffee/menu-list.ninjatpl new file mode 100644 index 0000000..0948ee7 --- /dev/null +++ b/templates/overrides/coffee/menu-list.ninjatpl @@ -0,0 +1,31 @@ +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+
+ {% for section in sections %} + + {% endfor %} +
+ {% if footnote %}

{{ footnote }}

{% endif %} +
+
+
diff --git a/templates/overrides/coffee/navbar.ninjatpl b/templates/overrides/coffee/navbar.ninjatpl new file mode 100644 index 0000000..97f8b18 --- /dev/null +++ b/templates/overrides/coffee/navbar.ninjatpl @@ -0,0 +1,74 @@ +{# coffee navbar — paper-strip chrome over the builtin menu provider. One navbar per page (fixed drawer ids). #} +{% set nv = variant %} +{% if not nv %}{% if template == "centered" %}{% set nv = "centered-logo" %}{% else %}{% set nv = "standard" %}{% endif %}{% endif %} +{% set sb = stickyBehavior %} +{% if not sb %}{% if sticky %}{% set sb = "sticky" %}{% else %}{% set sb = "none" %}{% endif %}{% endif %} +{% set cta_text = cta.text|default:ctaLabel %} +{% set cta_url = cta.url|default:ctaUrl %} +{% macro navlink(item) %}{{ item.label }}{% endmacro %} +{% macro dropdown(item) %}
{% endmacro %} +{% macro brand(size) %}{% if logoType == "text" and logoText.text %}{{ logoText.text }}{% elif logo %}{{ alt|default:companyName }}{% elif companyName %}{{ companyName }}{% endif %}{% endmacro %} +{% macro ctabtn(extra) %}{% if cta_text %}{{ cta_text }}{% endif %}{% endmacro %} +